brito-cli 0.7.5 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +54 -4
  2. package/README.pt-BR.md +54 -4
  3. package/dist/agent/approval.js +19 -2
  4. package/dist/agent/claude-cli.js +10 -4
  5. package/dist/agent/loop.js +38 -9
  6. package/dist/agent/prompt.js +27 -1
  7. package/dist/agent/sessions.js +98 -0
  8. package/dist/cli.js +296 -26
  9. package/dist/desktop/panels.js +93 -0
  10. package/dist/desktop/protocol.js +5 -0
  11. package/dist/desktop/serve.js +191 -23
  12. package/dist/engine.js +28 -4
  13. package/dist/local/digest.js +202 -0
  14. package/dist/local/slm.js +218 -0
  15. package/dist/mcp/server.js +36 -3
  16. package/dist/model.js +21 -0
  17. package/dist/orchestrator.js +131 -0
  18. package/dist/permissions.js +4 -0
  19. package/dist/provider/anthropic-params.js +32 -0
  20. package/dist/provider/anthropic.js +13 -9
  21. package/dist/provider/fallback.js +67 -8
  22. package/dist/provider/index.js +4 -0
  23. package/dist/provider/local.js +88 -0
  24. package/dist/provider/resolve.js +8 -0
  25. package/dist/repl.js +49 -6
  26. package/dist/rules.js +102 -0
  27. package/dist/skills/builtin.js +89 -0
  28. package/dist/skills/describe.js +3 -2
  29. package/dist/skills/learn.js +2 -2
  30. package/dist/skills/store.js +64 -8
  31. package/dist/skills/trial.js +3 -3
  32. package/dist/tools/delegate.js +31 -22
  33. package/dist/tools/digest.js +29 -0
  34. package/dist/tools/fs.js +51 -8
  35. package/dist/tools/implement.js +50 -0
  36. package/dist/tools/registry.js +10 -1
  37. package/dist/tools/skills.js +10 -6
  38. package/dist/tools/spec.js +47 -0
  39. package/dist/tools/todo.js +22 -0
  40. package/dist/ui/ui.js +12 -0
  41. package/dist/util/todos.js +4 -0
  42. package/package.json +10 -6
  43. package/dist/setup.js +0 -110
package/README.md CHANGED
@@ -18,7 +18,7 @@ Brito is an agent for software development. Use it in the terminal (`brito-cli`
18
18
  - **Wide coverage.** TypeScript/JavaScript, Python and the script blocks of `.svelte`, `.vue` and `.astro` components. Import aliases (`tsconfig` paths, `package.json` imports, SvelteKit's `$lib`) become real edges. Files ignored by `.gitignore` stay out.
19
19
  - **Skills that learn on their own, on probation.** After a task that tripped or was corrected, Brito reflects and creates a skill *in trial* without interrupting you. A skill proves itself by use: 3 clean uses promote it, 2 stumbles archive it (recoverable). Anything risky, or any change to a skill you approved, still waits for your `/skills approve`. `BRITO_SKILLS=ask` restores approval-for-everything; `off` disables learning.
20
20
  - **Safety rails for what gets learned.** A lint rejects secrets, credentials embedded in URLs, `curl | sh` and prompt-injection phrasing, and **warns about destructive commands** (see below).
21
- - **Permission modes.** manual, auto-edits, plan and "everything allowed" (`Shift+Tab` cycles them). Autonomous skills do not depend on the mode: they are created in trial and judged by results.
21
+ - **Permission modes and rules.** manual, auto-edits, plan and "everything allowed" (`Shift+Tab` cycles them), plus `allow`/`deny` rules per command or path (see [Permission rules](#permission-rules)). Editing a file requires having read it in the session, unchanged since. Autonomous skills do not depend on the mode: they are created in trial and judged by results.
22
22
  - **Two backends, one interface.** Use an Anthropic API key, or your Claude Code subscription through the `claude` CLI. Brito never reads or sends your subscription token.
23
23
  - **Terminal or desktop app.** `npm i -g brito-cli` gives you the `brito` command; the desktop app is a native executable with a window (projects and sessions in a sidebar, chat, permission prompts), no Node or terminal needed. See [Install](#install).
24
24
  - **Images.** Drag an image file into the prompt (or paste its path) and it is attached to the turn: PNG, JPEG, GIF or WebP, up to 5 MB.
@@ -73,6 +73,17 @@ flowchart LR
73
73
 
74
74
  The lint **rejects** secrets/tokens, absolute user paths, credentials in URLs (`user:password@host`), `curl | sh`, and instructions to override rules or skip approval. It **warns** (without blocking) about commands that destroy data: removing database or data files, `DROP`/`TRUNCATE`, `DELETE` without `WHERE`, migration resets, `git reset --hard`, force-push, Docker volume removal. A skill is a standing instruction, so a step that was only safe in the state of one moment must not silently become the default.
75
75
 
76
+ ## Local model (optional)
77
+
78
+ `brito slm pull` downloads a small local model (Qwen3-4B-Instruct-2507 Q4_K_M, Apache-2.0) to `~/.brito/models` through the optional `node-llama-cpp` dependency. `brito slm` shows the status and `brito slm rm` deletes it. Once downloaded it is used for:
79
+
80
+ - **Skill learning**: the reflection runs locally with grammar-constrained JSON and falls back to the usual model on any failure.
81
+ - **`digest` tool**: a scout and refiner for long texts. The local model picks the blocks most related to your objective and proposes excerpts; code then checks every quote against the source and drops what is not there, so the main model gets a small markdown draft with verified quotes and page numbers.
82
+
83
+ - **Fallback when Claude runs out**: chain Claude → `qwen` profile → local SLM, with the full agent loop and tools (same permissions). It is temporary: after a window (the reset time reported by the error, else 10 min, or 60 min for quota/auth; `BRITO_FALLBACK_MINUTES` overrides) the next turn tries Claude again. `/fallback` shows when. A 4B model errs more with tools: use it to keep going, not for big tasks. `BRITO_SLM_FALLBACK=off` removes it from the chain; `BRITO_SLM_CTX` sets its context.
84
+
85
+ `BRITO_SLM=off` disables it; `BRITO_SLM_MODEL` points to another GGUF (`hf:` URI or file path).
86
+
76
87
  ## Install
77
88
 
78
89
  ### Terminal (`brito-cli`)
@@ -103,7 +114,9 @@ export ANTHROPIC_API_KEY=sk-ant-... # Anthropic API
103
114
 
104
115
  ### Desktop app (executable)
105
116
 
106
- The same agent in a window: projects and sessions in a sidebar, chat with markdown, collapsible tool calls, permission prompts, and pickers for model, effort and permission mode. It needs neither Node nor a terminal.
117
+ The same agent in a window: projects and sessions in a sidebar, streaming chat with markdown and code copy, tool cards with diffs for edits, a pinned task list, permission prompts with a diff ("allow once / this session / always in this project"), and pickers for model, effort and permission mode. It needs neither Node nor a terminal (on Windows the code graph still needs Node.js 20+).
118
+
119
+ Beyond the chat: type `/` for commands and skills, `@` to pick project files (or drop files on the box), `Ctrl/Cmd+K` for the command palette (actions and full-text search across sessions), panels for Skills, Agents, the code Graph, Memory and Changed files, an engine badge (cloud / endpoint / local) with a fallback warning, light/dark theme and export to markdown.
107
120
 
108
121
  ![Brito desktop app answering a question with the code graph](docs/images/desktop-app.png)
109
122
 
@@ -130,6 +143,9 @@ cd your-project
130
143
  brito # opens the REPL
131
144
  brito "who calls parseConfig?" # single prompt, no REPL
132
145
  brito index # (re)index the graph; it is also kept up to date automatically
146
+ brito -c # resume the most recent session in this project
147
+ brito sessions # list saved sessions; resume one with brito --resume <id>
148
+ brito slm pull # (optional) download the local model used by digest and skill learning
133
149
  brito query "MATCH (c:Symbol {kind:'component'})-[:CALLS]->(f:Symbol) RETURN c.file, f.name LIMIT 5"
134
150
  ```
135
151
 
@@ -150,10 +166,15 @@ OK: 20 arquivos, 66 símbolos, 54 chamadas, 17 imports | +0 ~0 -0 =20 (28ms)
150
166
  | `/fallback` | shows the main model and the fallback (qwen), and whether the switch already happened |
151
167
  | `/modo` or `Shift+Tab` | switch permission mode: manual → auto-edits → plan → bypass ("everything allowed") |
152
168
  | `/skills` | list skills · `pending` · `approve <name>` · `reject <name>` · `consolidate` · `rm <name>` · `promote <name>` |
169
+ | `/permissions` | list permission rules · `allow <rule>` · `deny <rule>` |
153
170
  | `/learn` | extract skills from the last turn now |
171
+ | `/grill-me`, `/grill-with-docs`, `/wayfinder`, `/grill-diff` | built-in manual skills: interview you until the idea is firm (`--um` = one question per message), also save decisions with `remember`, map big work and delegate, question the current diff |
172
+ | `/skills suggest <task>` | rank skills (built-ins included) by relevance to a task |
154
173
  | `/agents`, `/tools`, `/index`, `/context` | inspect subagents, tools, the index and the context window |
155
174
  | `!command` | run a shell command directly, without calling the model |
156
- | `/clear`, `/exit` | clear the conversation, quit |
175
+ | `/todos` | show the session's task list (the agent keeps it with `todo_write` on multi-step tasks) |
176
+ | `/sessions` | list this project's saved sessions |
177
+ | `/clear`, `/exit` | start another conversation (the previous one stays saved), quit |
157
178
 
158
179
  ## What the graph indexes
159
180
 
@@ -163,6 +184,26 @@ OK: 20 arquivos, 66 símbolos, 54 chamadas, 17 imports | +0 ~0 -0 =20 (28ms)
163
184
  - **Backend languages:** the graph holds classes/interfaces/structs/traits/enums/modules, functions and methods (`Class.method`), calls and imports. Imports name a package, not a path, so Brito matches them against the end of your files' paths (`com.acme.Repo` ↔ `src/main/java/com/acme/Repo.java`; PHP PSR-4 namespaces; Rust `use crate::…`; Ruby `require`/`require_relative`; Go packages through the `go.mod` module path). External packages (`java.util`, `serde`, `github.com/gin-gonic/gin`) never match and create no edges. There is no type inference: a call like `userRepository.save()` is linked by matching the receiver name to a type in the project (`UserRepository`), `this`/`self`/receivers resolve to the enclosing class, and a bare call inside a class tries the class's own methods first.
164
185
  - **Not yet:** `extends` by package (`@tsconfig/node20`), sibling-workspace `exports`, and function references without a call in a template (`onclick={fn}`). Calls are resolved statically with name-based fallbacks, so dynamic dispatch and reflection are not captured.
165
186
 
187
+ ## Permission rules
188
+
189
+ To stop approving the same command every time (without resorting to "everything allowed"), declare rules in `.brito/settings.json` (per project, can be committed), `.brito/settings.local.json` (just you) or `~/.brito/settings.json` (all projects). The lists add up.
190
+
191
+ ```json
192
+ {
193
+ "permissions": {
194
+ "allow": ["bash(npm test:*)", "bash(npm run typecheck)", "bash(git status:*)", "edit_file(src/**)"],
195
+ "deny": ["bash(git push:*)", "bash(rm -rf*)", "read_file(.env*)"]
196
+ }
197
+ }
198
+ ```
199
+
200
+ - `tool` matches any use; `tool(pattern)` narrows it. In `bash`, `*` matches anything and a trailing `:*` matches the prefix alone or followed by arguments (`git status:*` allows `git status --short`, not `git statusx`). In paths, `*` does not cross directories and `**` does.
201
+ - **`allow` skips the prompt** in manual and auto-edits modes. Plan mode still blocks.
202
+ - **`deny` always blocks**, even in "everything allowed".
203
+ - A compound command is allowed only if **every piece** (`&&`, `||`, `;`, `|`) matches a rule: `bash(npm test*)` does not allow `npm test && rm -rf ~`. Commands with `$(...)`, backticks or redirection to a file are never allowed by a rule; `2>&1` and `> /dev/null` are fine.
204
+ - The rules also apply to the `claude` CLI's Bash, Edit and Write when Brito runs on your subscription.
205
+ - `/permissions` lists the rules; `/permissions allow bash(npm test:*)` writes to `.brito/settings.json`.
206
+
166
207
  ## Databases and other sensitive resources
167
208
 
168
209
  Brito has **no database drivers and connects to nothing on its own**. When it needs to touch a database, it uses your project's own tooling through the shell, under the permission mode you chose. Because every project is different (a local file, a remote server, a container), declare the sensitive parts in `BRITO.md` (or `CLAUDE.md` / `AGENTS.md`), which is read into the system prompt:
@@ -220,6 +261,15 @@ With Claude as the main model (API or `claude` CLI), the `qwen` profile is the a
220
261
  - Without `ANTHROPIC_API_KEY` and without the `claude` CLI, Brito opens straight on Qwen, with a warning. If the fallback is not ready, the usual error includes why (e.g. `BRITO_QWEN_API_KEY` is missing). An invalid `qwen` profile is still an error, never a silent fallback.
221
262
  - `/fallback` shows the state. In the desktop app, send `/fallback` as a message; the status bar shows Qwen while the fallback is active.
222
263
 
264
+ ### Orchestrator mode: Claude plans, Qwen implements
265
+
266
+ `/orquestrar on` keeps Claude as the main model but hands the implementation to an endpoint profile (`qwen` by default). Claude writes the spec and the task list to `spec/<feature>/spec.md` and `spec/<feature>/tasks.md` (`spec_save` tool), waits for your approval, and only then sends one task at a time to the `implement` tool, checking off in `tasks.md` the ones that pass its review. It loses its file-editing tools (`write_file`/`edit_file`, or `Edit`/`Write`/`MultiEdit`/`NotebookEdit` in `claude` CLI mode): its only write is `spec_save`, which can only write those two files. It keeps reading, the graph and `bash` to check the work.
267
+
268
+ - The implementer runs in an isolated context (it does not see the conversation), with the graph of the symbols passed in `focus`. Its edits and commands follow the permission mode, and in `claude` CLI mode they are approved in the terminal just like Claude's.
269
+ - When it finishes, the tool itself appends `git status --short`, `git diff --stat` and the output of the task's `verify` command, so Claude judges by what changed on disk, not by the implementer's report.
270
+ - `/orquestrar` shows the state; `/orquestrar off` turns it off; `/orquestrar modelo <profile>` picks another implementer profile (also `BRITO_IMPL_MODEL` or `implModel` in the config). They combine: `/orquestrar on modelo qwen`. With `--global`, what you asked for is saved in the config: `/orquestrar on --global` turns the mode on every time Brito starts (`orchestrator: true`), and `/orquestrar off --global` undoes it. The status bar shows `⚙ →qwen` while it is on. In the desktop app, use the "Implementa" picker.
271
+ - It only turns on with a valid profile, its key in the environment and a main model other than the implementer. If the main model becomes Qwen (fallback or `/model qwen`), the mode turns off with a warning.
272
+
223
273
  ## Configuration
224
274
 
225
275
  `BRITO_MODEL`, `BRITO_EFFORT`, `BRITO_EXPLORE_MODEL`, `BRITO_LEARN_MODEL`, `BRITO_HOME` (where global skills and history live), `BRITO_CONFIG`, `BRITO_COLOR`, `BRITO_BANNER` (`compact` or `full`; default: full screen on windows of 95+ columns and 28+ rows), `BRITO_INDEX_IGNORED`, `BRITO_SKILL_MIN_TOOLS` (how long a turn must be to trigger reflection when nothing else does), `BRITO_SKILLS` (`auto` default, `ask`, `off`), `BRITO_SHELL` (shell used by the `bash` tool; on Windows defaults to Git Bash when installed).
@@ -229,7 +279,7 @@ Project data lives in `.brito/` (graph, project skills, agents) and global data
229
279
  ## Limitations
230
280
 
231
281
  - Early software (v0.5.0), one maintainer.
232
- - The desktop app is new: a single window (no tabs yet), and with an API key (no `claude` CLI) the model does not resume a conversation after you reopen the app, although the screen shows it.
282
+ - The desktop app is young: a single window (no tabs yet), and no image attachments (drop files to reference them with `@path`). API-mode conversations from before this version are not resumed when you reopen the app (new ones are, from `.brito/sessions`).
233
283
  - **The interface and the agent's default language are Brazilian Portuguese.** Translating them is a welcome contribution.
234
284
  - Skill learning is a heuristic: it proposes, you decide. Expect some proposals to be rejected.
235
285
 
package/README.pt-BR.md CHANGED
@@ -18,7 +18,7 @@ O Brito é um agente para desenvolvimento de software. Use no terminal (`brito-c
18
18
  - **Cobertura ampla.** TypeScript/JavaScript, Python e os blocos de script de componentes `.svelte`, `.vue` e `.astro`. Aliases de import (`paths` do tsconfig, `imports` do package.json, `$lib` do SvelteKit) viram arestas de verdade. O que o `.gitignore` ignora fica de fora.
19
19
  - **Skills que aprendem sozinhas, em período de teste.** Depois de uma tarefa em que algo tropeçou ou foi corrigido, o Brito reflete e cria uma skill *em teste* sem te interromper. A skill se prova pelo uso: 3 usos limpos a promovem, 2 tropeços a arquivam (recuperável). O que é arriscado, ou qualquer mudança numa skill que você aprovou, continua esperando o seu `/skills approve`. `BRITO_SKILLS=ask` volta a pedir aprovação para tudo; `off` desliga o aprendizado.
20
20
  - **Proteções sobre o que é aprendido.** Um lint rejeita segredos, credenciais embutidas em URL, `curl | sh` e frases de injeção de prompt, e **avisa sobre comandos destrutivos** (veja abaixo).
21
- - **Modos de permissão.** manual, auto-edições, plano e "tudo liberado" (`Shift+Tab` alterna). As skills autônomas não dependem do modo: nascem em teste e são julgadas pelo resultado.
21
+ - **Modos e regras de permissão.** manual, auto-edições, plano e "tudo liberado" (`Shift+Tab` alterna), mais regras `allow`/`deny` por comando ou caminho (veja [Regras de permissão](#regras-de-permissão)). Editar um arquivo exige tê-lo lido na sessão, sem mudanças desde então. As skills autônomas não dependem do modo: nascem em teste e são julgadas pelo resultado.
22
22
  - **Dois backends, uma interface.** Use uma chave da API da Anthropic ou a sua assinatura do Claude Code pela CLI `claude`. O Brito nunca lê nem envia o token da sua assinatura.
23
23
  - **Terminal ou app de desktop.** `npm i -g brito-cli` instala o comando `brito`; o app de desktop é um executável nativo com janela (projetos e sessões numa barra lateral, chat, pedidos de permissão), sem precisar de Node nem de terminal. Veja [Instalação](#instalação).
24
24
  - **Imagens.** Arraste um arquivo de imagem para o prompt (ou cole o caminho) e ele é anexado ao turno: PNG, JPEG, GIF ou WebP, até 5 MB.
@@ -69,6 +69,17 @@ flowchart LR
69
69
 
70
70
  O lint **rejeita** segredos/tokens, caminhos absolutos do usuário, credenciais em URL (`usuario:senha@host`), `curl | sh` e instruções para sobrepor regras ou pular aprovação. Ele **avisa** (sem bloquear) sobre comandos que destroem dados: apagar arquivos de banco ou de dados, `DROP`/`TRUNCATE`, `DELETE` sem `WHERE`, reset de migrações, `git reset --hard`, force push, remoção de volumes do Docker. Uma skill é uma instrução permanente, então um passo que só era seguro no estado de um momento não pode virar o padrão em silêncio.
71
71
 
72
+ ## Modelo local (opcional)
73
+
74
+ `brito slm pull` baixa um modelo local pequeno (Qwen3-4B-Instruct-2507 Q4_K_M, Apache-2.0) para `~/.brito/models`, pela dependência opcional `node-llama-cpp`. `brito slm` mostra o status e `brito slm rm` o apaga. Depois de baixado, ele é usado em:
75
+
76
+ - **Aprendizado de skills**: a reflexão roda localmente com JSON restrito por gramática e cai no modelo de sempre se algo falhar.
77
+ - **Tool `digest`**: batedor e refinador de textos longos. O modelo local escolhe os blocos mais ligados ao seu objetivo e propõe trechos; o código confere cada citação contra a fonte e descarta o que não existe, e o modelo principal recebe um rascunho pequeno em markdown com citações verificadas e números de página.
78
+
79
+ - **Fallback quando o Claude acaba**: cadeia Claude → perfil `qwen` → SLM local, com o loop completo do agente e as tools (mesmas permissões). É temporário: passada a janela (o horário de reset que o erro informa, senão 10 min, ou 60 min para cota/auth; `BRITO_FALLBACK_MINUTES` sobrepõe) o próximo turno tenta o Claude de novo. O `/fallback` mostra quando. Um modelo de 4B erra mais com tools: use para continuar, não para tarefas grandes. `BRITO_SLM_FALLBACK=off` tira o SLM da cadeia; `BRITO_SLM_CTX` define o contexto.
80
+
81
+ `BRITO_SLM=off` desliga; `BRITO_SLM_MODEL` aponta para outro GGUF (URI `hf:` ou caminho de arquivo).
82
+
72
83
  ## Instalação
73
84
 
74
85
  ### Terminal (`brito-cli`)
@@ -99,7 +110,9 @@ export ANTHROPIC_API_KEY=sk-ant-... # API da Anthropic
99
110
 
100
111
  ### App de desktop (executável)
101
112
 
102
- O mesmo agente numa janela: projetos e sessões numa barra lateral, chat com markdown, chamadas de ferramenta recolhíveis, pedidos de permissão e seletores de modelo, esforço e modo de permissão. Não precisa de Node nem de terminal.
113
+ O mesmo agente numa janela: projetos e sessões numa barra lateral, chat com streaming, markdown e copiar código, cartões de ferramenta com diff nas edições, lista de tarefas fixa, pedidos de permissão com diff ("permitir uma vez / nesta sessão / sempre neste projeto") e seletores de modelo, esforço e modo de permissão. Não exige Node nem terminal (no Windows o grafo de código ainda precisa do Node.js 20+).
114
+
115
+ Além do chat: `/` abre comandos e skills, `@` escolhe arquivos do projeto (ou arraste arquivos para o campo), `Ctrl/Cmd+K` abre a paleta (ações e busca em todas as sessões), há painéis de Skills, Agentes, Grafo de código, Memória e Arquivos alterados, selo do motor (nuvem / endpoint / local) com aviso de fallback, tema claro/escuro e exportação em markdown.
103
116
 
104
117
  ![App de desktop do Brito respondendo uma pergunta com o grafo de código](docs/images/desktop-app.png)
105
118
 
@@ -126,6 +139,9 @@ cd seu-projeto
126
139
  brito # abre o REPL
127
140
  brito "quem chama parseConfig?" # prompt único, sem REPL
128
141
  brito index # (re)indexa o grafo; ele também se mantém atualizado sozinho
142
+ brito -c # retoma a sessão mais recente deste projeto
143
+ brito sessoes # lista as sessões salvas; retome uma com brito --resume <id>
144
+ brito slm pull # (opcional) baixa o modelo local usado por digest e pelo aprendizado de skills
129
145
  brito query "MATCH (c:Symbol {kind:'component'})-[:CALLS]->(f:Symbol) RETURN c.file, f.name LIMIT 5"
130
146
  ```
131
147
 
@@ -146,10 +162,15 @@ OK: 20 arquivos, 66 símbolos, 54 chamadas, 17 imports | +0 ~0 -0 =20 (28ms)
146
162
  | `/fallback` | mostra o modelo principal e o fallback (qwen), e se a troca já aconteceu |
147
163
  | `/modo` ou `Shift+Tab` | troca o modo de permissão: manual → auto-edições → plano → tudo liberado |
148
164
  | `/skills` | lista skills · `pending` · `approve <nome>` · `reject <nome>` · `consolidate` · `rm <nome>` · `promote <nome>` |
165
+ | `/permissions` | lista as regras de permissão · `allow <regra>` · `deny <regra>` |
149
166
  | `/learn` | extrai skills do último turno agora |
167
+ | `/grill-me`, `/grill-with-docs`, `/wayfinder`, `/grill-diff` | skills manuais embutidas: entrevista até a ideia ficar firme (`--um` = uma pergunta por mensagem), também grava decisões com `remember`, mapeia trabalho grande e delega, questiona o diff atual |
168
+ | `/skills suggest <tarefa>` | ranqueia as skills (embutidas inclusive) por relevância para a tarefa |
150
169
  | `/agents`, `/tools`, `/index`, `/context` | inspecionam subagentes, tools, o índice e a janela de contexto |
151
170
  | `!comando` | roda um comando no shell direto, sem chamar o modelo |
152
- | `/clear`, `/exit` | limpa a conversa, sai |
171
+ | `/todos` | mostra a lista de tarefas da sessão (o agente a mantém com `todo_write` em tarefas de vários passos) |
172
+ | `/sessions` | lista as sessões salvas deste projeto |
173
+ | `/clear`, `/exit` | começa outra conversa (a anterior continua salva), sai |
153
174
 
154
175
  ## O que o grafo indexa
155
176
 
@@ -159,6 +180,26 @@ OK: 20 arquivos, 66 símbolos, 54 chamadas, 17 imports | +0 ~0 -0 =20 (28ms)
159
180
  - **Linguagens de backend:** o grafo guarda classes/interfaces/structs/traits/enums/módulos, funções e métodos (`Classe.metodo`), chamadas e imports. Nelas o import nomeia um pacote, não um caminho; o Brito casa o especificador com o final do caminho dos seus arquivos (`com.acme.Repo` ↔ `src/main/java/com/acme/Repo.java`; namespaces PSR-4 do PHP; `use crate::…` do Rust; `require`/`require_relative` do Ruby; pacotes Go pelo módulo do `go.mod`). Pacote externo (`java.util`, `serde`, `github.com/gin-gonic/gin`) nunca casa e não gera aresta. Não há inferência de tipos: `userRepository.save()` é ligado casando o nome do receptor com um tipo do projeto (`UserRepository`), `this`/`self`/receiver resolvem para a classe que contém a chamada, e uma chamada solta dentro de classe tenta primeiro os métodos da própria classe.
160
181
  - **Ainda não:** `extends` por pacote (`@tsconfig/node20`), `exports` de workspaces vizinhos e referências de função sem chamada em um template (`onclick={fn}`). As chamadas são resolvidas estaticamente, com fallback por nome, então despacho dinâmico e reflexão não são capturados.
161
182
 
183
+ ## Regras de permissão
184
+
185
+ Para não aprovar o mesmo comando toda vez (nem precisar do "tudo liberado"), declare regras em `.brito/settings.json` (do projeto, pode ir para o git), `.brito/settings.local.json` (só seu) ou `~/.brito/settings.json` (todos os projetos). As listas se somam.
186
+
187
+ ```json
188
+ {
189
+ "permissions": {
190
+ "allow": ["bash(npm test:*)", "bash(npm run typecheck)", "bash(git status:*)", "edit_file(src/**)"],
191
+ "deny": ["bash(git push:*)", "bash(rm -rf*)", "read_file(.env*)"]
192
+ }
193
+ }
194
+ ```
195
+
196
+ - `tool` vale para qualquer uso; `tool(padrão)` restringe. Em `bash`, `*` casa qualquer coisa e `:*` no fim casa o prefixo sozinho ou com argumentos (`git status:*` libera `git status --short`, não `git statusx`). Em caminhos, `*` não cruza diretórios e `**` cruza.
197
+ - **`allow` dispensa a pergunta** nos modos manual e auto-edições. O modo plano continua bloqueando.
198
+ - **`deny` bloqueia sempre**, inclusive no "tudo liberado".
199
+ - Um comando composto só é liberado se **cada pedaço** (`&&`, `||`, `;`, `|`) casar com uma regra: `bash(npm test*)` não libera `npm test && rm -rf ~`. Comandos com `$(...)`, crases ou redirecionamento para arquivo nunca são liberados por regra; `2>&1` e `> /dev/null` podem.
200
+ - As regras valem também para o Bash, o Edit e o Write do `claude` CLI quando o Brito roda sobre a sua assinatura.
201
+ - `/permissions` lista as regras; `/permissions allow bash(npm test:*)` grava no `.brito/settings.json`.
202
+
162
203
  ## Bancos de dados e outros recursos sensíveis
163
204
 
164
205
  O Brito **não tem drivers de banco e não se conecta a nada sozinho**. Quando precisa mexer em um banco, usa as ferramentas do próprio projeto pelo shell, sob o modo de permissão que você escolheu. Como cada projeto é diferente (um arquivo local, um servidor remoto, um container), declare as partes sensíveis no `BRITO.md` (ou `CLAUDE.md` / `AGENTS.md`), que é lido para o system prompt:
@@ -223,6 +264,15 @@ Com o Claude como modelo principal (API ou `claude` CLI), o perfil `qwen` é o f
223
264
  estado inativo — troca automática se o Claude falhar por limite, crédito, auth ou sobrecarga
224
265
  ```
225
266
 
267
+ ### Modo orquestrador: o Claude planeja, o Qwen implementa
268
+
269
+ `/orquestrar on` mantém o Claude como modelo principal, mas passa a implementação para um perfil de endpoint (`qwen` por padrão). O Claude grava a spec e a lista de tasks em `spec/<feature>/spec.md` e `spec/<feature>/tasks.md` (tool `spec_save`), espera sua aprovação e só então manda uma task por vez para a tool `implement`, marcando no `tasks.md` as que passaram na revisão. Ele perde as tools de edição (`write_file`/`edit_file`, ou `Edit`/`Write`/`MultiEdit`/`NotebookEdit` no modo `claude` CLI): a única escrita dele é a `spec_save`, que só grava esses dois arquivos. Fica com a leitura, o grafo e o `bash` para conferir o trabalho.
270
+
271
+ - O implementador roda em contexto isolado (não vê a conversa), com o grafo dos símbolos passados em `focus`. As edições e comandos dele seguem o modo de permissão e, no modo `claude` CLI, são aprovados no terminal como os do Claude.
272
+ - Quando ele termina, a própria tool anexa `git status --short`, `git diff --stat` e a saída do comando `verify` da task: o Claude julga pelo que mudou no disco, não pelo relato do implementador.
273
+ - `/orquestrar` mostra o estado; `/orquestrar off` desliga; `/orquestrar modelo <perfil>` escolhe outro perfil implementador (também `BRITO_IMPL_MODEL` ou `implModel` no config). Os dois combinam: `/orquestrar on modelo qwen`. Com `--global`, o que foi pedido fica salvo no config: `/orquestrar on --global` liga o modo sempre que o Brito abrir (`orchestrator: true`), e `/orquestrar off --global` desfaz. A barra de status mostra `⚙ →qwen` enquanto o modo está ligado. No app de desktop, use o seletor "Implementa".
274
+ - Só liga com o perfil válido, a chave no ambiente e um modelo principal diferente do implementador. Se o principal virar o Qwen (fallback ou `/model qwen`), o modo desliga com aviso.
275
+
226
276
  ## Configuração
227
277
 
228
278
  `BRITO_MODEL`, `BRITO_EFFORT`, `BRITO_EXPLORE_MODEL`, `BRITO_LEARN_MODEL`, `BRITO_HOME` (onde ficam skills globais e histórico), `BRITO_CONFIG`, `BRITO_COLOR`, `BRITO_BANNER` (`compact` ou `full`; padrão: tela cheia em janelas de 95+ colunas e 28+ linhas), `BRITO_INDEX_IGNORED`, `BRITO_SKILL_MIN_TOOLS` (tamanho mínimo do turno para disparar a reflexão quando nada mais dispara), `BRITO_SKILLS` (`auto` padrão, `ask`, `off`), `BRITO_SHELL` (shell da tool `bash`; no Windows usa o Git Bash se instalado).
@@ -232,7 +282,7 @@ Os dados do projeto ficam em `.brito/` (grafo, skills do projeto, agentes) e os
232
282
  ## Limitações
233
283
 
234
284
  - Software em fase inicial (v0.5.0), um mantenedor.
235
- - O app de desktop é novo: uma janela só (sem abas ainda) e, com chave de API (sem o `claude` CLI), o modelo não retoma a conversa ao reabrir o app, embora a tela a mostre.
285
+ - O app de desktop é jovem: uma janela só (sem abas ainda) e sem anexo de imagem (arraste arquivos para referenciá-los com `@caminho`). Conversas do modo API feitas antes desta versão não são retomadas ao reabrir o app (as novas são, a partir de `.brito/sessions`).
236
286
  - **A interface e o idioma padrão do agente são português do Brasil.** Traduzi-los é uma contribuição bem-vinda.
237
287
  - O aprendizado de skills é heurístico: ele propõe, você decide. Espere rejeitar algumas propostas.
238
288
 
@@ -3,11 +3,22 @@ import net from "node:net";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
5
5
  import { describeClaudeTool, perms } from "../permissions.js";
6
+ import { decide, loadRules } from "../rules.js";
6
7
  /**
7
8
  * Aprovações do claude CLI: o claude chama a tool MCP `approve` (--permission-prompt-tool), que roda no
8
9
  * processo do servidor MCP e consulta ESTE processo por um socket unix, onde está o terminal (ou a janela do app).
9
10
  */
10
- export function startApprovalServer(io) {
11
+ /** Traduz uma tool do claude CLI para o nome e o input das tools do Brito e decide pelas regras de permissão. */
12
+ export function claudeDecision(root, tool, input) {
13
+ const file = typeof input?.file_path === "string" ? input.file_path : undefined;
14
+ const mapped = tool === "Bash" ? { name: "bash", input: { command: input?.command } } :
15
+ (tool === "Edit" || tool === "MultiEdit") && file ? { name: "edit_file", input: { path: file } } :
16
+ tool === "Write" && file ? { name: "write_file", input: { path: file } } :
17
+ { name: tool, input };
18
+ return decide(loadRules(root), mapped.name, mapped.input, root);
19
+ }
20
+ /** `root` liga as regras de permissão do projeto; sem ele, toda chamada é perguntada como antes. */
21
+ export function startApprovalServer(io, root) {
11
22
  // No Windows um socket unix em %TEMP% não é aceito por net.listen: o endereço tem que ser um named pipe.
12
23
  const sock = process.platform === "win32" ? `\\\\.\\pipe\\brito-approve-${process.pid}` : path.join(os.tmpdir(), `brito-approve-${process.pid}.sock`);
13
24
  try {
@@ -25,7 +36,13 @@ export function startApprovalServer(io) {
25
36
  try {
26
37
  const req = JSON.parse(buf.slice(0, nl));
27
38
  const plan = req.tool_name === "ExitPlanMode";
28
- const ok = await io.ask(describeClaudeTool(req.tool_name, req.input), { force: plan });
39
+ // As regras do Brito valem também para as tools do claude: deny bloqueia, allow dispensa a pergunta.
40
+ const decision = root && !plan ? claudeDecision(root, req.tool_name, req.input) : "ask";
41
+ if (decision === "deny") {
42
+ conn.end(JSON.stringify({ allow: false, message: "Bloqueado por uma regra deny das permissões do Brito (.brito/settings.json). Não tente contornar." }) + "\n");
43
+ return;
44
+ }
45
+ const ok = decision === "allow" || await io.ask(describeClaudeTool(req.tool_name, req.input), { force: plan });
29
46
  if (ok && plan)
30
47
  perms.mode = "manual";
31
48
  reply = ok ? { allow: true } : { allow: false, message: plan ? "Plano não aprovado pelo usuário. Pergunte o que ajustar." : "O usuário negou a execução." };
@@ -7,6 +7,7 @@ import { skillsIndex } from "../skills/store.js";
7
7
  import { MCP_NAME, MCP_PREFIX, APPROVE_TOOL } from "../mcp/server.js";
8
8
  import { ui as terminalUi } from "../ui/ui.js";
9
9
  import { MODE_ENV, perms } from "../permissions.js";
10
+ import { CLAUDE_EDIT_TOOLS, ORCH_MCP_TIMEOUT, ORCH_NOTE, ORCH_NOTE_CLAUDE, orch, orchEnv } from "../orchestrator.js";
10
11
  import { DEFAULT_MODEL, wireEffort } from "../model.js";
11
12
  import { subagentModel } from "../engine.js";
12
13
  import { claudeCommand } from "../util/platform.js";
@@ -211,11 +212,13 @@ export class ClaudeSession {
211
212
  aborted = false;
212
213
  args(text, imageDirs = []) {
213
214
  const { root, approvalSock, stats, mcpCommand } = this.o;
214
- const mcp = mcpConfig(mcpCommand, approvalSock, perms.mode, this.o.env);
215
- const readOnlyMcp = ["graph_search", "graph_callers", "graph_callees", "graph_impact", "graph_context", "graph_query", "graph_index", "remember", "recall", "skill_load"];
215
+ // O modo orquestrador vale por turno, como o de permissão: o servidor MCP do turno sobe com ele no env.
216
+ const mcp = mcpConfig(mcpCommand, approvalSock, perms.mode, { ...this.o.env, ...orchEnv() });
217
+ const readOnlyMcp = ["graph_search", "graph_callers", "graph_callees", "graph_impact", "graph_context", "graph_query", "graph_index", "remember", "recall", "skill_load", "digest"];
216
218
  const system = buildSystemPrompt(root, stats, skillsIndex(root)) +
217
219
  `\n\n# Nesta sessão\nAs tools do Brito chegam com o prefixo \`${MCP_PREFIX}\` (ex.: ${MCP_PREFIX}graph_context). Use-as como descrito acima. ` +
218
- `read_file/grep/glob/edit_file/bash correspondem a Read/Grep/Glob/Edit/Bash, e \`delegate\` corresponde à tool Agent/Task com os subagentes configurados (explore, plan, general e os do projeto).`;
220
+ `read_file/grep/glob/edit_file/bash correspondem a Read/Grep/Glob/Edit/Bash, e \`delegate\` corresponde à tool Agent/Task com os subagentes configurados (explore, plan, general e os do projeto).` +
221
+ (orch.on ? ORCH_NOTE + ORCH_NOTE_CLAUDE : "");
219
222
  const args = [
220
223
  "-p", text,
221
224
  this.started ? "--resume" : "--session-id", this.id,
@@ -234,6 +237,8 @@ export class ClaudeSession {
234
237
  // O modo vale por turno: Shift+Tab no REPL muda o próximo turno.
235
238
  const mode = { manual: "default", auto: "acceptEdits", plan: "plan", bypass: "bypassPermissions" }[perms.mode];
236
239
  args.push("--permission-mode", mode);
240
+ if (orch.on)
241
+ args.push("--disallowedTools", ...CLAUDE_EDIT_TOOLS);
237
242
  if (perms.mode !== "bypass") {
238
243
  args.push("--allowedTools", ...readOnlyMcp.map((n) => MCP_PREFIX + n), "--permission-prompt-tool", `${MCP_PREFIX}${APPROVE_TOOL}`);
239
244
  }
@@ -255,7 +260,8 @@ export class ClaudeSession {
255
260
  const child = (this.child = spawn(cmd.command, cmd.args, {
256
261
  cwd: this.o.root, stdio: ["ignore", "pipe", "pipe"],
257
262
  // `/reasoning none`: o claude desliga o raciocínio com MAX_THINKING_TOKENS=0 (não há `--effort none`)
258
- env: { ...process.env, ...this.o.env, BRITO_DEPTH: String(Number(process.env.BRITO_DEPTH ?? 0) + 1), ...(wireEffort(this.effort).thinking ? {} : { MAX_THINKING_TOKENS: "0" }) },
263
+ env: { ...process.env, ...this.o.env, BRITO_DEPTH: String(Number(process.env.BRITO_DEPTH ?? 0) + 1),
264
+ ...(orch.on && !process.env.MCP_TOOL_TIMEOUT ? { MCP_TOOL_TIMEOUT: ORCH_MCP_TIMEOUT } : {}), ...(wireEffort(this.effort).thinking ? {} : { MAX_THINKING_TOKENS: "0" }) },
259
265
  }));
260
266
  let buf = "", stderr = "", ok = true, interrupted = false;
261
267
  const failure = { stderr: "", code: null };
@@ -5,8 +5,9 @@ import { initialEffort, initialModel } from "../model.js";
5
5
  import { providerFor } from "../provider/index.js";
6
6
  import { compactAtFor, resolveModel } from "../provider/resolve.js";
7
7
  import { fallbackStatus } from "../engine.js";
8
- import { activateFallback, classifyAnthropicError, fallbackWarning, shortError, triggersFallback } from "../provider/fallback.js";
8
+ import { activateFallback, classifyAnthropicError, fallbackExpired, fallbackUntil, fallbackWarning, returnToPrimary, shortError, triggersFallback } from "../provider/fallback.js";
9
9
  import { fmtBytes, findImages, imageBlock, splitOversize } from "../util/images.js";
10
+ import { orchGuard } from "../orchestrator.js";
10
11
  export const MODEL = initialModel();
11
12
  export class Agent {
12
13
  system;
@@ -80,6 +81,7 @@ export class Agent {
80
81
  }
81
82
  else
82
83
  push({ role: "user", content: userText });
84
+ this.retryPrimaryIfExpired();
83
85
  await this.compactIfNeeded();
84
86
  let finalText = "";
85
87
  for (let step = 0; step < this.maxSteps; step++) {
@@ -116,7 +118,8 @@ export class Agent {
116
118
  const t0 = Date.now();
117
119
  if (!this.opts.quiet)
118
120
  this.ui.toolStart(u.id, u.name, u.input);
119
- const denied = this.opts.tools && !this.opts.tools.has(u.name);
121
+ const allow = this.allowed();
122
+ const denied = allow && !allow.has(u.name);
120
123
  const r = denied ? { text: `Tool não permitida para este agente: ${u.name}`, isError: true } : await execTool(u.name, u.input, this.ctx);
121
124
  if (this.opts.quiet)
122
125
  this.log(terminalUi.toolLine(u.name, u.input, Date.now() - t0, r.isError));
@@ -147,31 +150,57 @@ export class Agent {
147
150
  try {
148
151
  return await providerFor(this.model).send({
149
152
  model: this.model,
150
- maxTokens: target.kind === "openai" && thinking ? 16_384 : 8192,
153
+ // Na Anthropic a resposta vem por streaming, então o teto pode ser alto sem risco de timeout: um
154
+ // write_file grande não é mais cortado no meio.
155
+ maxTokens: target.kind === "openai" ? (thinking ? 16_384 : 8192) : 32_000,
151
156
  system: typeof this.system === "function" ? this.system() : this.system,
152
- tools: toApiTools(this.opts.tools),
157
+ tools: toApiTools(this.allowed()),
153
158
  messages: this.messages,
154
159
  thinking,
160
+ effort: this.effort,
155
161
  }, hooks);
156
162
  }
157
163
  catch (e) {
158
164
  const fb = this.opts.fallback;
159
- if (!fb || fb.active || hooks.signal?.aborted || target.kind !== "anthropic")
165
+ if (!fb || hooks.signal?.aborted)
160
166
  throw e;
161
- const kind = classifyAnthropicError(e);
167
+ // Claude → qwen → SLM: com o fallback ativo só desce um degrau (qwen que falha cai no SLM), nunca o SLM.
168
+ const onQwen = fb.active && target.kind === "openai" && !target.local && target.name === "qwen";
169
+ if (fb.active ? !onQwen : target.kind !== "anthropic" || target.local)
170
+ throw e;
171
+ if (onQwen && target.kind !== "openai")
172
+ throw e;
173
+ const kind = onQwen ? "network" : classifyAnthropicError(e);
162
174
  if (!triggersFallback(kind))
163
175
  throw e;
164
- const status = fallbackStatus(this.model, process.env);
176
+ const status = fallbackStatus(fb.primary, process.env, onQwen ? ["qwen"] : []);
165
177
  if (!status.ready)
166
178
  throw e;
167
179
  const why = shortError(e);
168
- this.ui.warn(fallbackWarning(kind, why, status.label));
169
- activateFallback(fb, `${kind} (${why})`);
180
+ const until = onQwen ? fb.until : fallbackUntil(kind, why);
181
+ this.ui.warn(fallbackWarning(kind, why, status.label, until));
182
+ activateFallback(fb, `${kind} (${why})`, Date.now(), until, status.name);
170
183
  this.model = status.name;
184
+ orchGuard(this.model, (m) => this.ui.warn(m)); // o qwen virou o principal: não há mais a quem delegar
171
185
  await this.compactIfNeeded();
172
186
  return this.callModel(hooks);
173
187
  }
174
188
  }
189
+ allowed() {
190
+ return typeof this.opts.tools === "function" ? this.opts.tools() : this.opts.tools;
191
+ }
192
+ /**
193
+ * Fallback temporário: passada a janela, o próximo turno tenta o Claude de novo (se falhar, `callModel` reativa o
194
+ * fallback com uma janela nova). Só com ANTHROPIC_API_KEY: sem ela o Claude não roda neste loop (volta via /model).
195
+ */
196
+ retryPrimaryIfExpired() {
197
+ const fb = this.opts.fallback;
198
+ if (!fb || !fallbackExpired(fb) || !process.env.ANTHROPIC_API_KEY?.trim() || resolveModel(fb.primary).kind !== "anthropic")
199
+ return;
200
+ returnToPrimary(fb);
201
+ this.model = fb.primary;
202
+ this.ui.info(`voltando ao Claude (${fb.primary}) para testar se o limite acabou`);
203
+ }
175
204
  async compactIfNeeded() {
176
205
  if (estimateTokens(this.messages) < compactAtFor(this.model))
177
206
  return;
@@ -34,13 +34,39 @@ export function buildSystemPrompt(root, graphStats, skills = "") {
34
34
  - Edições e comandos shell pedem aprovação do usuário. Prefira edit_file a write_file. Verifique seu trabalho (typecheck/testes) via bash quando existir.
35
35
  - Para investigações amplas ou sub-tarefas grandes, use delegate: o subagente trabalha em contexto isolado e devolve só o resultado. Vários delegate somente-leitura (explore/plan) no mesmo turno rodam em paralelo. Passe \`focus\` com os símbolos relevantes. Não delegue o que resolve com 1–2 chamadas de graph_*.
36
36
  - SKILLS: procedimentos reutilizáveis aprendidos neste projeto. Se alguma se aplica à tarefa, chame skill_load ANTES de começar e siga-a. Se ela estiver errada ou incompleta, corrija com skill_save. Ao terminar tarefas complexas, o sistema pode propor novas skills; não crie skill para fatos de uma tarefa única.
37
+ - Em tarefas com 3+ passos, mantenha a lista com todo_write: um item in_progress por vez, marcado completed assim que terminar. Leia um arquivo (read_file) antes de editá-lo; se ele mudou desde a leitura, leia de novo.
37
38
  - Seja conciso. Não repita conteúdo de arquivos na resposta.
38
39
 
39
40
  # Ambiente
40
41
  Diretório do projeto: ${root}
41
42
  Data: ${new Date().toISOString().slice(0, 10)}${shellNote()}${skills ? `\n\n# Skills disponíveis (use skill_load)\n${skills}` : ""}${projectDoc}`;
42
43
  }
43
- import { skillsIndex } from "../skills/store.js";
44
+ import { listSkills, skillsIndex } from "../skills/store.js";
45
+ /**
46
+ * Repete o system prompt anterior enquanto `key()` não muda. O cache de prompt da API é um prefixo: qualquer byte
47
+ * diferente no system (a contagem de símbolos após um sync, a ordem das skills por uso) invalidaria o cache da
48
+ * conversa inteira a cada passo. A chave lista só o que muda o que o modelo deve fazer.
49
+ */
50
+ export function memoSystem(key, build) {
51
+ let last;
52
+ let value = "";
53
+ return () => {
54
+ const k = key();
55
+ if (k !== last) {
56
+ last = k;
57
+ value = build();
58
+ }
59
+ return value;
60
+ };
61
+ }
62
+ /**
63
+ * Chave do `memoSystem` da sessão principal: modo plano, grafo vazio ou não (o aviso de "grafo vazio" muda a
64
+ * orientação) e o conjunto de skills com o status. Contagens e ordem por uso ficam de fora de propósito.
65
+ */
66
+ export function systemKey(root, planMode, graphEmpty) {
67
+ const skills = listSkills(root).map((s) => `${s.name}:${s.scope}:${s.status}`).sort().join(",");
68
+ return `${planMode}|${graphEmpty}|${skills}`;
69
+ }
44
70
  export function buildSubagentPrompt(root, def) {
45
71
  return `${def.prompt}
46
72
 
@@ -0,0 +1,98 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ const MAX_SESSIONS = 50;
4
+ const ID = /^[\w-]{1,80}$/;
5
+ export const sessionsDir = (root) => path.join(root, ".brito", "sessions");
6
+ const fileOf = (root, id) => {
7
+ if (!ID.test(id))
8
+ throw new Error(`id de sessão inválido: ${id}`);
9
+ return path.join(sessionsDir(root), `${id}.json`);
10
+ };
11
+ const stripImages = (msgs) => msgs.map((m) => typeof m.content === "string" ? m : {
12
+ ...m,
13
+ content: m.content.map((b) => (b.type === "image" || b.type === "document" ? { type: "text", text: "[anexo omitido ao salvar a sessão]" } : b)),
14
+ });
15
+ export function titleOf(msgs, fallback = "") {
16
+ const first = msgs?.find((m) => m.role === "user");
17
+ const text = typeof first?.content === "string" ? first.content : first?.content.flatMap((b) => (b.type === "text" ? [b.text] : [])).join(" ") ?? fallback;
18
+ return (text || fallback).replace(/\s+/g, " ").trim().slice(0, 80);
19
+ }
20
+ /** Grava (ou atualiza) a sessão. Nunca derruba o turno: falha de disco vira aviso de quem chamou. */
21
+ export function saveSession(root, s, now = new Date()) {
22
+ const dir = sessionsDir(root);
23
+ fs.mkdirSync(dir, { recursive: true });
24
+ // Conversas são pessoais e podem citar dados sensíveis: nunca vão para o git por acidente.
25
+ try {
26
+ fs.writeFileSync(path.join(dir, ".gitignore"), "*\n", { flag: "wx" });
27
+ }
28
+ catch (e) {
29
+ if (e.code !== "EEXIST")
30
+ throw e;
31
+ }
32
+ const prev = loadSession(root, s.id);
33
+ const out = {
34
+ v: 1, ...s,
35
+ ...(s.messages ? { messages: stripImages(s.messages) } : {}),
36
+ createdAt: s.createdAt ?? prev?.createdAt ?? now.toISOString(),
37
+ updatedAt: now.toISOString(),
38
+ };
39
+ const file = fileOf(root, s.id);
40
+ const tmp = `${file}.tmp`;
41
+ fs.writeFileSync(tmp, JSON.stringify(out));
42
+ fs.renameSync(tmp, file); // atômico: um Ctrl+C no meio não deixa JSON cortado
43
+ prune(root);
44
+ }
45
+ export function loadSession(root, id) {
46
+ try {
47
+ const s = JSON.parse(fs.readFileSync(fileOf(root, id), "utf8"));
48
+ return s?.v === 1 && typeof s.id === "string" ? s : null;
49
+ }
50
+ catch {
51
+ return null;
52
+ }
53
+ }
54
+ /** Mais recentes primeiro. */
55
+ export function listSessions(root) {
56
+ let names;
57
+ try {
58
+ names = fs.readdirSync(sessionsDir(root)).filter((n) => n.endsWith(".json"));
59
+ }
60
+ catch {
61
+ return [];
62
+ }
63
+ return names
64
+ .flatMap((n) => { const s = loadSession(root, n.slice(0, -5)); return s ? [s] : []; })
65
+ .sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
66
+ }
67
+ export const latestSession = (root, engine) => listSessions(root).find((s) => !engine || s.engine === engine) ?? null;
68
+ /** Mantém as `MAX_SESSIONS` mais recentes, pela data do arquivo (sem abrir históricos que podem ter megabytes). */
69
+ function prune(root) {
70
+ const dir = sessionsDir(root);
71
+ const files = fs.readdirSync(dir).filter((n) => n.endsWith(".json")).map((n) => {
72
+ try {
73
+ return { n, t: fs.statSync(path.join(dir, n)).mtimeMs };
74
+ }
75
+ catch {
76
+ return { n, t: 0 };
77
+ }
78
+ }).sort((a, b) => b.t - a.t);
79
+ for (const f of files.slice(MAX_SESSIONS)) {
80
+ try {
81
+ fs.unlinkSync(path.join(dir, f.n));
82
+ }
83
+ catch { /* já removida */ }
84
+ }
85
+ }
86
+ /**
87
+ * Histórico salvo pronto para reenviar: se o processo caiu no meio de um turno, o último `tool_use` pode ter ficado sem
88
+ * `tool_result`, e a API recusaria a conversa. Fecha cada um com erro, como no Ctrl+C.
89
+ */
90
+ export function repairHistory(msgs) {
91
+ const last = msgs[msgs.length - 1];
92
+ if (last?.role !== "assistant" || typeof last.content === "string")
93
+ return msgs;
94
+ const pending = last.content.filter((b) => b.type === "tool_use");
95
+ if (!pending.length)
96
+ return msgs;
97
+ return [...msgs, { role: "user", content: pending.map((u) => ({ type: "tool_result", tool_use_id: u.id, content: "Sessão interrompida antes desta tool terminar.", is_error: true })) }];
98
+ }