brito-cli 0.3.2 → 0.5.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.
package/README.md CHANGED
@@ -2,9 +2,9 @@
2
2
 
3
3
  **English** · [Português](README.pt-BR.md)
4
4
 
5
- > A coding-agent CLI that knows your codebase as a **graph** and learns your project's procedures as **skills you review**.
5
+ > A coding agent for your terminal (`brito-cli`) and your desktop (a native app) that knows your codebase as a **graph** and learns your project's procedures as **skills you review**.
6
6
 
7
- Brito is a terminal agent for software development. Instead of grepping and reading files blindly, it keeps a **knowledge graph of your code** (symbols, who calls whom, who imports whom) and answers questions like *"what breaks if I change this?"* straight from the graph, in a fraction of a second. It also keeps **memory notes** and **skills**: reusable procedures learned from your own work, which only become active after **you** approve them.
7
+ Brito is an agent for software development. Use it in the terminal (`brito-cli` on npm) or as a desktop app (an executable for macOS, Windows and Linux): same agent, same graph, same skills. Instead of grepping and reading files blindly, it keeps a **knowledge graph of your code** (symbols, who calls whom, who imports whom) and answers questions like *"what breaks if I change this?"* straight from the graph, in a fraction of a second. It also keeps **memory notes** and **skills**: reusable procedures learned from your own work, which only become active after **you** approve them.
8
8
 
9
9
  ![Brito answering a question with the code graph](docs/images/answer-with-graph.png)
10
10
 
@@ -14,10 +14,12 @@ Brito is a terminal agent for software development. Instead of grepping and read
14
14
 
15
15
  - **Graph-first exploration.** `graph_search`, `graph_context`, `graph_callers`, `graph_callees` and `graph_impact` answer "where is it, who uses it, what breaks" without reading whole files. The graph is embedded ([Kùzu](https://kuzudb.com)), local and incremental.
16
16
  - **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.
17
- - **Skills that learn, with a human in the loop.** After a complex task Brito reflects on what happened and *proposes* skills. Nothing becomes active until you run `/skills approve`.
17
+ - **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.
18
18
  - **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).
19
- - **Permission modes.** manual, auto-edits, plan and "everything allowed" (`Shift+Tab` cycles them). Even in the last one, skills are queued for review instead of being activated.
19
+ - **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.
20
20
  - **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.
21
+ - **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).
22
+ - **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.
21
23
  - **Subagents.** `explore`, `plan` and `general` run in isolated contexts (read-only ones in parallel); add your own in `.brito/agents/*.md`.
22
24
  - **MCP server.** `brito mcp-serve` exposes the graph, memory and skills to other MCP clients such as Claude Code. The query tools are marked `alwaysLoad`, so they are not hidden behind tool search.
23
25
 
@@ -29,9 +31,13 @@ flowchart LR
29
31
  cg <-->|"graph_search · graph_callers<br/>graph_impact · graph_context"| agent{{"Brito agent"}}
30
32
  mem[("Memory<br/>decisions · conventions")] <-->|"recall / remember"| agent
31
33
  agent -->|"after a complex task"| learn["Reflection"]
32
- learn -->|"proposes"| pending["Pending skills"]
33
- pending -->|"/skills approve<br/>you decide"| skills[("Skills<br/>.brito/skills")]
34
+ learn -->|"creates"| trial["Skill in trial"]
35
+ trial -->|"3 clean uses"| skills[("Skills<br/>.brito/skills")]
36
+ trial -->|"2 stumbles or 21 idle days"| trash["Archived (.trash)"]
37
+ learn -->|"risky or changes yours"| pending["Pending<br/>/skills approve"]
38
+ pending --> skills
34
39
  skills -->|"skill_load"| agent
40
+ trial -->|"skill_load"| agent
35
41
  ```
36
42
 
37
43
  - **Code graph**: functions, classes, methods and components as nodes; `CALLS` and `IMPORTS` as edges. It re-parses only files whose hash changed and updates itself after edits.
@@ -39,11 +45,12 @@ flowchart LR
39
45
  - **Skills**: step-by-step procedures for *your* project (commands, order, traps, how to verify). The agent loads one with `skill_load` before a task it covers.
40
46
  - **Journal**: Brito remembers across sessions which tasks repeated, which is the best hint that a procedure is general and worth saving.
41
47
 
42
- ## Skills you review, not magic
48
+ ## Skills that earn their place
43
49
 
44
50
  ![Reviewing a pending skill, with a destructive-command warning](docs/images/skills-review.png)
45
51
 
46
- 1. A proposal appears in `/skills pending`, flagged when it contains destructive commands.
52
+ 0. **Autonomous path (default).** The reflection saves a new skill as *trial*, marked "(em teste)" in the index. Each turn that loads it counts as a win (no error-then-fix, no user correction) or a loss. 3 wins (and at most 1 loss per 3 wins) promote it; 2 losses without more wins, or 21 days unused, archive it to `.brito/skills/.trash`. Brito prints one line per promotion or archive. Limits: at most 3 new automatic skills a day, project scope only, and a weekly automatic consolidation that only touches automatic skills.
53
+ 1. **Review path.** Skills with destructive commands, and changes to skills you approved, appear in `/skills pending`, flagged when they contain destructive commands.
47
54
  2. `/skills approve <name>` shows the **whole body** and the lint warnings, then asks for confirmation. That confirmation is required even in "everything allowed" mode.
48
55
  3. Approved skills are versioned (`history/`), ranked by use, and flagged when the files they cite no longer exist. `/skills consolidate` merges duplicates and prunes unused ones.
49
56
  4. A skill can be project-scoped or **global** (valid in every project); `/skills promote` moves one to global.
@@ -52,7 +59,9 @@ The lint **rejects** secrets/tokens, absolute user paths, credentials in URLs (`
52
59
 
53
60
  ## Install
54
61
 
55
- Requires Node >= 20. Developed on macOS; Linux should work, Windows is untested.
62
+ ### Terminal (`brito-cli`)
63
+
64
+ Requires Node >= 20. Developed on macOS; Linux should work. On Windows use `npm i -g brito-cli` (the `install.sh` script needs bash); install Git for Windows so the `bash` tool runs bash commands, otherwise it falls back to `cmd.exe` (override with `BRITO_SHELL`).
56
65
 
57
66
  ```bash
58
67
  # from npm (the command is `brito`)
@@ -76,6 +85,28 @@ export ANTHROPIC_API_KEY=sk-ant-... # Anthropic API
76
85
  # or: install Claude Code, run `claude` and /login — Brito will use your subscription
77
86
  ```
78
87
 
88
+ ### Desktop app (executable)
89
+
90
+ 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.
91
+
92
+ ![Brito desktop app answering a question with the code graph](docs/images/desktop-app.png)
93
+
94
+ *The desktop app on a small project: one question, one graph call (the collapsed "ran 1 command" line), and the answer as a table. The sidebar groups sessions by project; the footer shows the backend, the folder, the context size and the cost.*
95
+
96
+ Download it from the [Releases page](https://github.com/wilsonbrito/brito/releases):
97
+
98
+ | System | File |
99
+ |---|---|
100
+ | macOS | `Brito-<version>-macos-arm64.dmg` (Apple Silicon) or `-x64.dmg` (Intel): open it and drag Brito to Applications |
101
+ | Windows | `Brito-<version>-windows-x64-setup.exe` |
102
+ | Linux | `Brito-<version>-linux-x64.tar.gz`: extract it and run `./install.sh` (adds Brito to the applications menu) |
103
+
104
+ The builds are not signed with an Apple Developer ID or a Windows certificate, so macOS ("unidentified developer": right-click, then Open) and Windows SmartScreen warn on the first launch. macOS is the platform exercised the most; the Windows and Linux builds come from CI and have had less testing.
105
+
106
+ Like the terminal version, it uses `ANTHROPIC_API_KEY` when your shell exports it, otherwise your Claude Code subscription (`claude`, then `/login`). Opened from the Finder, the app reads your login shell to find them.
107
+
108
+ To build it yourself: `npm run desktop` (development), `npm run desktop:app` (macOS: installs `/Applications/Brito.app`) or `npm run desktop:dmg`.
109
+
79
110
  ## Quick start
80
111
 
81
112
  ```bash
@@ -133,13 +164,14 @@ Brito has **no database drivers and connects to nothing on its own**. When it ne
133
164
 
134
165
  ## Configuration
135
166
 
136
- `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).
167
+ `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).
137
168
 
138
169
  Project data lives in `.brito/` (graph, project skills, agents) and global data in `~/.brito`. The graph, memory and skills stay on your machine; as with any coding agent, the code the model reads is sent to the model provider you configured.
139
170
 
140
171
  ## Limitations
141
172
 
142
- - Early software (v0.3.1), one maintainer.
173
+ - Early software (v0.5.0), one maintainer.
174
+ - 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.
143
175
  - **The interface and the agent's default language are Brazilian Portuguese.** Translating them is a welcome contribution.
144
176
  - Skill learning is a heuristic: it proposes, you decide. Expect some proposals to be rejected.
145
177
 
@@ -161,6 +193,8 @@ npm test # 190+ tests, including a real MCP server and a real REPL
161
193
  | `src/tools/` | graph, memory, skills, filesystem and shell tools |
162
194
  | `src/mcp/` | MCP server |
163
195
  | `src/repl.ts`, `src/ui/` | terminal interface |
196
+ | `src/desktop/` | `brito gui-serve`: the backend the desktop window talks to (JSON over stdio) |
197
+ | `desktop/` | Electron window and packaging (`package.mjs`); `npm run bundle` builds the single-file backend it uses |
164
198
 
165
199
  ## License
166
200
 
package/README.pt-BR.md CHANGED
@@ -2,9 +2,9 @@
2
2
 
3
3
  [English](README.md) · **Português**
4
4
 
5
- > Uma CLI de agente de código que conhece o seu projeto como um **grafo** e aprende os procedimentos dele como **skills que você revisa**.
5
+ > Um agente de código para o seu terminal (`brito-cli`) e para o seu desktop (um app nativo) que conhece o seu projeto como um **grafo** e aprende os procedimentos dele como **skills que você revisa**.
6
6
 
7
- O Brito é um agente de terminal para desenvolvimento de software. Em vez de dar `grep` e ler arquivos às cegas, ele mantém um **grafo de conhecimento do código** (símbolos, quem chama quem, quem importa quem) e responde perguntas como *"o que quebra se eu mudar isto?"* direto do grafo, em uma fração de segundo. Ele também guarda **notas de memória** e **skills**: procedimentos reutilizáveis aprendidos no seu próprio trabalho, que só passam a valer depois que **você** aprova.
7
+ O Brito é um agente para desenvolvimento de software. Use no terminal (`brito-cli` no npm) ou como app de desktop (um executável para macOS, Windows e Linux): o mesmo agente, o mesmo grafo, as mesmas skills. Em vez de dar `grep` e ler arquivos às cegas, ele mantém um **grafo de conhecimento do código** (símbolos, quem chama quem, quem importa quem) e responde perguntas como *"o que quebra se eu mudar isto?"* direto do grafo, em uma fração de segundo. Ele também guarda **notas de memória** e **skills**: procedimentos reutilizáveis aprendidos no seu próprio trabalho, que só passam a valer depois que **você** aprova.
8
8
 
9
9
  ![Brito respondendo uma pergunta com o grafo de código](docs/images/answer-with-graph.png)
10
10
 
@@ -14,10 +14,12 @@ O Brito é um agente de terminal para desenvolvimento de software. Em vez de dar
14
14
 
15
15
  - **Exploração guiada pelo grafo.** `graph_search`, `graph_context`, `graph_callers`, `graph_callees` e `graph_impact` respondem "onde está, quem usa, o que quebra" sem ler arquivos inteiros. O grafo é embutido ([Kùzu](https://kuzudb.com)), local e incremental.
16
16
  - **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.
17
- - **Skills que aprendem, com um humano no circuito.** Depois de uma tarefa complexa o Brito reflete sobre o que aconteceu e *propõe* skills. Nada fica ativo até você rodar `/skills approve`.
17
+ - **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.
18
18
  - **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).
19
- - **Modos de permissão.** manual, auto-edições, plano e "tudo liberado" (`Shift+Tab` alterna). Mesmo no último, as skills vão para a fila de revisão em vez de ficarem ativas.
19
+ - **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.
20
20
  - **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.
21
+ - **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).
22
+ - **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.
21
23
  - **Subagentes.** `explore`, `plan` e `general` rodam em contextos isolados (os somente-leitura, em paralelo); crie os seus em `.brito/agents/*.md`.
22
24
  - **Servidor MCP.** `brito mcp-serve` expõe grafo, memória e skills a outros clientes MCP, como o Claude Code. As tools de consulta são marcadas `alwaysLoad`, para não ficarem escondidas atrás da busca de tools.
23
25
 
@@ -39,11 +41,12 @@ flowchart LR
39
41
  - **Skills**: procedimentos passo a passo para o *seu* projeto (comandos, ordem, armadilhas, como verificar). O agente carrega uma com `skill_load` antes de uma tarefa que ela cobre.
40
42
  - **Journal**: o Brito lembra entre sessões quais tarefas se repetiram, o melhor indício de que um procedimento é geral e vale guardar.
41
43
 
42
- ## Skills que você revisa, sem mágica
44
+ ## Skills que merecem o lugar
43
45
 
44
46
  ![Revisando uma skill pendente, com aviso de comando destrutivo](docs/images/skills-review.png)
45
47
 
46
- 1. Uma proposta aparece em `/skills pending`, sinalizada quando contém comandos destrutivos.
48
+ 0. **Caminho autônomo (padrão).** A reflexão grava a skill nova como *em teste*, marcada "(em teste)" no índice. Cada turno que a carrega conta como vitória (sem erro→conserto nem correção do usuário) ou falha. 3 vitórias (e no máximo 1 falha a cada 3 vitórias) a promovem; 2 falhas sem mais vitórias, ou 21 dias sem uso, a arquivam em `.brito/skills/.trash`. O Brito imprime uma linha a cada promoção ou arquivamento. Limites: no máximo 3 skills automáticas novas por dia, só no escopo do projeto, e uma consolidação semanal automática que só mexe em skills automáticas.
49
+ 1. **Caminho de revisão.** Skills com comandos destrutivos, e mudanças em skills que você aprovou, aparecem em `/skills pending`, sinalizadas quando contêm comandos destrutivos.
47
50
  2. `/skills approve <nome>` mostra o **corpo inteiro** e os avisos do lint, e pede confirmação. Essa confirmação é exigida mesmo no modo "tudo liberado".
48
51
  3. Skills aprovadas são versionadas (`history/`), ranqueadas por uso e sinalizadas quando os arquivos que citam deixam de existir. `/skills consolidate` junta duplicadas e poda as sem uso.
49
52
  4. Uma skill pode ser do projeto ou **global** (vale em todos os projetos); `/skills promote` move uma para global.
@@ -52,6 +55,8 @@ O lint **rejeita** segredos/tokens, caminhos absolutos do usuário, credenciais
52
55
 
53
56
  ## Instalação
54
57
 
58
+ ### Terminal (`brito-cli`)
59
+
55
60
  Requer Node >= 20. Desenvolvido no macOS; no Linux deve funcionar, no Windows não foi testado.
56
61
 
57
62
  ```bash
@@ -76,6 +81,28 @@ export ANTHROPIC_API_KEY=sk-ant-... # API da Anthropic
76
81
  # ou: instale o Claude Code, rode `claude` e /login — o Brito usa a sua assinatura
77
82
  ```
78
83
 
84
+ ### App de desktop (executável)
85
+
86
+ 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.
87
+
88
+ ![App de desktop do Brito respondendo uma pergunta com o grafo de código](docs/images/desktop-app.png)
89
+
90
+ *O app de desktop num projeto pequeno: uma pergunta, uma chamada ao grafo (a linha recolhida "executou 1 comando") e a resposta em tabela. A barra lateral agrupa as sessões por projeto; o rodapé mostra o backend, a pasta, o tamanho do contexto e o custo.*
91
+
92
+ Baixe na [página de Releases](https://github.com/wilsonbrito/brito/releases):
93
+
94
+ | Sistema | Arquivo |
95
+ |---|---|
96
+ | macOS | `Brito-<versão>-macos-arm64.dmg` (Apple Silicon) ou `-x64.dmg` (Intel): abra e arraste o Brito para Aplicativos |
97
+ | Windows | `Brito-<versão>-windows-x64-setup.exe` |
98
+ | Linux | `Brito-<versão>-linux-x64.tar.gz`: extraia e rode `./install.sh` (coloca o Brito no menu de aplicativos) |
99
+
100
+ Os pacotes não são assinados com Apple Developer ID nem com certificado do Windows, então o macOS ("desenvolvedor não identificado": botão direito, depois Abrir) e o SmartScreen do Windows avisam na primeira abertura. O macOS é a plataforma mais testada; os pacotes de Windows e Linux saem do CI e foram menos exercitados.
101
+
102
+ Como na versão de terminal, ele usa a `ANTHROPIC_API_KEY` se o seu shell a exporta; senão, a sua assinatura do Claude Code (`claude`, depois `/login`). Aberto pelo Finder, o app lê o seu shell de login para encontrá-las.
103
+
104
+ Para compilar você mesmo: `npm run desktop` (desenvolvimento), `npm run desktop:app` (macOS: instala `/Applications/Brito.app`) ou `npm run desktop:dmg`.
105
+
79
106
  ## Começando
80
107
 
81
108
  ```bash
@@ -133,13 +160,14 @@ O Brito **não tem drivers de banco e não se conecta a nada sozinho**. Quando p
133
160
 
134
161
  ## Configuração
135
162
 
136
- `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).
163
+ `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).
137
164
 
138
165
  Os dados do projeto ficam em `.brito/` (grafo, skills do projeto, agentes) e os globais em `~/.brito`. O grafo, a memória e as skills ficam na sua máquina; como em qualquer agente de código, o código que o modelo lê é enviado ao provedor de modelo que você configurou.
139
166
 
140
167
  ## Limitações
141
168
 
142
- - Software em fase inicial (v0.3.1), um mantenedor.
169
+ - Software em fase inicial (v0.5.0), um mantenedor.
170
+ - 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.
143
171
  - **A interface e o idioma padrão do agente são português do Brasil.** Traduzi-los é uma contribuição bem-vinda.
144
172
  - O aprendizado de skills é heurístico: ele propõe, você decide. Espere rejeitar algumas propostas.
145
173
 
@@ -161,6 +189,8 @@ npm test # 190+ testes, incluindo um servidor MCP real e um REPL rea
161
189
  | `src/tools/` | tools de grafo, memória, skills, arquivos e shell |
162
190
  | `src/mcp/` | servidor MCP |
163
191
  | `src/repl.ts`, `src/ui/` | interface de terminal |
192
+ | `src/desktop/` | `brito gui-serve`: o backend com que a janela do desktop conversa (JSON por stdio) |
193
+ | `desktop/` | janela Electron e empacotamento (`package.mjs`); `npm run bundle` gera o backend em arquivo único que ela usa |
164
194
 
165
195
  ## Licença
166
196
 
@@ -0,0 +1,48 @@
1
+ import fs from "node:fs";
2
+ import net from "node:net";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { describeClaudeTool, perms } from "../permissions.js";
6
+ /**
7
+ * Aprovações do claude CLI: o claude chama a tool MCP `approve` (--permission-prompt-tool), que roda no
8
+ * processo do servidor MCP e consulta ESTE processo por um socket unix, onde está o terminal (ou a janela do app).
9
+ */
10
+ export function startApprovalServer(io) {
11
+ // No Windows um socket unix em %TEMP% não é aceito por net.listen: o endereço tem que ser um named pipe.
12
+ const sock = process.platform === "win32" ? `\\\\.\\pipe\\brito-approve-${process.pid}` : path.join(os.tmpdir(), `brito-approve-${process.pid}.sock`);
13
+ try {
14
+ fs.unlinkSync(sock);
15
+ }
16
+ catch { /* não existia */ }
17
+ const server = net.createServer((conn) => {
18
+ let buf = "";
19
+ conn.on("data", async (d) => {
20
+ buf += d.toString();
21
+ const nl = buf.indexOf("\n");
22
+ if (nl < 0)
23
+ return;
24
+ let reply;
25
+ try {
26
+ const req = JSON.parse(buf.slice(0, nl));
27
+ const plan = req.tool_name === "ExitPlanMode";
28
+ const ok = await io.ask(describeClaudeTool(req.tool_name, req.input), { force: plan });
29
+ if (ok && plan)
30
+ perms.mode = "manual";
31
+ 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." };
32
+ }
33
+ catch (e) {
34
+ reply = { allow: false, message: `erro no pedido de aprovação: ${e?.message ?? e}` };
35
+ }
36
+ conn.end(JSON.stringify(reply) + "\n");
37
+ });
38
+ conn.on("error", () => { });
39
+ });
40
+ server.listen(sock);
41
+ server.unref();
42
+ const close = () => { server.close(); try {
43
+ fs.unlinkSync(sock);
44
+ }
45
+ catch { /* já removido */ } };
46
+ process.on("exit", close);
47
+ return { path: sock, close };
48
+ }
@@ -1,17 +1,23 @@
1
+ import path from "node:path";
1
2
  import { spawn, spawnSync } from "node:child_process";
2
3
  import { randomUUID } from "node:crypto";
3
4
  import { listAgents } from "./agents.js";
4
5
  import { buildSystemPrompt } from "./prompt.js";
5
6
  import { skillsIndex } from "../skills/store.js";
6
7
  import { MCP_NAME, MCP_PREFIX, APPROVE_TOOL } from "../mcp/server.js";
7
- import { ui } from "../ui/ui.js";
8
+ import { ui as terminalUi } from "../ui/ui.js";
8
9
  import { MODE_ENV, perms } from "../permissions.js";
9
10
  import { wireEffort } from "../model.js";
11
+ import { claudeCommand } from "../util/platform.js";
12
+ import { fmtBytes, findImages, splitOversize } from "../util/images.js";
10
13
  // Backend alternativo: delega ao `claude` oficial, que autentica com o /login do usuário.
11
14
  // O brito nunca lê nem envia o token da assinatura. Grafo, memória e skills chegam ao claude via MCP.
12
15
  export function hasClaudeCli() {
13
- return spawnSync("claude", ["--version"], { stdio: "ignore" }).status === 0;
16
+ const c = claudeCommand(["--version"]);
17
+ return spawnSync(c.command, c.args, { stdio: "ignore" }).status === 0;
14
18
  }
19
+ /** Texto de um tool_result (string ou lista de blocos). */
20
+ const resultText = (c) => (typeof c === "string" ? c : Array.isArray(c) ? c.map((b) => (typeof b?.text === "string" ? b.text : "")).join("") : "");
15
21
  /** Nome das tools do Brito → nome que o claude enxerga. */
16
22
  const CLAUDE_TOOL = { read_file: "Read", glob: "Glob", grep: "Grep", write_file: "Write", edit_file: "Edit", bash: "Bash" };
17
23
  const claudeToolName = (n) => CLAUDE_TOOL[n] ?? MCP_PREFIX + n;
@@ -64,9 +70,9 @@ export function recordStreamEvent(turn, ev, loadedSkills) {
64
70
  * Config do servidor MCP entregue ao claude. O servidor roda em outro processo: o env leva o socket de aprovação
65
71
  * e o modo de permissão do REPL (vale por turno, pois cada turno sobe um claude novo com esta config).
66
72
  */
67
- export function mcpConfig(cmd, approvalSock, mode) {
73
+ export function mcpConfig(cmd, approvalSock, mode, extraEnv = {}) {
68
74
  // alwaysLoad: sem isso o claude adia as tools do Brito atrás do ToolSearch e o modelo raramente chega ao grafo.
69
- return { mcpServers: { [MCP_NAME]: { type: "stdio", command: cmd.command, args: cmd.args, alwaysLoad: true, env: { BRITO_APPROVE_SOCK: approvalSock, [MODE_ENV]: mode } } } };
75
+ return { mcpServers: { [MCP_NAME]: { type: "stdio", command: cmd.command, args: cmd.args, alwaysLoad: true, env: { ...extraEnv, BRITO_APPROVE_SOCK: approvalSock, [MODE_ENV]: mode } } } };
70
76
  }
71
77
  export class ClaudeSession {
72
78
  o;
@@ -93,11 +99,17 @@ export class ClaudeSession {
93
99
  constructor(o) {
94
100
  this.o = o;
95
101
  }
102
+ get sessionId() { return this.id; }
103
+ /** Retoma uma conversa do claude (--resume) em vez de começar outra. */
104
+ resume(id) { this.id = id; this.started = true; }
96
105
  reset() { this.id = randomUUID(); this.started = false; this.contextTokens = 0; this.recentTurns = []; }
97
- abort() { this.child?.kill("SIGINT"); }
98
- args(text) {
106
+ abort() { if (!this.child)
107
+ return; this.aborted = true; this.child.kill("SIGINT"); }
108
+ /** No Windows `kill("SIGINT")` termina o processo sem sinal nem código 130: lembramos que fomos nós que interrompemos. */
109
+ aborted = false;
110
+ args(text, imageDirs = []) {
99
111
  const { root, approvalSock, stats, mcpCommand } = this.o;
100
- const mcp = mcpConfig(mcpCommand, approvalSock, perms.mode);
112
+ const mcp = mcpConfig(mcpCommand, approvalSock, perms.mode, this.o.env);
101
113
  const readOnlyMcp = ["graph_search", "graph_callers", "graph_callees", "graph_impact", "graph_context", "graph_query", "graph_index", "remember", "recall", "skill_load"];
102
114
  const system = buildSystemPrompt(root, stats, skillsIndex(root)) +
103
115
  `\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. ` +
@@ -106,6 +118,8 @@ export class ClaudeSession {
106
118
  "-p", text,
107
119
  this.started ? "--resume" : "--session-id", this.id,
108
120
  "--output-format", "stream-json", "--verbose",
121
+ // Pastas das imagens anexadas: o Read do claude as lê sem pedir permissão (cada --add-dir é uma pasta).
122
+ ...imageDirs.flatMap((d) => ["--add-dir", d]),
109
123
  "--append-system-prompt", system,
110
124
  "--mcp-config", JSON.stringify(mcp),
111
125
  "--agents", JSON.stringify(claudeAgents()),
@@ -125,12 +139,21 @@ export class ClaudeSession {
125
139
  }
126
140
  /** Roda um turno renderizando o stream-json com a UI do Brito. */
127
141
  send(text) {
142
+ const ui = this.o.sink ?? terminalUi;
128
143
  return new Promise((resolve) => {
129
144
  ui.turnStart();
130
- const child = (this.child = spawn("claude", this.args(text), {
145
+ const { ok: imgs, tooBig } = splitOversize(findImages(text, this.o.root));
146
+ for (const i of tooBig)
147
+ ui.warn(`${i.name} passa de 5 MB e não foi anexada`);
148
+ if (imgs.length)
149
+ ui.info(`🖼 ${imgs.map((i) => `${i.name} (${fmtBytes(i.bytes)})`).join(", ")} anexada${imgs.length > 1 ? "s" : ""}`);
150
+ // O claude vê a imagem pela tool Read: o prompt diz quais arquivos são imagens anexadas pelo usuário.
151
+ const prompt = imgs.length ? `${text}\n\n[O usuário anexou ${imgs.length > 1 ? "estas imagens" : "esta imagem"}; abra com a tool Read para vê-${imgs.length > 1 ? "las" : "la"}:\n${imgs.map((i) => `- ${i.path}`).join("\n")}]` : text;
152
+ const cmd = claudeCommand(this.args(prompt, [...new Set(imgs.map((i) => path.dirname(i.path)))]));
153
+ const child = (this.child = spawn(cmd.command, cmd.args, {
131
154
  cwd: this.o.root, stdio: ["ignore", "pipe", "pipe"],
132
155
  // `/reasoning none`: o claude desliga o raciocínio com MAX_THINKING_TOKENS=0 (não há `--effort none`)
133
- env: wireEffort(this.effort).thinking ? process.env : { ...process.env, MAX_THINKING_TOKENS: "0" },
156
+ env: { ...process.env, ...this.o.env, BRITO_DEPTH: String(Number(process.env.BRITO_DEPTH ?? 0) + 1), ...(wireEffort(this.effort).thinking ? {} : { MAX_THINKING_TOKENS: "0" }) },
134
157
  }));
135
158
  let buf = "", stderr = "", ok = true, interrupted = false;
136
159
  const names = new Map();
@@ -163,7 +186,7 @@ export class ClaudeSession {
163
186
  else if (ev.type === "user") {
164
187
  for (const b of ev.message?.content ?? [])
165
188
  if (b.type === "tool_result")
166
- ui.toolEnd(b.tool_use_id, !!b.is_error);
189
+ ui.toolEnd(b.tool_use_id, !!b.is_error, resultText(b.content));
167
190
  }
168
191
  else if (ev.type === "result") {
169
192
  this.usage.input += (ev.usage?.input_tokens ?? 0) + (ev.usage?.cache_read_input_tokens ?? 0) + (ev.usage?.cache_creation_input_tokens ?? 0);
@@ -197,7 +220,8 @@ export class ClaudeSession {
197
220
  child.on("error", (e) => { ok = false; ui.error(`não consegui executar o claude: ${e.message}`); });
198
221
  child.on("close", (code, signal) => {
199
222
  this.child = null;
200
- interrupted = signal === "SIGINT" || code === 130;
223
+ interrupted = signal === "SIGINT" || code === 130 || this.aborted;
224
+ this.aborted = false;
201
225
  if (code !== 0 && !interrupted) {
202
226
  ok = false;
203
227
  if (stderr.trim())
@@ -1,4 +1,6 @@
1
- export const estimateTokens = (msgs) => Math.ceil(JSON.stringify(msgs).length / 4);
1
+ /** Imagem em base64 tem megabytes, mas custa ~1,6 mil tokens: troca o conteúdo por um marcador antes de medir ou resumir. */
2
+ const stubImages = (_k, v) => (v && typeof v === "object" && v.type === "image" ? { type: "image", note: "x".repeat(6400) } : v);
3
+ export const estimateTokens = (msgs) => Math.ceil(JSON.stringify(msgs, stubImages).length / 4);
2
4
  const MAX_OLD_RESULT = 1500;
3
5
  /** Fase 1 (barata): encurta tool_results antigos, preservando as últimas `keep` mensagens. */
4
6
  export function trimOldToolResults(msgs, keep = 8) {
@@ -33,7 +35,7 @@ export async function summarizeMiddle(client, model, msgs, keepTail = 6) {
33
35
  model,
34
36
  max_tokens: 2000,
35
37
  system: "Resuma a conversa de um agente de programação. Preserve: objetivo, decisões, arquivos/símbolos tocados, erros encontrados, estado atual e próximos passos. Seja denso.",
36
- messages: [{ role: "user", content: JSON.stringify(middle).slice(0, 150_000) }],
38
+ messages: [{ role: "user", content: JSON.stringify(middle, (_k, v) => (v && typeof v === "object" && v.type === "image" ? { type: "image", note: "[imagem omitida]" } : v)).slice(0, 150_000) }],
37
39
  });
38
40
  const summary = res.content.map((b) => (b.type === "text" ? b.text : "")).join("");
39
41
  return [...head, { role: "user", content: `[Resumo da conversa anterior]\n${summary}` }, { role: "assistant", content: "Entendido, continuando a partir do resumo." }, ...tail];
@@ -1,8 +1,9 @@
1
1
  import Anthropic from "@anthropic-ai/sdk";
2
2
  import { toApiTools, execTool, isConcurrent } from "../tools/registry.js";
3
3
  import { estimateTokens, summarizeMiddle, trimOldToolResults } from "./context.js";
4
- import { ui } from "../ui/ui.js";
4
+ import { ui as terminalUi } from "../ui/ui.js";
5
5
  import { initialModel } from "../model.js";
6
+ import { fmtBytes, findImages, imageBlock, splitOversize } from "../util/images.js";
6
7
  export const MODEL = initialModel();
7
8
  const COMPACT_AT = Number(process.env.BRITO_COMPACT_TOKENS ?? 120_000);
8
9
  export class Agent {
@@ -21,13 +22,15 @@ export class Agent {
21
22
  recentTurns = [];
22
23
  maxSteps;
23
24
  log;
25
+ ui;
24
26
  constructor(system, ctx, opts = {}) {
25
27
  this.system = system;
26
28
  this.ctx = ctx;
27
29
  this.opts = opts;
28
30
  this.model = opts.model ?? MODEL;
29
31
  this.maxSteps = opts.maxSteps ?? 40;
30
- this.log = opts.log ?? ((s) => ui.info(s));
32
+ this.ui = opts.sink ?? terminalUi;
33
+ this.log = opts.log ?? ((s) => this.ui.info(s));
31
34
  }
32
35
  /** Roda um turno completo (até não haver mais tool_use) e devolve o texto final. */
33
36
  async send(userText, signal) {
@@ -56,7 +59,18 @@ export class Agent {
56
59
  this.lastTurn = turn;
57
60
  this.recentTurns = [...this.recentTurns, turn].slice(-3);
58
61
  const push = (m) => { this.messages.push(m); turn.messages.push(m); };
59
- push({ role: "user", content: userText });
62
+ // Imagens arrastadas para o terminal chegam como caminhos no texto: anexa como blocos `image`. O registro do turno
63
+ // (usado na reflexão) guarda só o texto, para não carregar base64.
64
+ const { ok, tooBig } = splitOversize(findImages(userText, this.ctx.root));
65
+ for (const i of tooBig)
66
+ this.log(`⚠ ${i.name} passa de 5 MB e não foi anexada`);
67
+ if (ok.length) {
68
+ this.log(`🖼 ${ok.map((i) => `${i.name} (${fmtBytes(i.bytes)})`).join(", ")} anexada${ok.length > 1 ? "s" : ""}`);
69
+ this.messages.push({ role: "user", content: [...ok.map(imageBlock), { type: "text", text: userText }] });
70
+ turn.messages.push({ role: "user", content: userText });
71
+ }
72
+ else
73
+ push({ role: "user", content: userText });
60
74
  await this.compactIfNeeded();
61
75
  let finalText = "";
62
76
  for (let step = 0; step < this.maxSteps; step++) {
@@ -70,7 +84,7 @@ export class Agent {
70
84
  messages: this.messages,
71
85
  }, { signal });
72
86
  if (!this.opts.quiet)
73
- stream.on("text", (t) => ui.text(t));
87
+ stream.on("text", (t) => this.ui.text(t));
74
88
  const msg = await stream.finalMessage();
75
89
  this.contextTokens = msg.usage.input_tokens + (msg.usage.cache_read_input_tokens ?? 0) + (msg.usage.cache_creation_input_tokens ?? 0);
76
90
  this.usage.input += msg.usage.input_tokens + (msg.usage.cache_read_input_tokens ?? 0) + (msg.usage.cache_creation_input_tokens ?? 0);
@@ -82,7 +96,7 @@ export class Agent {
82
96
  if (text)
83
97
  finalText = text;
84
98
  if (!this.opts.quiet)
85
- ui.textEnd();
99
+ this.ui.textEnd();
86
100
  const uses = msg.content.filter((b) => b.type === "tool_use");
87
101
  if (msg.stop_reason !== "tool_use" || uses.length === 0) {
88
102
  turn.finished = true;
@@ -91,13 +105,13 @@ export class Agent {
91
105
  const runOne = async (u) => {
92
106
  const t0 = Date.now();
93
107
  if (!this.opts.quiet)
94
- ui.toolStart(u.id, u.name, u.input);
108
+ this.ui.toolStart(u.id, u.name, u.input);
95
109
  const denied = this.opts.tools && !this.opts.tools.has(u.name);
96
110
  const r = denied ? { text: `Tool não permitida para este agente: ${u.name}`, isError: true } : await execTool(u.name, u.input, this.ctx);
97
111
  if (this.opts.quiet)
98
- this.log(ui.toolLine(u.name, u.input, Date.now() - t0, r.isError));
112
+ this.log(terminalUi.toolLine(u.name, u.input, Date.now() - t0, r.isError));
99
113
  else
100
- ui.toolEnd(u.id, r.isError);
114
+ this.ui.toolEnd(u.id, r.isError, r.text);
101
115
  return { type: "tool_result", tool_use_id: u.id, content: r.text.slice(0, 40_000), is_error: r.isError };
102
116
  };
103
117
  // Leituras puras rodam em paralelo; qualquer escrita/aprovação força execução em série.
@@ -109,7 +123,7 @@ export class Agent {
109
123
  push({ role: "user", content: results });
110
124
  await this.compactIfNeeded();
111
125
  }
112
- ui.warn("Limite de passos atingido.");
126
+ this.ui.warn("Limite de passos atingido.");
113
127
  return finalText || "(limite de passos atingido sem resposta final)";
114
128
  }
115
129
  async compactIfNeeded() {
@@ -1,5 +1,6 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
+ import { shellNote } from "../util/platform.js";
3
4
  /**
4
5
  * Frase sobre o grafo para o system prompt. "0 símbolos" sem contexto faz o modelo concluir que o grafo é inútil, mas o
5
6
  * projeto pode só estar vazio no início (e o grafo se atualiza sozinho a cada edição). Por isso: rótulo do momento da
@@ -37,7 +38,7 @@ export function buildSystemPrompt(root, graphStats, skills = "") {
37
38
 
38
39
  # Ambiente
39
40
  Diretório do projeto: ${root}
40
- Data: ${new Date().toISOString().slice(0, 10)}${skills ? `\n\n# Skills disponíveis (use skill_load)\n${skills}` : ""}${projectDoc}`;
41
+ Data: ${new Date().toISOString().slice(0, 10)}${shellNote()}${skills ? `\n\n# Skills disponíveis (use skill_load)\n${skills}` : ""}${projectDoc}`;
41
42
  }
42
43
  import { skillsIndex } from "../skills/store.js";
43
44
  export function buildSubagentPrompt(root, def) {
@@ -0,0 +1 @@
1
+ export {};