{"_id":"@codeharbor-br/atualia-sdk","_rev":"3-d16da908ddacfe6b85786084cf48a062","name":"@codeharbor-br/atualia-sdk","dist-tags":{"latest":"0.1.2"},"versions":{"0.1.0":{"name":"@codeharbor-br/atualia-sdk","version":"0.1.0","keywords":["atualia","sdk","proxy","collections"],"license":"UNLICENSED","_id":"@codeharbor-br/atualia-sdk@0.1.0","maintainers":[{"name":"leonardo.fertonani","email":"leonardo@codeharbor.com.br"}],"homepage":"https://github.com/codeharbor-br/atualia#readme","bugs":{"url":"https://github.com/codeharbor-br/atualia/issues"},"bin":{"atualia":"dist/cli.js","atualia-mcp":"dist/mcp.js"},"dist":{"shasum":"420e3664f38aa55c55e2a814f6e8b74ca51eb002","tarball":"https://registry.npmjs.org/@codeharbor-br/atualia-sdk/-/atualia-sdk-0.1.0.tgz","fileCount":21,"integrity":"sha512-haIGbNsfl3GSo4E4eg6Yu0QxFREOxU488Ovbs90CPj00Pm7FhMGndNo9z9j0RqSrsn9KWqJE7/Le0JQB42lJKA==","signatures":[{"sig":"MEQCIHNCGk4QDAdjVMUfU+AABKgV1jiwwOUNQIdEUp4+wsYAAiAhfDyvvPgj4ry66dsYzHYiwmI7oPXOOG91Oqn/imyG/Q==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":887401},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=20"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"}},"gitHead":"debfb89a5d2188d5455b2ebb337b062de5675bb7","scripts":{"dev":"tsup src/index.ts src/cli.ts src/mcp.ts src/scaffold.ts --format esm,cjs --dts --watch","test":"node --test test/","build":"tsup src/index.ts src/cli.ts src/mcp.ts src/scaffold.ts --format esm,cjs --dts --clean","prepack":"tsup src/index.ts src/cli.ts src/mcp.ts src/scaffold.ts --format esm,cjs --dts --clean","typecheck":"tsc --noEmit"},"_npmUser":{"name":"leonardo.fertonani","email":"leonardo@codeharbor.com.br"},"repository":{"url":"git+https://github.com/codeharbor-br/atualia.git","type":"git","directory":"sdk"},"_npmVersion":"10.8.2","description":"Cliente oficial do atualia: chama endpoints de proxy e consulta as collections do cliente com client_id + secret_key.","directories":{},"sideEffects":false,"_nodeVersion":"20.19.5","publishConfig":{"access":"public","registry":"https://registry.npmjs.org"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.3.0","typescript":"^5.6.0","@types/node":"^26.1.2"},"_npmOperationalInternal":{"tmp":"tmp/atualia-sdk_0.1.0_1786044613237_0.8161889599263232","host":"s3://npm-registry-packages-npm-production"}},"0.1.1":{"name":"@codeharbor-br/atualia-sdk","version":"0.1.1","keywords":["atualia","sdk","proxy","collections"],"license":"UNLICENSED","_id":"@codeharbor-br/atualia-sdk@0.1.1","maintainers":[{"name":"leonardo.fertonani","email":"leonardo@codeharbor.com.br"}],"homepage":"https://github.com/codeharbor-br/atualia#readme","bugs":{"url":"https://github.com/codeharbor-br/atualia/issues"},"bin":{"atualia":"dist/cli.js","atualia-mcp":"dist/mcp.js"},"dist":{"shasum":"583973220f10064898c183c4c47168618e5a445f","tarball":"https://registry.npmjs.org/@codeharbor-br/atualia-sdk/-/atualia-sdk-0.1.1.tgz","fileCount":21,"integrity":"sha512-9S0VBKP+SSuzmTLnXBYGwrHQ1tPc6cmraCSyP1UhVyHA38Ge7x4zwOBV0gPyDvftikiKjT8KH7GQOhO+0pVckw==","signatures":[{"sig":"MEYCIQC5Ky8ngNqi55hqT8xrUoXSrbVF16qQdHwnvJEmvN73LgIhAMufwS8IOFiMgCGD7JatMoKFgeK5CdfrC59wes+Wr8P2","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":888702},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=20"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"}},"gitHead":"debfb89a5d2188d5455b2ebb337b062de5675bb7","scripts":{"dev":"tsup src/index.ts src/cli.ts src/mcp.ts src/scaffold.ts --format esm,cjs --dts --watch","test":"node --test test/","build":"tsup src/index.ts src/cli.ts src/mcp.ts src/scaffold.ts --format esm,cjs --dts --clean","prepack":"tsup src/index.ts src/cli.ts src/mcp.ts src/scaffold.ts --format esm,cjs --dts --clean","typecheck":"tsc --noEmit"},"_npmUser":{"name":"leonardo.fertonani","email":"leonardo@codeharbor.com.br"},"repository":{"url":"git+https://github.com/codeharbor-br/atualia.git","type":"git","directory":"sdk"},"_npmVersion":"10.8.2","description":"Cliente oficial do atualia: chama endpoints de proxy e consulta as collections do cliente com client_id + secret_key.","directories":{},"sideEffects":false,"_nodeVersion":"20.19.5","publishConfig":{"access":"public","registry":"https://registry.npmjs.org"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.3.0","typescript":"^5.6.0","@types/node":"^26.1.2"},"_npmOperationalInternal":{"tmp":"tmp/atualia-sdk_0.1.1_1786044949481_0.3187906711149775","host":"s3://npm-registry-packages-npm-production"}},"0.1.2":{"name":"@codeharbor-br/atualia-sdk","version":"0.1.2","description":"Cliente oficial do atualia: chama endpoints de proxy e consulta as collections do cliente com client_id + secret_key.","license":"UNLICENSED","type":"module","main":"./dist/index.cjs","module":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"}},"sideEffects":false,"scripts":{"build":"tsup src/index.ts src/cli.ts src/mcp.ts src/scaffold.ts --format esm,cjs --dts --clean","dev":"tsup src/index.ts src/cli.ts src/mcp.ts src/scaffold.ts --format esm,cjs --dts --watch","test":"node --test test/","typecheck":"tsc --noEmit","prepack":"tsup src/index.ts src/cli.ts src/mcp.ts src/scaffold.ts --format esm,cjs --dts --clean"},"keywords":["atualia","sdk","proxy","collections"],"engines":{"node":">=20"},"devDependencies":{"@types/node":"^26.1.2","tsup":"^8.3.0","typescript":"^5.6.0"},"publishConfig":{"access":"public","registry":"https://registry.npmjs.org"},"repository":{"type":"git","url":"git+https://github.com/codeharbor-br/atualia.git","directory":"sdk"},"bin":{"atualia":"dist/cli.js","atualia-mcp":"dist/mcp.js"},"_id":"@codeharbor-br/atualia-sdk@0.1.2","gitHead":"5673befd2bc27d79994680010298a4bb973861f9","bugs":{"url":"https://github.com/codeharbor-br/atualia/issues"},"homepage":"https://github.com/codeharbor-br/atualia#readme","_nodeVersion":"20.19.5","_npmVersion":"10.8.2","dist":{"integrity":"sha512-GU1UsHR7+pxHUuC//tGkKdBhUwyq6nCBoEO8reA1lUlpCQD3uMVx998qqUQPfgs5XUXeXPfq3mSaa1+DS6ocvA==","shasum":"2132dc90a733d2363ce6e776cc8900ccf6d60f5d","tarball":"https://registry.npmjs.org/@codeharbor-br/atualia-sdk/-/atualia-sdk-0.1.2.tgz","fileCount":21,"unpackedSize":889539,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIFgRTL2e0Y/ExTAp82waFsgkmcOVMFWzF+Tdnyw2/LHlAiEAtiE7L8pdVzW2uMWw2rn3MaKWPRklvPilS/a+zgZdhME="}]},"_npmUser":{"name":"leonardo.fertonani","email":"leonardo@codeharbor.com.br"},"directories":{},"maintainers":[{"name":"leonardo.fertonani","email":"leonardo@codeharbor.com.br"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/atualia-sdk_0.1.2_1786045269423_0.6153616645211117"},"_hasShrinkwrap":false}},"time":{"created":"2026-08-06T19:30:12.962Z","modified":"2026-08-06T19:41:09.843Z","0.1.0":"2026-08-06T19:30:13.408Z","0.1.1":"2026-08-06T19:35:49.680Z","0.1.2":"2026-08-06T19:41:09.666Z"},"bugs":{"url":"https://github.com/codeharbor-br/atualia/issues"},"license":"UNLICENSED","homepage":"https://github.com/codeharbor-br/atualia#readme","keywords":["atualia","sdk","proxy","collections"],"repository":{"type":"git","url":"git+https://github.com/codeharbor-br/atualia.git","directory":"sdk"},"description":"Cliente oficial do atualia: chama endpoints de proxy e consulta as collections do cliente com client_id + secret_key.","maintainers":[{"name":"leonardo.fertonani","email":"leonardo@codeharbor.com.br"}],"readme":"# @codeharbor-br/atualia-sdk\n\nCliente oficial do atualia: chama as **APIs** liberadas para o seu app e consulta as\ncollections dele.\n\nVem em duas formas — uma **lib** para usar em código e uma **CLI** para usar no\nterminal (ou por um agente de IA).\n\nSe o que você quer é o app que roda **dentro do painel do atualia**, comece por\n[App embarcado no painel](#app-embarcado-no-painel): ali não existe credencial nenhuma a\nconfigurar.\n\n```bash\nnpm install @codeharbor-br/atualia-sdk\n```\n\n## Os dois transportes\n\nNão é ambiente, é onde o código roda:\n\n| | com credencial | embarcado no painel |\n| --- | --- | --- |\n| montagem | `criarCliente({...})` | `criarClienteEmbarcado()` |\n| autenticação | assinatura HMAC, sempre | nenhuma: a sessão de quem está logado |\n| onde roda | servidor, CLI, job | iframe do app dentro do painel |\n| configuração | url, app, client_id, secret_key | nada |\n\nA superfície é a mesma nos dois (`api`, `collection`, `proxy`), de propósito: o mesmo\ncódigo de tela funciona nos dois.\n\n**Não existe modo de browser com credencial.** Existiu: `client_id` público e a origem da\npágina conferida contra uma lista cadastrada no app, para o app exibido fora do painel.\nEsse caso deixou de existir, e com ele o modo — hoje `/api/sdk/**` recusa qualquer chamada\nsem assinatura, e assinar exige a secret, que não cabe num bundle.\n\nCada credencial tem **teto por minuto**, e ele conta **execução de API e acesso a dado**.\nListar APIs, ler documentação e ler esquema é livre — é o que um agente faz para descobrir\no que existe antes de agir.\n\n## Credenciais\n\nA credencial pertence a um **app**: no painel, abra o app e vá em **Acesso**. A secret\naparece uma única vez.\n\nO escopo é o app — a credencial alcança as collections e as APIs daquele app, e mais\nnada. Assim, vazar a credencial de um app não expõe os outros.\n\nA `secret_key` é credencial de **servidor**, e é a única forma de chamar: sem ela não há\nassinatura, e sem assinatura a API recusa. Num bundle de front-end ela fica legível para\nqualquer visitante, então não vá por ali — dentro do painel o app usa\n`criarClienteEmbarcado()`, que não precisa de credencial. O SDK avisa no console se\ndetectar uma secret rodando em browser.\n\n```bash\nexport ATUALIA_URL=https://atualia-api.codeharbor.com.br\nexport ATUALIA_APP_ID=agenda        # id ou slug do app\nexport ATUALIA_CLIENT_ID=ak_...\nexport ATUALIA_SECRET_KEY=sk_...\n```\n\nNo terminal dá para pular isso: `atualia login` pergunta os quatro campos um a um,\ntesta a credencial e grava em `~/.atualia/config.json` (permissão 600). Variável de\nambiente sempre vence o arquivo, para CI e container não dependerem do que está na\nmáquina.\n\n## Como a autenticação funciona\n\nCada chamada é **assinada** (HMAC-SHA256) com a secret, que nunca trafega. A assinatura cobre método, caminho, timestamp, um nonce e o hash do\ncorpo — então uma requisição capturada não pode ser reenviada nem alterada.\n\nConsequências práticas:\n\n- **relógio importa**: mais de 5 minutos de diferença com o servidor derruba a\n  chamada. O erro diz isso explicitamente;\n- **cada chamada é única**: repetir a mesma assinatura é recusado (`nonce`\n  repetido);\n- **não dá para reproduzir com `curl` copiado** — é o que faz da lib o caminho de\n  uso, e não um detalhe de conveniência.\n\n`secretKey` é obrigatório: uma chamada sem assinatura é recusada com 401, e a mensagem\nexplica que o caminho do browser é `criarClienteEmbarcado()`.\n\n## App embarcado no painel\n\nÉ o caso normal, e nele **não existe credencial**:\n\n```ts\nimport { criarClienteEmbarcado } from '@codeharbor-br/atualia-sdk'\n\n// Sem baseUrl, sem appId, sem client_id: nada a configurar.\nexport const atualia = criarClienteEmbarcado()\n\n// A superfície é a mesma do cliente com credencial:\nconst { corpo } = await atualia.api.call('core-sql-all', { body: { sql: 'SELECT ...' } })\nconst { itens } = await atualia.collection('pacientes').limit(20).get()\n```\n\nComo funciona: o app roda num iframe dentro do painel e manda cada pedido ao painel por\n`postMessage`. Quem executa é o painel, com a **sessão de quem está logado**. Três\nconsequências:\n\n- **não há o que vazar**: o bundle é público e não carrega segredo nenhum;\n- **fora do painel o app não funciona**: sem alguém logado, ninguém executa por ele;\n- **o alcance é o do app**: o painel resolve o app pela sessão, não por algo que o app\n  mande.\n\nO painel também conta ao app onde ele está:\n\n```ts\nconst { app, appNome, cliente, clienteNome, usuario } = await atualia.pronto()\nconst cancelar = atualia.aoTrocarContexto((c) => setCliente(c.clienteNome))\n```\n\nE o app se recusa a rodar fora do painel:\n\n```ts\nimport { estaEmbarcado, hostQueEmbarca } from '@codeharbor-br/atualia-sdk'\n\nconst paineis = import.meta.env.VITE_ATUALIA_PAINEL.split(',')\nif (!estaEmbarcado() || hostQueEmbarca(paineis) === false) {\n  // mostra o aviso em vez de tentar chamar\n}\n```\n\n`ancestorOrigins` (que é o que `hostQueEmbarca` lê) é preenchido pelo navegador e a\npágina não consegue falsificar. No Firefox ele não existe: ali a resposta é `null` e quem\ndecide é o handshake, que sem o painel não completa.\n\nPara o app aparecer na barra lateral, informe o endereço publicado em **Apps > o app >\nPublicação**. `atualia create` monta esse app inteiro para você — ver abaixo.\n\n## Lib\n\n```ts\nimport { criarCliente } from '@codeharbor-br/atualia-sdk'\n\nconst atualia = criarCliente({\n  baseUrl: process.env.ATUALIA_URL!,\n  appId: process.env.ATUALIA_APP_ID!,\n  clientId: process.env.ATUALIA_CLIENT_ID!,\n  secretKey: process.env.ATUALIA_SECRET_KEY!,\n})\n\nawait atualia.ping() // confere credencial, assinatura e relógio\n```\n\n### APIs\n\nO destino real (URL, headers, credencial) fica no servidor. Você manda a chave e os\nvalores dos campos declarados.\n\nA chamada aceita os parâmetros **separados por seção**, o que deixa o contrato\nvisível no próprio código:\n\n```ts\nconst { corpo } = await atualia.api.call('core-pedido-criar', {\n  method: 'POST',                          // conferido contra o cadastro\n  headers: { instancia: 'erp_cliente01' }, // vira header na saída\n  params: { pagina: 1 },                   // vira query string\n  path: { codigo: 42 },                    // entra no caminho da URL\n  body: { estmov, estimo },                // vira corpo JSON\n})\n```\n\nAs seções são `headers`, `params` (query), `path` e `body`. O `method` é\n**declarativo**: o método real vem do cadastro (é o que impede trocar um GET por um\nDELETE), e declará-lo faz o servidor conferir — se divergir, a chamada é recusada\ncom mensagem clara em vez de executar a operação que você não quis.\n\nTambém aceita a forma plana, quando você não se importa com a divisão:\n\n```ts\nawait atualia.api.call('core-pedido-criar', { instancia: 'erp_cliente01', estmov, estimo })\n```\n\nQuem posiciona de fato é o servidor, a partir do cadastro — a divisão é contrato\nlegível, não roteamento. Um campo declarado em `body` que o cadastro diz ser de\nquery ainda vai para a query.\n\n#### Campos que vêm do ambiente do app\n\nUm campo com `variavelAmbiente` preenchido é obrigatório mas **não precisa ser\nenviado**: o valor está configurado no app (painel → o app → Ambiente) e o servidor o\naplica na saída.\nÉ o caso da `instancia` do Core, obrigatória em todos os endpoints dele.\n\n```ts\nconst pedido = await atualia.api.get('core-pedido-criar')\npedido?.parametros\n  .filter((p) => p.variavelAmbiente)\n  .forEach((p) => console.log(p.nome, '← ambiente:', p.variavelAmbiente))\n// instancia ← ambiente: instancia\n\n// Com a instância configurada, a chamada não a repete:\nawait atualia.api.call('core-sql-one', { sql: 'SELECT 1 FROM dual' })\n```\n\nEnviar o valor continua valendo e tem preferência sobre a variável — é assim que se\nchama em nome de outra instância sem mexer na configuração.\n`atualia.api.tools()` já deixa esses campos fora do `required`, e a CLI os mostra como\n`obrig.: ambiente` em vez de pedir o valor.\n\nDescobrindo o que existe:\n\n```ts\nconst endpoints = await atualia.api.list()   // as do app + as compartilhadas\nconst consulta = await atualia.api.get('core-sql-all')\nconsole.log(consulta?.documentacao)      // documentação completa, em Markdown\nconsole.log(consulta?.exemploResposta)   // forma da resposta, sem precisar chamar\n```\n\nPara dar os endpoints a um modelo como ferramentas:\n\n```ts\nconst tools = await atualia.api.tools() // name, description e input_schema\n```\n\n> `atualia.proxy` é o mesmo objeto, mantido como apelido: o painel de quem publica\n> chama isso de proxy, porque descreve a mecânica. Para quem consome, é API.\n\n### Collections\n\nCada collection é uma tabela real no seu schema no Postgres. Você a endereça só\npelo nome: o app vem da credencial, então não existe forma de pedir a collection de\noutro app.\n\n```ts\nconst pacientes = atualia.collection('pacientes')\n\nconst { total, itens } = await pacientes\n  .where('ativo', '=', true)\n  .where('idade', '>=', 18)\n  .orderBy('nome')\n  .limit(50)\n  .get()\n\nconst um = await pacientes.where('cpf', '=', '11111111111').first()\nconst quantos = await pacientes.count()\n\nawait pacientes.insert({ nome: 'Maria Souza', cpf: '11111111111', idade: 40 })\nawait pacientes.update(id, { idade: 41 })\nawait pacientes.delete(id)\n```\n\nOperadores: `=`, `!=`, `>`, `>=`, `<`, `<=`, `contem`, `comeca_com`,\n`termina_com`, `em`, `nulo`, `nao_nulo`.\n\nCampo não declarado, tipo errado ou obrigatório ausente são recusados pelo servidor\ncom mensagem dizendo qual campo — a validação não é só do cliente.\n\n### Relatórios\n\nO template é desenhado no Jaspersoft Studio e configurado no painel do app: lá se diz de\nonde vem cada campo do `.jrxml` e quais filtros o relatório aceita. Daqui você só escolhe\nformato e filtros.\n\n```ts\nconst relatorios = await atualia.relatorio.list()\nconst contrato = await atualia.relatorio.get('faturamento')  // filtros, tipos, formatos\n\nconst { url, linhas, expiraEm } = await atualia.relatorio.gerar('faturamento', {\n  formato: 'XLSX',                                    // PDF (padrão) ou XLSX\n  filtros: { data_de: '2026-01-01', data_ate: '2026-01-31' },\n})\n```\n\nA resposta é uma **URL assinada e temporária**, nunca o arquivo: o download não passa pelo\nbackend, e a URL pode ser entregue a quem pediu. `linhas` responde \"o filtro cortou tudo?\"\nsem baixar nada.\n\nFiltro não declarado é recusado, e obrigatório sem valor também — a mensagem diz qual.\nFonte, colunas e ordenação vêm do cadastro; quem chama não escolhe, do mesmo jeito que não\nescolhe o destino de uma API.\n\n## CLI\n\nO pacote instala o binário `atualia`.\n\n```bash\natualia                            # estado atual: APIs e collections\natualia create meu-app             # gera um app React+Vite documentado deste app\natualia login                      # pergunta url, app, client_id e secret_key\natualia config                     # mostra o que está valendo e de onde veio\natualia ping                       # credencial, assinatura e relógio\n\natualia api list                   # APIs disponíveis\natualia api search pedido          # procura por nome, resumo ou campo\natualia api doc core-sql-all       # documentação, campos por seção e retorno\natualia api doc core-sql-all --full  # documentação inteira, sem truncar\natualia api call core-sql-all --sql 'SELECT cliente, nome FROM cadcli LIMIT 20'\n\natualia db list                    # collections deste app\natualia db doc pacientes           # campos, tipos, documentação e operadores\natualia db query pacientes --where ativo=true --order nome:asc --limit 20\natualia db insert pacientes --json '{\"nome\": \"Maria\"}'\natualia db update pacientes <id> --json '{\"idade\": 41}'\natualia db delete pacientes <id>\n\natualia relatorio list             # relatórios publicados neste app\natualia relatorio doc faturamento  # filtros aceitos, tipos e documentação\natualia relatorio publicar relatorios/faturamento.json   # sobe o .jrxml e o de-para\natualia relatorio gerar faturamento --data_de 2026-01-01 --data_ate 2026-01-31\natualia relatorio gerar faturamento --formato xlsx\n\natualia tools                      # definições de ferramenta (JSON) para um modelo\n```\n\n`atualia relatorio publicar` é o comando de quem **escreveu** o template: o `.jrxml`\nmais um manifesto JSON com a fonte, o de-para dos campos e os filtros, numa chamada.\nO servidor compila o template e valida o mapeamento inteiro antes de gravar, e\nrepublicar o mesmo identificador substitui — ajustar o layout e rodar de novo não\nduplica o cadastro. No painel o ciclo continua sendo em dois passos (subir o arquivo,\ndepois mapear), que é o certo para quem desenhou no Jaspersoft Studio e ainda não sabe\nquais campos o template declara.\n\n`atualia api doc <chave>` e `atualia db doc <collection>` são os comandos que\nrespondem **o que chamar e o que esperar de cada campo**: resumo, documentação,\ncampos agrupados por seção (headers, params, path, body) com tipo,\nobrigatoriedade, exemplo e valores aceitos, exemplo de retorno e o trecho de\ncódigo pronto.\n\nFiltros da consulta: `--where campo=valor` (igualdade) ou\n`--where campo=operador:valor`, repetível — os filtros somam com AND. Para `em`,\nsepare os valores com `|`. Os operadores `nulo` e `nao_nulo` não levam valor.\n`--order campo:desc`, `--limit` e `--offset` completam a consulta.\n\n### `atualia create`\n\nGera um projeto React + Vite pronto para o Cloudflare Pages, que nasce sabendo o app e\ncom a mesma stack do painel — **shadcn/ui** (Tailwind v4), **TanStack Query**, **React\nRouter** e **axios**:\n\n```\n.env.local              o endereço do painel — e nada mais: o app não tem credencial\ncomponents.json         config do shadcn: `npx shadcn add <componente>` já funciona\nsrc/components/layout/  a casca: sidebar, barra superior e a lista de telas\nsrc/components/ui/      os do painel (button, card, table, input, select, field...)\n                        + combobox.tsx: escolher UM registro buscando, que o registry não tem\nsrc/lib/api.ts          cliente embarcado (sem credencial) + axios + mensagemDoErro\nsrc/lib/contexto.ts     useContexto(): app, cliente e o usuário logado\nsrc/lib/query-client.ts padrões do TanStack Query\nsrc/lib/utils.ts        cn(), que todo componente do shadcn importa\nsrc/paginas/            uma tela por arquivo: inicio, exemplo-cadastro, fora-do-painel\nsrc/App.tsx             o mapa de rotas\nsrc/main.tsx            portão de entrada, providers e router\nsrc/iframe.ts           links para fora e storage dentro do iframe\nsrc/index.css           tema (tokens do painel), tema claro só, altura do frame\npublic/_redirects       fallback de SPA — sem ele, sub-rota dá 404 depois do deploy\n.claude/skills/erp/     como atacar tarefa que toca o legado do cliente\nAGENTS.md               instruções para um agente de IA trabalhar no projeto\nwrangler.toml           npm run deploy publica no Pages\n```\n\nAs duas telas de `src/paginas` são **exemplo, e se declaram exemplo**: `inicio` lista dado\nreal (o ERP do cliente, ou a primeira collection) e `exemplo-cadastro` é o padrão de tela\ndeste projeto — formulário com `react-hook-form` + `zod`, `Combobox` para escolher um\nregistro e `Select` para lista fechada. Não há pasta `docs/`: contrato de API e de\ncollection é dado do servidor, e quem responde o que existe hoje é `npx atualia`.\n\nA stack não é preferência: o app é exibido **dentro** do painel, então componente copiado\nde lá cola aqui e a tela não parece outro produto colado na página. A tela inicial já\nlista dados reais da primeira collection.\n\nNão há lista de endpoints em arquivo, de propósito: as APIs e as collections mudam no\npainel sem este código mudar, e uma cópia congelada passaria a mentir. O que o projeto\nensina é **como perguntar** — pela CLI e pelo painel `/docs`, que lê do servidor a cada\nabertura.\n\nNão roda `npm install`: os comandos ficam impressos no fim.\n\n### Formatos de saída\n\n| destino | formato | como forçar |\n| --- | --- | --- |\n| terminal | tabelas alinhadas, com cor | `--humano` |\n| pipe, arquivo, script | JSON | `--json` |\n| agente de IA | [TOON](https://toonformat.dev/) — ~40% menos tokens | `--toon` |\n\nA escolha é pelo destino, não por preferência: `atualia api list` num terminal sai\nlegível, e `atualia api list \\| jq` sai JSON sem precisar de flag. Quem paga token\npor caractere pede `--toon` e recebe a mesma informação em menos espaço.\n\n### Convenções da CLI\n\n- **sem argumento mostra estado**, não manual;\n- **erros vão para stdout** no mesmo formato dos dados, com o comando que resolve;\n- **stderr é só diagnóstico** — nada de dado por lá;\n- **exit codes**: `0` sucesso, `1` erro, `2` erro de uso;\n- **prompt só existe com terminal**: em pipeline, valor faltando é erro imediato —\n  travar esperando um input que não vem é pior que falhar com mensagem.\n\n## MCP\n\nO pacote instala também o binário `atualia-mcp`: o mesmo catálogo da CLI, servido pelo\nprotocolo MCP para um agente que não tem shell (Claude Code, Claude Desktop, qualquer\ncliente MCP).\n\n### Instalar no Claude Code\n\n**1. Instale o pacote.**\nO binário precisa existir no PATH antes do registro — o Claude Code executa o comando\nque você registrar, não resolve pacote por conta.\n\n```bash\nnpm i -g @codeharbor-br/atualia-sdk\nwhich atualia-mcp   # tem que imprimir um caminho\n```\n\nNão rode `atualia-mcp` na mão para \"testar\": ele é um servidor de stdio e fica esperando\nmensagem no stdin, parado.\nQuem conversa com ele é o cliente MCP.\n\n**2. Configure a credencial.**\nO servidor MCP lê a mesma configuração da CLI, então `login` uma vez serve para os dois:\n\n```bash\natualia login   # pergunta url, app, client_id e secret_key; grava em ~/.atualia/config.json\n```\n\n**3. Registre o servidor.**\nRode de dentro do projeto onde ele deve valer:\n\n```bash\nclaude mcp add atualia -- atualia-mcp\n```\n\nTudo depois de `--` é o comando que o Claude Code executa; sem esse separador, o resto da\nlinha vira argumento do `add`.\n\nO escopo decide quem enxerga o servidor, e a escolha importa porque a credencial aponta\npara um app só:\n\n| escopo | onde vale | quando usar |\n| --- | --- | --- |\n| `--scope local` (padrão) | só você, só neste projeto | o normal: um app por projeto |\n| `--scope project` | todo mundo, via `.mcp.json` versionado | time inteiro no mesmo app |\n| `--scope user` | você, em todos os projetos | uma credencial que serve para tudo que você faz |\n\n**4. Confirme que subiu.**\n\n```bash\nclaude mcp list          # atualia deve aparecer como ✔ Connected\nclaude mcp get atualia   # escopo, comando e variáveis em uso\n```\n\nDentro de uma sessão, `/mcp` mostra o mesmo e lista as ferramentas.\nA partir daí o agente chama `atualia_status` e segue do catálogo.\n\n### Credencial na configuração do servidor\n\nEm CI, container ou máquina compartilhada, onde não há `atualia login` a rodar, as\nvariáveis vão no próprio registro — elas têm precedência sobre o arquivo:\n\n```bash\nclaude mcp add atualia \\\n  -e ATUALIA_URL=https://atualia-api.codeharbor.com.br \\\n  -e ATUALIA_APP_ID=agenda \\\n  -e ATUALIA_CLIENT_ID=ak_... \\\n  -e ATUALIA_SECRET_KEY=sk_... \\\n  -- atualia-mcp\n```\n\nCom `--scope project` isso entra num `.mcp.json` que vai para o git, e aí a `secret_key`\nliteral não pode estar lá.\nDuas saídas: versionar só `ATUALIA_URL` e `ATUALIA_APP_ID` e deixar a credencial de cada\npessoa no `atualia login`, ou usar a expansão que o Claude Code faz no `.mcp.json`\n(`${VAR}` e `${VAR:-padrão}`, em `command`, `args`, `env`, `url` e `headers`):\n\n```json\n{\n  \"mcpServers\": {\n    \"atualia\": {\n      \"command\": \"atualia-mcp\",\n      \"env\": {\n        \"ATUALIA_URL\": \"${ATUALIA_URL:-https://atualia-api.codeharbor.com.br}\",\n        \"ATUALIA_APP_ID\": \"agenda\",\n        \"ATUALIA_CLIENT_ID\": \"${ATUALIA_CLIENT_ID}\",\n        \"ATUALIA_SECRET_KEY\": \"${ATUALIA_SECRET_KEY}\"\n      }\n    }\n  }\n}\n```\n\nVariável sem valor e sem padrão não impede o arquivo de carregar: o `claude mcp list`\navisa e o texto `${VAR}` vai cru para o servidor, que responde erro de credencial.\n\n### Outros clientes MCP\n\nCliente que se configura por arquivo (`claude_desktop_config.json`, o `.mcp.json` de um\nprojeto, a configuração de MCP do seu editor) recebe o mesmo servidor:\n\n```json\n{\n  \"mcpServers\": {\n    \"atualia\": {\n      \"command\": \"atualia-mcp\",\n      \"env\": { \"ATUALIA_APP_ID\": \"agenda\", \"ATUALIA_CLIENT_ID\": \"ak_...\" }\n    }\n  }\n}\n```\n\n### Trocar de app, atualizar, remover\n\n```bash\natualia login                  # aponta a credencial para outro app\nclaude mcp remove atualia      # sem -s, remove do escopo em que estiver\nclaude mcp add atualia -- atualia-mcp\n```\n\nTrocar a credencial não exige reinstalar nada: o servidor lê a configuração ao subir.\nDepois de `atualia login`, saia e volte à sessão para o servidor reiniciar.\n\n### Quando não conecta\n\n| sintoma | causa provável |\n| --- | --- |\n| `Status: ✘ Failed to connect` | `atualia-mcp` não está no PATH do processo que abriu o Claude Code — teste com `which atualia-mcp` no mesmo terminal |\n| conecta, mas toda ferramenta responde \"Configuração ausente\" | não há `~/.atualia/config.json` nem variáveis no registro; rode `atualia login` ou passe `-e` |\n| ferramentas somem depois de um `npm i -g` | o link do binário mudou; `claude mcp remove` e `add` de novo |\n| erro 401 em toda chamada | credencial de outro app ou revogada — confira com `atualia config` e `atualia ping` |\n\nPara ver o que o servidor fala, `claude --debug` mostra a conversa e o stderr do processo.\nNada além de JSON-RPC vai para o stdout, então não espere log por lá.\n\n### As ferramentas\n\n| ferramenta | o que faz |\n| --- | --- |\n| `atualia_status` | catálogo do app: credencial em uso, APIs e collections com resumo |\n| `atualia_api_doc` | contrato de uma API: documentação, campos, exemplo de retorno |\n| `atualia_api_call` | executa a API pela chave, com os campos declarados |\n| `atualia_db_doc` | esquema de uma collection |\n| `atualia_db_query` | consulta com filtros, ordenação e paginação |\n| `atualia_db_insert` / `update` / `delete` | escrita nas collections |\n| `atualia_relatorio_doc` | contrato de um relatório: filtros, tipos e formatos |\n| `atualia_relatorio_gerar` | gera o relatório e devolve a URL temporária |\n\n`atualia_status` é a porta de entrada: dele saem as chaves de API, os nomes de collection e\nos nomes de relatório que as outras ferramentas recebem.\nNão existe ferramenta de listagem separada porque o status já traz as três listas.\n\nVale para o MCP tudo o que vale para a CLI: a credencial define o app, o app define o\nalcance, e a documentação de cada API e collection vem do servidor.\nA resposta de cada ferramenta é TOON, pela mesma razão da CLI.\nErro de execução volta como conteúdo com `isError`, e não como erro de protocolo, para o\nmodelo poder ler a mensagem e corrigir a chamada.\n\n## Documentação vem do servidor\n\nToda API e toda collection carregam a documentação escrita por quem as cadastrou:\n`documentacao` (Markdown), `exemploResposta` e, por campo, `descricao` e `exemplo`.\nÉ o que permite descobrir o contrato sem sair da ferramenta — e, na API\ncompartilhada, é a única documentação disponível, já que a URL de destino fica\noculta.\n\n## Erros\n\n```ts\nimport { AtualiaError } from '@codeharbor-br/atualia-sdk'\n\ntry {\n  await atualia.api.call('core-pedido-criar', { body: { estimo } }) // falta a instância\n} catch (erro) {\n  if (erro instanceof AtualiaError) {\n    console.error(erro.status, erro.message) // 400 Parâmetro obrigatório ausente: 'Instância'\n  }\n}\n```\n\nStatus do **sistema de destino** não é exceção: um 404 ou 500 do ERP chega em\n`resposta.status`, para você distinguir \"a chamada falhou\" de \"o destino respondeu\nerro\".\n\nDois status que valem tratar explicitamente:\n\n- **429** — teto por minuto da credencial. O header `Retry-After` diz quantos segundos\n  esperar;\n- **401** — credencial, assinatura ou relógio. A mensagem distingue o que é erro de\n  configuração (relógio fora da janela, app divergente, chamada sem assinatura) do que é\n  credencial inválida.\n","readmeFilename":"README.md"}