# hobots

SDK JavaScript/TypeScript da **Hobots** para robôs (RPAs) e integrações. **O agente é a própria tarefa**: o seu programa é o agente, ele faz uma coisa, e a plataforma passa a receber logs, itens processados, progresso, prints, erros e o "estou vivo" do robô.

- **`hobots.register(handler)`** — **modo sob demanda**: o robô fica online aguardando **solicitações** criadas no app (formulário do agente e agendamentos) e executa o handler a cada uma. Módulo **Trabalhos**.
- **`hobots.start(handler)`** — **modo sempre ligado**: o robô abre a própria execução ao iniciar e roda o handler (o seu loop) dentro dela; a plataforma só recebe logs, itens e progresso. Sem formulário, sem solicitante.

Uma chamada por processo: `register` **ou** `start`. Não há slug nem nome de task — o que o robô faz é o próprio agente, identificado pelo Client Secret.

---

## Instalação

```bash
npm install hobots
# ou: pnpm add hobots · yarn add hobots
```

```js
import hobots from 'hobots'; // ou: import { hobots, register, start } from 'hobots'
```

CommonJS: `const { hobots } = require('hobots')`.

---

## Em 30 segundos

```bash
export HOBOTS_CLIENT_SECRET=hb_<64 hex>   # Configurações → Agentes
export HOBOTS_INSTANCE_ID=vm-cliente-01   # opcional — default: hostname da máquina
export HOBOTS_ENVIRONMENT=staging         # opcional — default: production
```

```js
import hobots from 'hobots';

// sob demanda: uma execução por solicitação do app (formulário do agente ou agendamento)
hobots.register(async params => {
  hobots.log.info(`lote ${params.lote}`);
  hobots.progress.total(params.notas.length);
  for (const nota of params.notas) {
    await baixar(nota);
    hobots.items.succeeded({ numero_nota: nota }, { id: nota });
    hobots.progress.advance();
  }
});

process.on('SIGINT', () => void hobots.close(15_000).then(() => process.exit(0)));
```

Um robô que vive rodando usa `start` no lugar de `register` — o handler é o loop:

```js
hobots.start(async () => {
  while (true) {
    await hobots.sleep(60_000); // cancelável
    for (const arquivo of await novosArquivos()) hobots.items.succeeded({ arquivo });
    hobots.log.info('ciclo ok');
  }
});
```

O objeto **`hobots`** é global: `hobots.log`, `hobots.items`, `hobots.progress`, `hobots.transaction`… resolvem a execução **atual** de qualquer módulo do programa — sem passar contexto de mão em mão, mesmo com várias execuções em paralelo. Depois do `register()`/`start()` ele também existe como `globalThis.hobots`.

---

## Credenciais e identidade

| Env | Opção | O que é |
| --- | --- | --- |
| `HOBOTS_CLIENT_SECRET` | `clientSecret` | O **Client Secret do agente** (`hb_<64 hex>`), copiado em **Configurações → Agentes**. Viaja só no header `Authorization: Bearer`, nunca na URL. Ausente ou inválido → `register()`/`start()` lança na hora, citando a env (sem ecoar o valor). |
| `HOBOTS_INSTANCE_ID` | `instanceId` | Identidade **estável** desta instância do robô (`vm-cliente-01`). Sem ela, o hostname. Heartbeat e execuções convergem nela: **um agente só** no painel. |
| `HOBOTS_ENVIRONMENT` | `environment` | Ambiente lógico do robô (`development` \| `testing` \| `staging` \| `production`). Sem ela, `production`. |
| `HOBOTS_API_URL` | — | Só em desenvolvimento: aponta o SDK para outro host (`http://localhost:3001`). Em produção é sempre `https://api2.hobots.app`. |

A opção passada em `register()`/`start()` tem precedência sobre a env — útil para copiar e colar o snippet do app.

---

## Os dois modos

| | `register(handler, options?)` | `start(handler, options?)` |
| --- | --- | --- |
| Quem dispara | Uma **solicitação** no app (formulário do agente, ou agendamento) | O próprio robô, ao iniciar |
| `params` do handler | O JSON validado do formulário | `{}` |
| Quantas execuções | Uma por solicitação; `concurrency` em paralelo (1..5) | **Uma só** — o handler é o seu loop; quando ele retorna, a execução fecha |
| O processo | Fica vivo fazendo polling | Fica vivo pelo heartbeat, mesmo depois do handler retornar |
| Reexecutar pelo app | Sim | Não — só reiniciando o robô |
| No painel | Instância "sob demanda" | Instância "sempre ligado" |

**Um processo = um agente = uma chamada.** A segunda chamada a `register()`/`start()` lança (`o agente já foi iniciado`). Um robô que precisa monitorar uma pasta *e* atender solicitações são dois agentes — dois processos, cada um com o seu Client Secret.

> **Modo start e o limite de logs:** o servidor guarda no máximo **1000 linhas de log por execução**. Num loop que roda por dias, logue o que importa (ciclos com novidade, avisos, erros) e registre o trabalho em `hobots.items` — itens não têm teto por execução (só o armazenamento do plano limita). As steps de uma run que não termina só chegam à Central com `hobots.flush()` — chame-o ao fim de cada rodada que processou itens.

---

## O objeto `hobots`

| Membro | O que faz |
| --- | --- |
| `hobots.log.debug/info/warn/error/success(msg, ...rest)` | Log da execução, enviado incrementalmente ao app (e ecoado no terminal como `[hobots] INFO …`). Aceita vários argumentos como `console.log` — `hobots.log.info('nota', numero, 'ok')` — cada um vira texto, unidos por espaço. Um `hobots.image(...)` como **último** argumento pendura um print na linha. **Nunca lança** — pode ir em `finally`. |
| `hobots.items.succeeded(payload?, { id, image }?)` · `.failed(msg, payload?, …)` · `.occurrence(msg, payload?, …)` · `.add({...})` | **A entrega do robô**: cada item processado, com status e atributos (viram colunas no app — declare-as com a opção `items: { columns: [...] }` do `register`/`start`). `image` pendura um print no item. Bufferizado, em lotes. |
| `hobots.items.each(lista, async (el, item) => …, { id, payload }?)` | **Loop com o contrato garantido**: cada elemento vira step + item; exceção no handler = item `failed` com a mensagem e o loop **segue** até o fim; progresso automático. Só cancelamento/timeout interrompem. |
| `hobots.progress.total(n)` · `.advance(k = 1)` · `.set(atual, total?)` · `.current` · `.snapshot` | Barra, percentual e tempo restante na execução. Sem `total()` nada é enviado. |
| `hobots.flush(timeoutMs?)` | **Entrega já** o que a execução fez: as steps raiz finalizadas viram transactions parciais (waterfall na Central sem esperar a run terminar) e itens, logs e progresso pendentes saem sem esperar o throttle. O bot decide quando — num agente `start()`, ao fim de cada rodada que processou itens. Aguarda com teto (15 s) e nunca lança. |
| `hobots.transaction.startChild(op, descrição)` | Uma **step** no waterfall de performance da execução. A transaction da execução (`run`) já existe e é finalizada pelo SDK. `step.setStatus(status, msg?, { image })` / `setError(err, msg?, { image })` penduram um print na step. |
| `hobots.image(bytes \| caminho, { caption?, contentType? })` | Embrulha um **print** (PNG/JPEG/WEBP, até 5 MB, 20 por execução) para ir junto de um log, item ou step. Puro: o upload roda em segundo plano quando o evento é registrado; nunca lança nem bloqueia. |
| `hobots.captureException(erro, hint?)` | Abre uma **issue** (módulo Issues). `hint.attachment` anexa um print à linha de log correspondente. |
| `hobots.retry(fn, { retries = 3, delayMs = 1_000, shouldRetry? })` | async-retry ciente do cancelamento da execução. `delayMs` aceita função `(tentativa) => ms`. |
| `hobots.sleep(ms)` | Espera **cancelável**: rejeita com `RunCanceled` se a execução for cancelada. |
| `hobots.signal` · `hobots.throwIfCanceled()` | Cancelamento cooperativo manual, para controle fino. |
| `hobots.files.list()` · `.get(campo)` · `.download(refOuCampo, { dest }?)` · `.downloadAll(campo)` · `.read(refOuCampo)` | **Arquivos dos campos de upload** do formulário: baixa para o disco (ou para a memória) e consome na origem — ver [Arquivos do formulário](#arquivos-do-formulário). `isFileRef(valor)` reconhece um arquivo nos params. |
| `hobots.runId` · `hobots.params` | Identidade da execução atual. |
| `hobots.register(...)` · `hobots.start(...)` · `hobots.close(timeoutMs?)` | Ciclo de vida do processo. |

O handler também recebe o mesmo contexto como 2º argumento (`(params, ctx) => …`), para quem prefere explícito. Fora de qualquer execução (ex.: antes do boot), os membros de contexto caem em fallbacks seguros e avisam uma vez — `hobots.log` continua imprimindo no terminal. Evite `const { log } = hobots` no topo de um módulo: o valor capturado é o de fora de execução; chame `hobots.log.info(...)`.

---

## Arquivos do formulário

Um campo de **upload** no formulário do agente chega nos `params` como um `FileRef` — `{ id, name, size, contentType, storage }` — ou uma lista deles (campo "vários arquivos"). O bot não precisa saber de onde o arquivo veio: `hobots.files.download` traz para o disco desta máquina e devolve o caminho.

```js
import { hobots } from 'hobots';

hobots.register(async () => {
  // campo único: o caminho do arquivo baixado (pasta temporária da execução)
  const planilha = await hobots.files.download('planilha');
  await importarPlanilha(planilha);

  // campo "vários arquivos": um caminho por arquivo, na ordem, na pasta escolhida
  const notas = await hobots.files.downloadAll('notas', { dest: './entrada' });
  for (const nota of notas) await lancarNota(nota);

  // ou direto em memória (Buffer) — a cópia temporária é apagada
  const contrato = await hobots.files.read('contrato');
});
```

- **Cada arquivo é baixado uma vez.** O download consome a origem: enviado pela Central (`storage: 's3'`), o SDK pede uma URL temporária, baixa em streaming, confere o tamanho e **só então** confirma — o servidor apaga o arquivo na hora; uma segunda tentativa recebe `arquivo "x" já baixado ou expirado` (`TasksApiError` com `status` 410). Escolhido no Studio (`storage: 'local'`), o arquivo nunca sai do computador: o SDK copia de `$HOBOTS_LOCAL_FILES_DIR/<id>/` e apaga a pasta de origem — num bot de outro computador (sem a env ou sem a pasta), o erro é `arquivo "x" enviado pelo Studio de outro computador ou já consumido`.
- **Falha não perde o arquivo:** rede caiu, tamanho divergente, execução cancelada — nada é confirmado e o `download` pode ser tentado de novo (ex.: dentro de `hobots.retry`).
- **Onde o arquivo vai parar:** sem `dest`, numa pasta temporária **da execução, apagada quando ela termina** — guarde o que precisar além disso com `dest`. `dest` pasta (existente ou terminada em `/`) recebe o nome original; outro caminho vira o nome do arquivo final.
- `hobots.files.list()` lista todos os arquivos dos params; `hobots.files.get('campo')` devolve o `FileRef` (ou a lista); `download` aceita o nome do campo ou o próprio `FileRef` (um item da lista, por exemplo). Para campo com vários arquivos use `downloadAll` (o `download('campo')` lança, apontando para ele).
- Respeita o cancelamento: uma execução cancelada no meio do download lança `RunCanceled` e não consome o arquivo.

---

## Cancelamento automático

Uma execução cancelada no app **não mata** o handler — o SDK dispara `hobots.signal` e o próximo ponto de parada lança **`RunCanceled`**:

- `hobots.items.*`, `hobots.progress.total/advance/set`, `hobots.transaction.startChild()`, `hobots.sleep()` e cada tentativa de `hobots.retry()`.
- **Nunca `hobots.log.*`** — um log dentro de um `finally` de limpeza não pode explodir.

Num loop de itens, isso costuma bastar: registrar o item processado já é o ponto de parada. Para controle fino, `hobots.throwIfCanceled()` e `hobots.signal` continuam disponíveis. Quando o cancelamento veio do app, o `RunCanceled` é **suprimido** — a execução já está `cancelada` no servidor e nada é reportado por cima. Limpeza (fechar navegador, desfazer estado) vai em `try/finally`.

> **Atenção:** não use `if (hobots.signal.aborted) return;`. Retornar normalmente reporta a execução como **`succeeded`**. É preciso **lançar** — e é o que os pontos de parada fazem.

---

## Retry

Quando o handler lança, o SDK **tenta de novo na mesma execução**: aviso no log (`tentativa 1/2 falhou: … — nova tentativa em 5000ms`), espera cancelável, re-invocação com os mesmos `params`. Default: **1 tentativa extra** com 5 s. Itens e progresso já enviados permanecem — escreva handlers **idempotentes** (use o `id` de negócio nos itens: repetido, o servidor atualiza em vez de duplicar).

```js
hobots.register(handler, { retries: 3, retryDelayMs: attempt => attempt * 10_000 });
```

Nunca há nova tentativa em **cancelamento**, **timeout** (`TaskTimeout` é terminal) ou execução já cancelada no app. Para uma chamada instável dentro do handler, use `hobots.retry()`.

---

## Opções

```ts
hobots.register(handler, options?: RegisterOptions);
hobots.start(handler, options?: StartOptions);
```

| Opção | Default | Descrição |
| --- | --- | --- |
| `clientSecret` | env `HOBOTS_CLIENT_SECRET` | Client Secret do agente |
| `instanceId` | env `HOBOTS_INSTANCE_ID` → hostname | Identidade da instância |
| `environment` | env `HOBOTS_ENVIRONMENT` → `'production'` | `development` \| `testing` \| `staging` \| `production` — decide quem é alertado |
| `release` | — | Versão do programa, ex. `robo-nfe@1.4.2` |
| `tags` | — | Tags de todos os eventos |
| `debug` | `false` | Loga a atividade do SDK |
| `machineMetrics` | `true` | CPU/memória/disco no heartbeat e em cada evento |
| `captureUnhandled` | `true` | `uncaughtException`/`unhandledRejection` viram issues |
| `items` | — | `{ columns: [...] }` — colunas da aba de itens no app (uma declaração, do agente) |
| `retries` | `1` | Tentativas extras do handler (0 desliga) |
| `retryDelayMs` | `5_000` | ms fixo ou `(tentativa) => ms` |
| `timeoutMs` | — (infinito) | Timeout por tentativa; terminal (`TaskTimeout`). O handler não é morto: o signal dispara e o slot é liberado |
| `concurrency` (register) | `1` | Execuções simultâneas do agente (1..5) |
| `pollIntervalMs` (register) | `2_000` | Intervalo entre polls sem trabalho (mín. 1_000) |
| avançadas: `installExitHandlers`, `logFlushIntervalMs`, `cancelCheckIntervalMs`, `cancelGraceMs` | `true`, `2_000`, `5_000`, `10_000` | Ver [Referência de API](./docs/api-reference.md) |

Como só há uma chamada por processo, todas as opções são as **do agente** — não existe "opção de processo" separada da "opção da task".

---

## Encerramento

O heartbeat mantém o processo vivo por design. `hobots.close(timeoutMs = 30_000)` para o polling, aguarda as execuções em voo, para o heartbeat (libera o processo) e faz o flush final:

```js
process.on('SIGINT', () => {
  void hobots.close(15_000).then(() => process.exit(0));
});
```

Sem handler próprio de sinal, um `SIGINT`/`SIGTERM` (ou erro fatal) com execuções ativas reporta cada uma como `failed`/`AgentTerminated` antes do processo sair — o app mostra a falha na hora.

> **Dados sensíveis:** logs, itens, mensagens de erro e prints são enviados ao servidor e ficam visíveis no app. O SDK redige padrões óbvios de segredo, mas não borra prints nem filtra dados pessoais. Capture só o necessário.

---

## Changelog resumido

- **4.2.0** — **arquivos do formulário**: `hobots.files` (`list`, `get`, `download`, `downloadAll`, `read`) e `isFileRef`; tipos `FileRef`, `FileStorage`, `FileDownloadResponse` em `hobots/protocol`.
- **4.0.0** — **o agente é a própria tarefa** (breaking): `register(handler, options?)`/`start(handler, options?)` **sem slug**; **uma chamada por processo** (a segunda lança); `hobots.task`/`ctx.task` removidos; tipos `TaskHandler`→`RunHandler`, `TaskOptions`→`RunOptions`, `TaskItemsOptions`→`ItemsOptions`; wire: `InstanceInfo.mode` (`on_demand` \| `always_on`) e `InstanceInfo.items` no lugar de `tasks`/`task_items`, endpoints em `/v1/runs/*`; eco no terminal `[hobots] INFO …`. Guia de migração no [CHANGELOG](./CHANGELOG.md).
- **3.4.0** — `hobots.attach()` removido; o print vai junto do evento: `hobots.image(...)` no último argumento de `log.*`, `{ image }` em itens e steps.
- **3.1.0** — `hobots.log.*` aceita vários argumentos (como `console.log`); `retries` inválido (`NaN`) cai no default em vez de tentar para sempre; 429 de itens e 429/501/503 de anexo são definitivos (sem esperar a ladder — `quota`/`unsupported` passam a funcionar de fato); timeout **por requisição** nos anexos; truncamento nos limites do servidor (tag 512, fingerprint 10×255, nome de transaction 500, 200 steps, `exception.value` 8192, `filename` 1024); PNG acima de 20 000 px não declara dimensões; PATCH terminal nunca passa de 100 logs.

## Documentação

- [Getting started](./docs/getting-started.md) — instalação, envs, o primeiro agente nos dois modos.
- [Execuções](./docs/runs.md) — handler, `hobots.*`, itens, progresso, anexos, cancelamento, retry, concorrência, idempotência.
- [Conceitos](./docs/concepts.md) — o vocabulário: agente, execução, evento, transaction, step, heartbeat, instância, claim…
- [Referência de API](./docs/api-reference.md) — assinaturas, tabelas de opções e tempos.
- [Protocolo & transporte](./docs/protocol.md) — endpoints, envelope, retries, claim.
- Exemplos runnable em [`packages/hobots-sdk-demos`](../hobots-sdk-demos).

## Licença

[MIT](./LICENSE) © Hobots
