brito-cli 0.4.0 → 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 +32 -3
- package/README.pt-BR.md +32 -3
- package/dist/agent/approval.js +48 -0
- package/dist/agent/claude-cli.js +25 -8
- package/dist/agent/context.js +4 -2
- package/dist/agent/loop.js +23 -9
- package/dist/agent/sink.js +1 -0
- package/dist/cli.js +16 -47
- package/dist/desktop/serve.js +209 -0
- package/dist/graph/indexer.js +1 -1
- package/dist/setup.js +110 -0
- package/dist/tools/fs.js +1 -1
- package/dist/ui/dock.js +10 -2
- package/dist/util/images.js +75 -0
- package/package.json +13 -4
package/README.md
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
**English** · [Português](README.pt-BR.md)
|
|
4
4
|
|
|
5
|
-
> A coding
|
|
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
|
|
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
|

|
|
10
10
|
|
|
@@ -18,6 +18,8 @@ Brito is a terminal agent for software development. Instead of grepping and read
|
|
|
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
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
|
|
|
@@ -57,6 +59,8 @@ The lint **rejects** secrets/tokens, absolute user paths, credentials in URLs (`
|
|
|
57
59
|
|
|
58
60
|
## Install
|
|
59
61
|
|
|
62
|
+
### Terminal (`brito-cli`)
|
|
63
|
+
|
|
60
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`).
|
|
61
65
|
|
|
62
66
|
```bash
|
|
@@ -81,6 +85,28 @@ export ANTHROPIC_API_KEY=sk-ant-... # Anthropic API
|
|
|
81
85
|
# or: install Claude Code, run `claude` and /login — Brito will use your subscription
|
|
82
86
|
```
|
|
83
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
|
+

|
|
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
|
+
|
|
84
110
|
## Quick start
|
|
85
111
|
|
|
86
112
|
```bash
|
|
@@ -144,7 +170,8 @@ Project data lives in `.brito/` (graph, project skills, agents) and global data
|
|
|
144
170
|
|
|
145
171
|
## Limitations
|
|
146
172
|
|
|
147
|
-
- Early software (v0.
|
|
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.
|
|
148
175
|
- **The interface and the agent's default language are Brazilian Portuguese.** Translating them is a welcome contribution.
|
|
149
176
|
- Skill learning is a heuristic: it proposes, you decide. Expect some proposals to be rejected.
|
|
150
177
|
|
|
@@ -166,6 +193,8 @@ npm test # 190+ tests, including a real MCP server and a real REPL
|
|
|
166
193
|
| `src/tools/` | graph, memory, skills, filesystem and shell tools |
|
|
167
194
|
| `src/mcp/` | MCP server |
|
|
168
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 |
|
|
169
198
|
|
|
170
199
|
## License
|
|
171
200
|
|
package/README.pt-BR.md
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
[English](README.md) · **Português**
|
|
4
4
|
|
|
5
|
-
>
|
|
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
|
|
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
|

|
|
10
10
|
|
|
@@ -18,6 +18,8 @@ O Brito é um agente de terminal para desenvolvimento de software. Em vez de dar
|
|
|
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
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
|
|
|
@@ -53,6 +55,8 @@ O lint **rejeita** segredos/tokens, caminhos absolutos do usuário, credenciais
|
|
|
53
55
|
|
|
54
56
|
## Instalação
|
|
55
57
|
|
|
58
|
+
### Terminal (`brito-cli`)
|
|
59
|
+
|
|
56
60
|
Requer Node >= 20. Desenvolvido no macOS; no Linux deve funcionar, no Windows não foi testado.
|
|
57
61
|
|
|
58
62
|
```bash
|
|
@@ -77,6 +81,28 @@ export ANTHROPIC_API_KEY=sk-ant-... # API da Anthropic
|
|
|
77
81
|
# ou: instale o Claude Code, rode `claude` e /login — o Brito usa a sua assinatura
|
|
78
82
|
```
|
|
79
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
|
+

|
|
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
|
+
|
|
80
106
|
## Começando
|
|
81
107
|
|
|
82
108
|
```bash
|
|
@@ -140,7 +166,8 @@ Os dados do projeto ficam em `.brito/` (grafo, skills do projeto, agentes) e os
|
|
|
140
166
|
|
|
141
167
|
## Limitações
|
|
142
168
|
|
|
143
|
-
- Software em fase inicial (v0.
|
|
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.
|
|
144
171
|
- **A interface e o idioma padrão do agente são português do Brasil.** Traduzi-los é uma contribuição bem-vinda.
|
|
145
172
|
- O aprendizado de skills é heurístico: ele propõe, você decide. Espere rejeitar algumas propostas.
|
|
146
173
|
|
|
@@ -162,6 +189,8 @@ npm test # 190+ testes, incluindo um servidor MCP real e um REPL rea
|
|
|
162
189
|
| `src/tools/` | tools de grafo, memória, skills, arquivos e shell |
|
|
163
190
|
| `src/mcp/` | servidor MCP |
|
|
164
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 |
|
|
165
194
|
|
|
166
195
|
## Licença
|
|
167
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
|
+
}
|
package/dist/agent/claude-cli.js
CHANGED
|
@@ -1,19 +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";
|
|
10
11
|
import { claudeCommand } from "../util/platform.js";
|
|
12
|
+
import { fmtBytes, findImages, splitOversize } from "../util/images.js";
|
|
11
13
|
// Backend alternativo: delega ao `claude` oficial, que autentica com o /login do usuário.
|
|
12
14
|
// O brito nunca lê nem envia o token da assinatura. Grafo, memória e skills chegam ao claude via MCP.
|
|
13
15
|
export function hasClaudeCli() {
|
|
14
16
|
const c = claudeCommand(["--version"]);
|
|
15
17
|
return spawnSync(c.command, c.args, { stdio: "ignore" }).status === 0;
|
|
16
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("") : "");
|
|
17
21
|
/** Nome das tools do Brito → nome que o claude enxerga. */
|
|
18
22
|
const CLAUDE_TOOL = { read_file: "Read", glob: "Glob", grep: "Grep", write_file: "Write", edit_file: "Edit", bash: "Bash" };
|
|
19
23
|
const claudeToolName = (n) => CLAUDE_TOOL[n] ?? MCP_PREFIX + n;
|
|
@@ -66,9 +70,9 @@ export function recordStreamEvent(turn, ev, loadedSkills) {
|
|
|
66
70
|
* Config do servidor MCP entregue ao claude. O servidor roda em outro processo: o env leva o socket de aprovação
|
|
67
71
|
* e o modo de permissão do REPL (vale por turno, pois cada turno sobe um claude novo com esta config).
|
|
68
72
|
*/
|
|
69
|
-
export function mcpConfig(cmd, approvalSock, mode) {
|
|
73
|
+
export function mcpConfig(cmd, approvalSock, mode, extraEnv = {}) {
|
|
70
74
|
// alwaysLoad: sem isso o claude adia as tools do Brito atrás do ToolSearch e o modelo raramente chega ao grafo.
|
|
71
|
-
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 } } } };
|
|
72
76
|
}
|
|
73
77
|
export class ClaudeSession {
|
|
74
78
|
o;
|
|
@@ -95,14 +99,17 @@ export class ClaudeSession {
|
|
|
95
99
|
constructor(o) {
|
|
96
100
|
this.o = o;
|
|
97
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; }
|
|
98
105
|
reset() { this.id = randomUUID(); this.started = false; this.contextTokens = 0; this.recentTurns = []; }
|
|
99
106
|
abort() { if (!this.child)
|
|
100
107
|
return; this.aborted = true; this.child.kill("SIGINT"); }
|
|
101
108
|
/** No Windows `kill("SIGINT")` termina o processo sem sinal nem código 130: lembramos que fomos nós que interrompemos. */
|
|
102
109
|
aborted = false;
|
|
103
|
-
args(text) {
|
|
110
|
+
args(text, imageDirs = []) {
|
|
104
111
|
const { root, approvalSock, stats, mcpCommand } = this.o;
|
|
105
|
-
const mcp = mcpConfig(mcpCommand, approvalSock, perms.mode);
|
|
112
|
+
const mcp = mcpConfig(mcpCommand, approvalSock, perms.mode, this.o.env);
|
|
106
113
|
const readOnlyMcp = ["graph_search", "graph_callers", "graph_callees", "graph_impact", "graph_context", "graph_query", "graph_index", "remember", "recall", "skill_load"];
|
|
107
114
|
const system = buildSystemPrompt(root, stats, skillsIndex(root)) +
|
|
108
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. ` +
|
|
@@ -111,6 +118,8 @@ export class ClaudeSession {
|
|
|
111
118
|
"-p", text,
|
|
112
119
|
this.started ? "--resume" : "--session-id", this.id,
|
|
113
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]),
|
|
114
123
|
"--append-system-prompt", system,
|
|
115
124
|
"--mcp-config", JSON.stringify(mcp),
|
|
116
125
|
"--agents", JSON.stringify(claudeAgents()),
|
|
@@ -130,13 +139,21 @@ export class ClaudeSession {
|
|
|
130
139
|
}
|
|
131
140
|
/** Roda um turno renderizando o stream-json com a UI do Brito. */
|
|
132
141
|
send(text) {
|
|
142
|
+
const ui = this.o.sink ?? terminalUi;
|
|
133
143
|
return new Promise((resolve) => {
|
|
134
144
|
ui.turnStart();
|
|
135
|
-
const
|
|
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)))]));
|
|
136
153
|
const child = (this.child = spawn(cmd.command, cmd.args, {
|
|
137
154
|
cwd: this.o.root, stdio: ["ignore", "pipe", "pipe"],
|
|
138
155
|
// `/reasoning none`: o claude desliga o raciocínio com MAX_THINKING_TOKENS=0 (não há `--effort none`)
|
|
139
|
-
env: wireEffort(this.effort).thinking ?
|
|
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" }) },
|
|
140
157
|
}));
|
|
141
158
|
let buf = "", stderr = "", ok = true, interrupted = false;
|
|
142
159
|
const names = new Map();
|
|
@@ -169,7 +186,7 @@ export class ClaudeSession {
|
|
|
169
186
|
else if (ev.type === "user") {
|
|
170
187
|
for (const b of ev.message?.content ?? [])
|
|
171
188
|
if (b.type === "tool_result")
|
|
172
|
-
ui.toolEnd(b.tool_use_id, !!b.is_error);
|
|
189
|
+
ui.toolEnd(b.tool_use_id, !!b.is_error, resultText(b.content));
|
|
173
190
|
}
|
|
174
191
|
else if (ev.type === "result") {
|
|
175
192
|
this.usage.input += (ev.usage?.input_tokens ?? 0) + (ev.usage?.cache_read_input_tokens ?? 0) + (ev.usage?.cache_creation_input_tokens ?? 0);
|
package/dist/agent/context.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
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];
|
package/dist/agent/loop.js
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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(
|
|
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() {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/cli.js
CHANGED
|
@@ -7,16 +7,15 @@ import "./tools/bash.js";
|
|
|
7
7
|
import "./tools/graph.js";
|
|
8
8
|
import "./tools/delegate.js";
|
|
9
9
|
import "./tools/plan.js";
|
|
10
|
-
import net from "node:net";
|
|
11
|
-
import os from "node:os";
|
|
12
10
|
import fs from "node:fs";
|
|
13
|
-
import { PLAN_NOTE, perms
|
|
11
|
+
import { PLAN_NOTE, perms } from "./permissions.js";
|
|
14
12
|
import { loadProjectAgents, listAgents } from "./agent/agents.js";
|
|
15
13
|
import { GraphLockedError, GraphStore } from "./graph/store.js";
|
|
16
14
|
import { indexProject } from "./graph/indexer.js";
|
|
17
15
|
import { Agent, MODEL } from "./agent/loop.js";
|
|
18
16
|
import { EFFORTS, MODEL_ALIASES, MODEL_HELP, effortLabel, initialEffort, isEffort, loadReasoningDisplay, parseModelArgs, saveEffort, saveModel, saveReasoningDisplay } from "./model.js";
|
|
19
17
|
import { hasClaudeCli, ClaudeSession } from "./agent/claude-cli.js";
|
|
18
|
+
import { startApprovalServer } from "./agent/approval.js";
|
|
20
19
|
import { serveMcp } from "./mcp/server.js";
|
|
21
20
|
import "./tools/skills.js";
|
|
22
21
|
import { approvePending, lintSkill, listPending, listSkills, rejectPending, skillsIndex, staleRefs } from "./skills/store.js";
|
|
@@ -44,6 +43,12 @@ program
|
|
|
44
43
|
console.log(`OK: ${s.files} arquivos, ${s.symbols} símbolos, ${s.calls} chamadas, ${s.imports} imports | +${s.added} ~${s.changed} -${s.removed} =${s.unchanged} (${Date.now() - t}ms)`);
|
|
45
44
|
await g.close();
|
|
46
45
|
});
|
|
46
|
+
program
|
|
47
|
+
.command("gui-serve", { hidden: true })
|
|
48
|
+
.description("Backend do app desktop (JSON por stdio)")
|
|
49
|
+
.option("-d, --dir <dir>")
|
|
50
|
+
.option("--resume <id>", "retoma uma conversa do claude")
|
|
51
|
+
.action(async (o) => { const { serveDesktop } = await import("./desktop/serve.js"); await serveDesktop(rootOf(o), o.resume); });
|
|
47
52
|
program
|
|
48
53
|
.command("query <cypher>")
|
|
49
54
|
.description("Executa Cypher no grafo")
|
|
@@ -75,49 +80,6 @@ async function plainAsk(q) {
|
|
|
75
80
|
return /^(s|y)/i.test(a.trim());
|
|
76
81
|
}
|
|
77
82
|
/** Sem API key: o `claude` oficial (login da assinatura) faz o modelo; grafo/memória/skills chegam via MCP. */
|
|
78
|
-
/**
|
|
79
|
-
* Aprovações do claude CLI: o claude chama a tool MCP `approve` (--permission-prompt-tool), que roda no
|
|
80
|
-
* processo do servidor MCP e consulta ESTE processo por um socket unix, onde está o terminal.
|
|
81
|
-
*/
|
|
82
|
-
function startApprovalServer(io) {
|
|
83
|
-
// No Windows um socket unix em %TEMP% não é aceito por net.listen: o endereço tem que ser um named pipe.
|
|
84
|
-
const sock = process.platform === "win32" ? `\\\\.\\pipe\\brito-approve-${process.pid}` : path.join(os.tmpdir(), `brito-approve-${process.pid}.sock`);
|
|
85
|
-
try {
|
|
86
|
-
fs.unlinkSync(sock);
|
|
87
|
-
}
|
|
88
|
-
catch { /* não existia */ }
|
|
89
|
-
const server = net.createServer((conn) => {
|
|
90
|
-
let buf = "";
|
|
91
|
-
conn.on("data", async (d) => {
|
|
92
|
-
buf += d.toString();
|
|
93
|
-
const nl = buf.indexOf("\n");
|
|
94
|
-
if (nl < 0)
|
|
95
|
-
return;
|
|
96
|
-
let reply;
|
|
97
|
-
try {
|
|
98
|
-
const req = JSON.parse(buf.slice(0, nl));
|
|
99
|
-
const plan = req.tool_name === "ExitPlanMode";
|
|
100
|
-
const ok = await io.ask(describeClaudeTool(req.tool_name, req.input), { force: plan });
|
|
101
|
-
if (ok && plan)
|
|
102
|
-
perms.mode = "manual";
|
|
103
|
-
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." };
|
|
104
|
-
}
|
|
105
|
-
catch (e) {
|
|
106
|
-
reply = { allow: false, message: `erro no pedido de aprovação: ${e?.message ?? e}` };
|
|
107
|
-
}
|
|
108
|
-
conn.end(JSON.stringify(reply) + "\n");
|
|
109
|
-
});
|
|
110
|
-
conn.on("error", () => { });
|
|
111
|
-
});
|
|
112
|
-
server.listen(sock);
|
|
113
|
-
server.unref();
|
|
114
|
-
const close = () => { server.close(); try {
|
|
115
|
-
fs.unlinkSync(sock);
|
|
116
|
-
}
|
|
117
|
-
catch { /* já removido */ } };
|
|
118
|
-
process.on("exit", close);
|
|
119
|
-
return { path: sock, close };
|
|
120
|
-
}
|
|
121
83
|
/** Menus ↑↓+Enter do `/model`: modelo e depois escopo. null = cancelado. */
|
|
122
84
|
async function pickModel(current) {
|
|
123
85
|
const models = Object.entries(MODEL_ALIASES);
|
|
@@ -487,6 +449,12 @@ program
|
|
|
487
449
|
.option("-d, --dir <dir>", "raiz do projeto")
|
|
488
450
|
.option("-y, --yes", "aprovar edições e comandos automaticamente")
|
|
489
451
|
.action(async (promptParts, o) => {
|
|
452
|
+
// Trava anti-recursão: um brito iniciado por dentro de um claude que o próprio brito lançou (BRITO_DEPTH) pode abrir
|
|
453
|
+
// outro claude, e assim por diante. Dois níveis bastam; mais que isso é laço de processos.
|
|
454
|
+
if (Number(process.env.BRITO_DEPTH ?? 0) >= 2) {
|
|
455
|
+
console.error("brito aninhado demais (BRITO_DEPTH): abortando para evitar laço de processos.");
|
|
456
|
+
process.exit(1);
|
|
457
|
+
}
|
|
490
458
|
if (!process.env.ANTHROPIC_API_KEY) {
|
|
491
459
|
if (!hasClaudeCli()) {
|
|
492
460
|
console.error(c.err("Defina ANTHROPIC_API_KEY ou instale o Claude Code e rode `claude` → /login."));
|
|
@@ -573,7 +541,8 @@ program
|
|
|
573
541
|
},
|
|
574
542
|
});
|
|
575
543
|
});
|
|
576
|
-
|
|
544
|
+
// from: "node" explícito: dentro do Electron (ELECTRON_RUN_AS_NODE) o commander trataria o caminho do script como argumento.
|
|
545
|
+
program.parseAsync(process.argv, { from: "node" }).catch((e) => {
|
|
577
546
|
if (e instanceof GraphLockedError) {
|
|
578
547
|
console.error(c.err("✗ ") + e.message);
|
|
579
548
|
process.exit(1);
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import readline from "node:readline";
|
|
5
|
+
import { randomUUID } from "node:crypto";
|
|
6
|
+
import "../tools/fs.js";
|
|
7
|
+
import "../tools/bash.js";
|
|
8
|
+
import "../tools/graph.js";
|
|
9
|
+
import "../tools/delegate.js";
|
|
10
|
+
import "../tools/plan.js";
|
|
11
|
+
import "../tools/skills.js";
|
|
12
|
+
import { Agent } from "../agent/loop.js";
|
|
13
|
+
import { ClaudeSession, hasClaudeCli } from "../agent/claude-cli.js";
|
|
14
|
+
import { startApprovalServer } from "../agent/approval.js";
|
|
15
|
+
import { loadProjectAgents } from "../agent/agents.js";
|
|
16
|
+
import { buildSystemPrompt, graphStatsLine } from "../agent/prompt.js";
|
|
17
|
+
import { GraphStore } from "../graph/store.js";
|
|
18
|
+
import { indexProject } from "../graph/indexer.js";
|
|
19
|
+
import { DEFAULT_MODEL, initialEffort, initialModel, isEffort, MODEL_ALIASES } from "../model.js";
|
|
20
|
+
import { PLAN_NOTE, perms } from "../permissions.js";
|
|
21
|
+
import { skillsIndex } from "../skills/store.js";
|
|
22
|
+
/**
|
|
23
|
+
* Backend do app desktop, controlado por JSON em linhas.
|
|
24
|
+
* stdin → {t:"send",text} {t:"abort"} {t:"approve",id,allow} {t:"config",model?,effort?,mode?} {t:"reset"} {t:"quit"}
|
|
25
|
+
* stdout → linhas "@@{json}" (qualquer outra linha, como logs perdidos, a janela ignora).
|
|
26
|
+
* Motor: com ANTHROPIC_API_KEY usa a API direta (como o `brito` no terminal); sem ela, o `claude` CLI (assinatura).
|
|
27
|
+
*/
|
|
28
|
+
const MARK = "@@";
|
|
29
|
+
const emit = (ev) => process.stdout.write(MARK + JSON.stringify(ev) + "\n");
|
|
30
|
+
const sink = {
|
|
31
|
+
turnStart: () => emit({ t: "turnStart" }),
|
|
32
|
+
turnEnd: (ok = true) => emit({ t: "turnEnd", ok }),
|
|
33
|
+
text: (text) => emit({ t: "text", text }),
|
|
34
|
+
textEnd: () => emit({ t: "textEnd" }),
|
|
35
|
+
thinking: (text) => emit({ t: "thinking", text }),
|
|
36
|
+
toolStart: (id, name, input) => emit({ t: "toolStart", id, name, input }),
|
|
37
|
+
toolEnd: (id, isError, output) => emit({ t: "toolEnd", id, isError, output: (output ?? "").slice(0, 4000) }),
|
|
38
|
+
info: (msg) => emit({ t: "info", msg }),
|
|
39
|
+
warn: (msg) => emit({ t: "warn", msg }),
|
|
40
|
+
error: (msg) => emit({ t: "error", msg }),
|
|
41
|
+
};
|
|
42
|
+
const CTX_WINDOW = Number(process.env.BRITO_CONTEXT_WINDOW ?? 200_000);
|
|
43
|
+
export async function serveDesktop(root, resumeId) {
|
|
44
|
+
const pending = new Map();
|
|
45
|
+
const ask = (text, opts) => perms.mode === "bypass" && !opts?.force ? Promise.resolve(true) : new Promise((resolve) => {
|
|
46
|
+
const id = randomUUID();
|
|
47
|
+
pending.set(id, resolve);
|
|
48
|
+
emit({ t: "approval", id, text, force: !!opts?.force });
|
|
49
|
+
});
|
|
50
|
+
// Na pasta pessoal (projeto "Home") indexar tudo seria enorme e inútil: sem índice ali.
|
|
51
|
+
const isHome = path.resolve(root) === path.resolve(os.homedir());
|
|
52
|
+
const useApi = !!process.env.ANTHROPIC_API_KEY;
|
|
53
|
+
if (!useApi && !hasClaudeCli()) {
|
|
54
|
+
emit({ t: "error", msg: "Sem acesso ao modelo: defina ANTHROPIC_API_KEY (no seu shell) ou instale o Claude Code e rode `claude` → /login." });
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
loadProjectAgents(root);
|
|
58
|
+
let engine;
|
|
59
|
+
let syms = 0, notes = 0;
|
|
60
|
+
let closeApproval = () => { };
|
|
61
|
+
if (useApi) {
|
|
62
|
+
// API direta: o grafo fica aberto neste processo (sem servidor MCP), como no `brito` do terminal.
|
|
63
|
+
// No Home o banco vai para ~/.brito/home-graph, para não criar .brito dentro da pasta pessoal.
|
|
64
|
+
const graphRoot = isHome ? path.join(os.homedir(), ".brito", "home-graph") : root;
|
|
65
|
+
fs.mkdirSync(graphRoot, { recursive: true });
|
|
66
|
+
const graph = await GraphStore.open(graphRoot);
|
|
67
|
+
const sync = async () => { if (!isHome)
|
|
68
|
+
syms = (await indexProject(root, graph)).symbols; };
|
|
69
|
+
emit({ t: "info", msg: "sincronizando grafo…" });
|
|
70
|
+
try {
|
|
71
|
+
await sync();
|
|
72
|
+
}
|
|
73
|
+
catch (e) {
|
|
74
|
+
emit({ t: "warn", msg: `grafo indisponível: ${e?.message ?? e}` });
|
|
75
|
+
}
|
|
76
|
+
try {
|
|
77
|
+
notes = Number((await graph.run("MATCH (s:Note) RETURN count(s) AS n"))[0].n);
|
|
78
|
+
}
|
|
79
|
+
catch { /* sem notas */ }
|
|
80
|
+
const ctx = { root, graph, ask, sync, dirty: false, loadedSkills: new Set() };
|
|
81
|
+
const agent = new Agent(() => buildSystemPrompt(root, graphStatsLine(syms, notes, "now"), skillsIndex(root)) + (perms.mode === "plan" ? PLAN_NOTE : ""), ctx, { model: initialModel(), sink });
|
|
82
|
+
let ac = null;
|
|
83
|
+
const id = randomUUID();
|
|
84
|
+
if (resumeId)
|
|
85
|
+
emit({ t: "info", msg: "No modo API o histórico da conversa não é retomado ao reabrir; a tela mostra o que já foi dito, mas o modelo recomeça." });
|
|
86
|
+
engine = {
|
|
87
|
+
kind: "api", sessionId: id, effort: "",
|
|
88
|
+
get model() { return agent.model; }, set model(m) { agent.model = m; },
|
|
89
|
+
setModel: (m) => { agent.model = m; },
|
|
90
|
+
async send(text) {
|
|
91
|
+
ac = new AbortController();
|
|
92
|
+
sink.turnStart();
|
|
93
|
+
let ok = true;
|
|
94
|
+
try {
|
|
95
|
+
await agent.send(text, ac.signal);
|
|
96
|
+
}
|
|
97
|
+
catch (e) {
|
|
98
|
+
ok = false;
|
|
99
|
+
ac.signal.aborted ? sink.warn("⏹ interrompido") : sink.error(e?.message ?? String(e));
|
|
100
|
+
}
|
|
101
|
+
finally {
|
|
102
|
+
sink.turnEnd(ok);
|
|
103
|
+
ac = null;
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
abort: () => ac?.abort(),
|
|
107
|
+
reset: () => { agent.messages = []; agent.contextTokens = 0; agent.recentTurns = []; agent.lastTurn = null; },
|
|
108
|
+
stats: () => ({ usage: agent.usage, contextTokens: agent.contextTokens, contextWindow: CTX_WINDOW, costUsd: 0 }),
|
|
109
|
+
close: async () => { await graph.close(); },
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
const approval = startApprovalServer({ ask });
|
|
114
|
+
closeApproval = approval.close;
|
|
115
|
+
// O kuzu aceita um único processo com escrita: indexa, lê as stats e solta o banco para o servidor MCP.
|
|
116
|
+
if (!isHome)
|
|
117
|
+
try {
|
|
118
|
+
const graph = await GraphStore.open(root);
|
|
119
|
+
emit({ t: "info", msg: "sincronizando grafo…" });
|
|
120
|
+
await indexProject(root, graph);
|
|
121
|
+
syms = Number((await graph.run("MATCH (s:Symbol) RETURN count(s) AS n"))[0].n);
|
|
122
|
+
notes = Number((await graph.run("MATCH (s:Note) RETURN count(s) AS n"))[0].n);
|
|
123
|
+
await graph.close();
|
|
124
|
+
}
|
|
125
|
+
catch (e) {
|
|
126
|
+
emit({ t: "warn", msg: `grafo indisponível: ${e?.message ?? e}` });
|
|
127
|
+
}
|
|
128
|
+
const session = new ClaudeSession({
|
|
129
|
+
root, approvalSock: approval.path, stats: graphStatsLine(syms, notes, "start"), sink,
|
|
130
|
+
mcpCommand: { command: process.execPath, args: [...process.execArgv, process.argv[1], "mcp-serve", "-d", root] },
|
|
131
|
+
// Dentro do Electron o executável é o próprio app: este env o faz agir como Node nos filhos (claude → mcp-serve).
|
|
132
|
+
env: process.versions.electron || process.env.ELECTRON_RUN_AS_NODE ? { ELECTRON_RUN_AS_NODE: "1" } : undefined,
|
|
133
|
+
});
|
|
134
|
+
if (resumeId)
|
|
135
|
+
session.resume(resumeId);
|
|
136
|
+
session.effort = initialEffort();
|
|
137
|
+
engine = {
|
|
138
|
+
kind: "claude",
|
|
139
|
+
get sessionId() { return session.sessionId; },
|
|
140
|
+
get model() { return session.model; },
|
|
141
|
+
get effort() { return session.effort; }, set effort(e) { session.effort = e; },
|
|
142
|
+
setModel: (m) => { session.requestedModel = m; session.model = m || "claude (assinatura)"; },
|
|
143
|
+
send: async (text) => { await session.send(text); },
|
|
144
|
+
abort: () => session.abort(),
|
|
145
|
+
reset: () => session.reset(),
|
|
146
|
+
stats: () => ({ usage: session.usage, contextTokens: session.contextTokens, contextWindow: session.contextWindow, costUsd: session.costUsd }),
|
|
147
|
+
close: async () => { },
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
let busy = false;
|
|
151
|
+
const ready = () => emit({ t: "ready", engine: engine.kind, sessionId: engine.sessionId, model: engine.model, effort: engine.effort, mode: perms.mode, symbols: syms, notes, root });
|
|
152
|
+
ready();
|
|
153
|
+
const rl = readline.createInterface({ input: process.stdin });
|
|
154
|
+
const quit = async () => { closeApproval(); try {
|
|
155
|
+
await engine.close();
|
|
156
|
+
}
|
|
157
|
+
catch { /* saindo */ } process.exit(0); };
|
|
158
|
+
rl.on("close", quit);
|
|
159
|
+
rl.on("line", async (line) => {
|
|
160
|
+
let m;
|
|
161
|
+
try {
|
|
162
|
+
m = JSON.parse(line);
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
switch (m.t) {
|
|
168
|
+
case "send": {
|
|
169
|
+
if (busy)
|
|
170
|
+
return emit({ t: "warn", msg: "já existe um turno em andamento" });
|
|
171
|
+
busy = true;
|
|
172
|
+
try {
|
|
173
|
+
await engine.send(String(m.text ?? ""));
|
|
174
|
+
}
|
|
175
|
+
finally {
|
|
176
|
+
busy = false;
|
|
177
|
+
emit({ t: "usage", ...engine.stats(), model: engine.model, sessionId: engine.sessionId });
|
|
178
|
+
}
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
case "abort":
|
|
182
|
+
engine.abort();
|
|
183
|
+
break;
|
|
184
|
+
case "approve":
|
|
185
|
+
pending.get(m.id)?.(!!m.allow);
|
|
186
|
+
pending.delete(m.id);
|
|
187
|
+
break;
|
|
188
|
+
case "config": {
|
|
189
|
+
if (typeof m.model === "string" && m.model)
|
|
190
|
+
engine.setModel(MODEL_ALIASES[m.model] ?? m.model);
|
|
191
|
+
else if (m.model === "")
|
|
192
|
+
engine.setModel(engine.kind === "api" ? DEFAULT_MODEL : "");
|
|
193
|
+
if (m.effort !== undefined)
|
|
194
|
+
engine.effort = isEffort(m.effort) ? m.effort : "";
|
|
195
|
+
if (["manual", "auto", "plan", "bypass"].includes(m.mode))
|
|
196
|
+
perms.mode = m.mode;
|
|
197
|
+
ready();
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
case "reset":
|
|
201
|
+
engine.reset();
|
|
202
|
+
ready();
|
|
203
|
+
break;
|
|
204
|
+
case "quit":
|
|
205
|
+
await quit();
|
|
206
|
+
break;
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
}
|
package/dist/graph/indexer.js
CHANGED
package/dist/setup.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import readline from "node:readline";
|
|
2
|
+
import { hasClaudeCli } from "./agent/claude-cli.js";
|
|
3
|
+
import { DEFAULT_MODEL, MODEL_ALIASES, saveEffort, saveModel, saveReasoningDisplay } from "./model.js";
|
|
4
|
+
import { c } from "./ui/theme.js";
|
|
5
|
+
const out = (s = "") => process.stdout.write(s + "\n");
|
|
6
|
+
/** Menu de setas (↑/↓, j/k, número, Enter). Ctrl+C cancela o assistente. */
|
|
7
|
+
async function select(title, options, current = 0) {
|
|
8
|
+
const stdin = process.stdin;
|
|
9
|
+
readline.emitKeypressEvents(stdin);
|
|
10
|
+
stdin.setRawMode(true);
|
|
11
|
+
stdin.resume();
|
|
12
|
+
let idx = current;
|
|
13
|
+
let drawn = 0;
|
|
14
|
+
const render = () => {
|
|
15
|
+
if (drawn)
|
|
16
|
+
process.stdout.write(`\x1b[${drawn}A\x1b[J`);
|
|
17
|
+
const lines = [c.bold(title)];
|
|
18
|
+
options.forEach((o, i) => {
|
|
19
|
+
const on = i === idx;
|
|
20
|
+
lines.push(` ${on ? c.accent("❯") : " "} ${on ? c.light(o.label) : o.label}${o.hint ? c.muted(" " + o.hint) : ""}`);
|
|
21
|
+
});
|
|
22
|
+
lines.push(c.muted(" ↑/↓ navegar · Enter confirmar"));
|
|
23
|
+
process.stdout.write(lines.join("\n") + "\n");
|
|
24
|
+
drawn = lines.length;
|
|
25
|
+
};
|
|
26
|
+
render();
|
|
27
|
+
return new Promise((resolve) => {
|
|
28
|
+
const onKey = (_, key) => {
|
|
29
|
+
if (key.ctrl && key.name === "c") {
|
|
30
|
+
stdin.setRawMode(false);
|
|
31
|
+
out();
|
|
32
|
+
process.exit(130);
|
|
33
|
+
}
|
|
34
|
+
if (key.name === "up" || key.name === "k")
|
|
35
|
+
idx = (idx + options.length - 1) % options.length;
|
|
36
|
+
else if (key.name === "down" || key.name === "j")
|
|
37
|
+
idx = (idx + 1) % options.length;
|
|
38
|
+
else if (key.sequence && /^[1-9]$/.test(key.sequence) && Number(key.sequence) <= options.length)
|
|
39
|
+
idx = Number(key.sequence) - 1;
|
|
40
|
+
else if (key.name === "return") {
|
|
41
|
+
stdin.off("keypress", onKey);
|
|
42
|
+
stdin.setRawMode(false);
|
|
43
|
+
stdin.pause();
|
|
44
|
+
process.stdout.write(`\x1b[${drawn}A\x1b[J`);
|
|
45
|
+
out(`${c.bold(title)} ${c.ok("✓")} ${c.light(options[idx].label)}`);
|
|
46
|
+
return resolve(options[idx].value);
|
|
47
|
+
}
|
|
48
|
+
render();
|
|
49
|
+
};
|
|
50
|
+
stdin.on("keypress", onKey);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
const step = (n, total, name) => out(`\n${c.accent(`◆ Passo ${n}/${total}`)} ${c.muted("·")} ${c.bold(name)}`);
|
|
54
|
+
export async function runSetup(opts = {}) {
|
|
55
|
+
const interactive = !!process.stdin.isTTY && !!process.stdout.isTTY && !opts.yes;
|
|
56
|
+
const total = 4;
|
|
57
|
+
const hasKey = !!process.env.ANTHROPIC_API_KEY;
|
|
58
|
+
const hasCli = hasClaudeCli();
|
|
59
|
+
out();
|
|
60
|
+
out(c.accent(" ╭─────────────────────────────────────╮"));
|
|
61
|
+
out(c.accent(" │") + c.bold(" Brito · assistente de configuração".padEnd(37)) + c.accent("│"));
|
|
62
|
+
out(c.accent(" ╰─────────────────────────────────────╯"));
|
|
63
|
+
out(c.muted(" Escolhas ficam em ~/.brito/config.json; rode `brito setup` de novo quando quiser mudar."));
|
|
64
|
+
// 1 · autenticação
|
|
65
|
+
step(1, total, "Autenticação");
|
|
66
|
+
out(` ${hasKey ? c.ok("✓") : c.muted("·")} ANTHROPIC_API_KEY ${hasKey ? "definida" : "não definida"}`);
|
|
67
|
+
out(` ${hasCli ? c.ok("✓") : c.muted("·")} Claude Code (claude CLI) ${hasCli ? "encontrado" : "não encontrado"}`);
|
|
68
|
+
if (!hasKey && !hasCli) {
|
|
69
|
+
out(c.warn(" Nenhum acesso detectado. Escolha um e depois rode `brito`:"));
|
|
70
|
+
out(c.muted(" • export ANTHROPIC_API_KEY=sk-ant-… (chave da API)"));
|
|
71
|
+
out(c.muted(" • npm i -g @anthropic-ai/claude-code && claude → /login (assinatura)"));
|
|
72
|
+
}
|
|
73
|
+
let model = DEFAULT_MODEL;
|
|
74
|
+
let effort;
|
|
75
|
+
let showReasoning = true;
|
|
76
|
+
if (interactive) {
|
|
77
|
+
// 2 · modelo
|
|
78
|
+
step(2, total, "Modelo padrão");
|
|
79
|
+
const names = Object.keys(MODEL_ALIASES);
|
|
80
|
+
const hints = { fable: "mais capaz", opus: "raciocínio profundo", sonnet: "equilíbrio (recomendado)", haiku: "rápido e barato" };
|
|
81
|
+
model = await select("Qual modelo usar por padrão?", names.map((n) => ({ label: n, hint: hints[n], value: MODEL_ALIASES[n] })), names.indexOf("sonnet"));
|
|
82
|
+
// 3 · esforço
|
|
83
|
+
step(3, total, "Esforço de raciocínio");
|
|
84
|
+
effort = await select("Quanto o modelo deve pensar?", [
|
|
85
|
+
{ label: "padrão", hint: "deixa o claude decidir", value: undefined },
|
|
86
|
+
{ label: "low", hint: "respostas rápidas", value: "low" },
|
|
87
|
+
{ label: "medium", value: "medium" },
|
|
88
|
+
{ label: "high", hint: "tarefas difíceis", value: "high" },
|
|
89
|
+
{ label: "max", hint: "mais lento e caro", value: "max" },
|
|
90
|
+
]);
|
|
91
|
+
// 4 · exibição
|
|
92
|
+
step(4, total, "Exibição");
|
|
93
|
+
showReasoning = await select("Mostrar o raciocínio do modelo?", [
|
|
94
|
+
{ label: "sim", hint: "recortado, como no Hermes", value: true },
|
|
95
|
+
{ label: "não", value: false },
|
|
96
|
+
]);
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
out(c.muted(`\n Modo não interativo: usando padrões (${DEFAULT_MODEL}).`));
|
|
100
|
+
}
|
|
101
|
+
saveModel(model);
|
|
102
|
+
saveEffort(effort);
|
|
103
|
+
saveReasoningDisplay({ show: showReasoning });
|
|
104
|
+
const row = (k, v) => out(` ${c.muted(k.padEnd(12))} ${v}`);
|
|
105
|
+
out(`\n${c.ok("✓")} ${c.bold("Tudo pronto")}`);
|
|
106
|
+
row("modelo", model);
|
|
107
|
+
row("esforço", effort ?? "padrão");
|
|
108
|
+
row("raciocínio", showReasoning ? "visível" : "oculto");
|
|
109
|
+
out(`\n Próximo passo: ${c.light("cd seu-projeto && brito")}\n`);
|
|
110
|
+
}
|
package/dist/tools/fs.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import fg from "
|
|
4
|
+
import { glob as fg } from "tinyglobby";
|
|
5
5
|
import { defineTool } from "./registry.js";
|
|
6
6
|
const abs = (root, p) => {
|
|
7
7
|
const full = path.resolve(root, p);
|
package/dist/ui/dock.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { droppedImages } from "../util/images.js";
|
|
1
2
|
import fs from "node:fs";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import stringWidth from "string-width";
|
|
@@ -7,7 +8,7 @@ const graphemes = (s) => Array.from(seg.segment(s), (x) => x.segment);
|
|
|
7
8
|
const ANSI = /\x1b\[[0-9;]*[A-Za-z]/y;
|
|
8
9
|
const MAX_INPUT_ROWS = 8;
|
|
9
10
|
const MAX_MENU = 7;
|
|
10
|
-
const PASTE_RE = /\[colado #(\d+): [^\]]*\]/g;
|
|
11
|
+
const PASTE_RE = /\[(?:colado|imagem) #(\d+): [^\]]*\]/g;
|
|
11
12
|
export class Dock {
|
|
12
13
|
opts;
|
|
13
14
|
onSubmit = () => { };
|
|
@@ -409,7 +410,14 @@ export class Dock {
|
|
|
409
410
|
finishPaste() {
|
|
410
411
|
const text = this.pasteBuf.replace(/\r\n?/g, "\n");
|
|
411
412
|
const lines = text.split("\n").length;
|
|
412
|
-
|
|
413
|
+
// Arrastar uma imagem para o terminal cola o caminho dela: vira um marcador curto que volta a ser o caminho no envio.
|
|
414
|
+
const imgs = droppedImages(text);
|
|
415
|
+
if (imgs.length) {
|
|
416
|
+
const n = ++this.pasteN;
|
|
417
|
+
this.pastes.set(n, imgs.map((i) => (/\s/.test(i.path) ? `"${i.path}"` : i.path)).join(" "));
|
|
418
|
+
this.insert(`[imagem #${n}: ${imgs.length === 1 ? imgs[0].name : `${imgs.length} imagens`}] `);
|
|
419
|
+
}
|
|
420
|
+
else if (lines >= 2 || text.length > 300) {
|
|
413
421
|
const n = ++this.pasteN;
|
|
414
422
|
this.pastes.set(n, text);
|
|
415
423
|
const size = text.length >= 1000 ? `${(text.length / 1000).toFixed(1)}K` : String(text.length);
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
const TYPES = { png: "image/png", jpg: "image/jpeg", jpeg: "image/jpeg", gif: "image/gif", webp: "image/webp" };
|
|
6
|
+
const IMG_EXT = /\.(png|jpe?g|gif|webp)$/i;
|
|
7
|
+
/** Limite da API por imagem (5 MB) e teto de anexos por mensagem. */
|
|
8
|
+
export const MAX_IMAGE_BYTES = 5 * 1024 * 1024;
|
|
9
|
+
export const MAX_IMAGES = 5;
|
|
10
|
+
/** Divide o texto em tokens, respeitando aspas e (fora do Windows) `\ ` como espaço do nome do arquivo. */
|
|
11
|
+
function tokens(text) {
|
|
12
|
+
const win = process.platform === "win32";
|
|
13
|
+
const re = win ? /'([^']*)'|"([^"]*)"|(\S+)/g : /'([^']*)'|"([^"]*)"|((?:\\.|\S)+)/g;
|
|
14
|
+
return [...text.matchAll(re)].map((m) => {
|
|
15
|
+
if (m[1] !== undefined)
|
|
16
|
+
return m[1];
|
|
17
|
+
if (m[2] !== undefined)
|
|
18
|
+
return m[2];
|
|
19
|
+
return win ? m[3] : m[3].replace(/\\(.)/g, "$1");
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
function toPath(token, root) {
|
|
23
|
+
let t = token.trim();
|
|
24
|
+
if (/^file:\/\//i.test(t)) {
|
|
25
|
+
try {
|
|
26
|
+
t = fileURLToPath(t);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return t;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (t === "~" || t.startsWith("~/"))
|
|
33
|
+
t = path.join(os.homedir(), t.slice(1));
|
|
34
|
+
return path.resolve(root, t);
|
|
35
|
+
}
|
|
36
|
+
/** Imagens existentes citadas no texto (caminhos soltos, entre aspas, escapados ou file://). Sem duplicatas; para em MAX_IMAGES. */
|
|
37
|
+
export function findImages(text, root = process.cwd()) {
|
|
38
|
+
const seen = new Set();
|
|
39
|
+
const out = [];
|
|
40
|
+
for (const tok of tokens(text)) {
|
|
41
|
+
if (!IMG_EXT.test(tok))
|
|
42
|
+
continue;
|
|
43
|
+
const p = toPath(tok, root);
|
|
44
|
+
if (seen.has(p))
|
|
45
|
+
continue;
|
|
46
|
+
try {
|
|
47
|
+
const st = fs.statSync(p);
|
|
48
|
+
if (!st.isFile())
|
|
49
|
+
continue;
|
|
50
|
+
seen.add(p);
|
|
51
|
+
out.push({ path: p, name: path.basename(p), mediaType: TYPES[path.extname(p).slice(1).toLowerCase()], bytes: st.size });
|
|
52
|
+
}
|
|
53
|
+
catch { /* não existe: era só texto parecido com caminho */ }
|
|
54
|
+
if (out.length >= MAX_IMAGES)
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
/** O texto inteiro é só caminhos de imagem (o que um arrastar-e-soltar produz)? Devolve as imagens, ou [] se tem mais coisa. */
|
|
60
|
+
export function droppedImages(text, root = process.cwd()) {
|
|
61
|
+
const toks = tokens(text);
|
|
62
|
+
if (!toks.length || !toks.every((t) => IMG_EXT.test(t)))
|
|
63
|
+
return [];
|
|
64
|
+
const imgs = findImages(text, root);
|
|
65
|
+
return imgs.length === toks.length ? imgs : [];
|
|
66
|
+
}
|
|
67
|
+
export const fmtBytes = (n) => (n >= 1_048_576 ? `${(n / 1_048_576).toFixed(1)} MB` : `${Math.max(1, Math.round(n / 1024))} kB`);
|
|
68
|
+
/** Separa o que dá para enviar do que passa do limite da API. */
|
|
69
|
+
export function splitOversize(imgs) {
|
|
70
|
+
return { ok: imgs.filter((i) => i.bytes <= MAX_IMAGE_BYTES), tooBig: imgs.filter((i) => i.bytes > MAX_IMAGE_BYTES) };
|
|
71
|
+
}
|
|
72
|
+
/** Bloco `image` da API da Anthropic, com o arquivo em base64. */
|
|
73
|
+
export function imageBlock(img) {
|
|
74
|
+
return { type: "image", source: { type: "base64", media_type: img.mediaType, data: fs.readFileSync(img.path).toString("base64") } };
|
|
75
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "brito-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Coding-agent CLI with a code knowledge graph, memory and reviewable skills / CLI de agente de código com grafo de conhecimento, memória e skills revisáveis",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -13,21 +13,26 @@
|
|
|
13
13
|
"test": "tsx --test test/*.test.ts",
|
|
14
14
|
"eval:learn": "tsx test/eval-learn.ts",
|
|
15
15
|
"prepare": "tsc",
|
|
16
|
-
"prepublishOnly": "npm run typecheck && npm test"
|
|
16
|
+
"prepublishOnly": "npm run typecheck && npm test",
|
|
17
|
+
"desktop": "npm run build && npm run bundle && npm --prefix desktop install --no-audit --no-fund && npm --prefix desktop start",
|
|
18
|
+
"bundle": "node build-bundle.mjs",
|
|
19
|
+
"desktop:app": "npm run build && npm run bundle && npm --prefix desktop install --no-audit --no-fund && node desktop/package.mjs --install",
|
|
20
|
+
"desktop:dmg": "npm run build && npm run bundle && npm --prefix desktop install --no-audit --no-fund && node desktop/package.mjs --dmg"
|
|
17
21
|
},
|
|
18
22
|
"dependencies": {
|
|
19
23
|
"@anthropic-ai/sdk": "^0.131.0",
|
|
20
24
|
"@modelcontextprotocol/sdk": "^1.32.0",
|
|
21
25
|
"commander": "latest",
|
|
22
|
-
"fast-glob": "latest",
|
|
23
26
|
"kuzu": "latest",
|
|
24
27
|
"string-width": "^8.3.0",
|
|
28
|
+
"tinyglobby": "^0.2.17",
|
|
25
29
|
"tree-sitter-wasms": "latest",
|
|
26
30
|
"web-tree-sitter": "0.24.7",
|
|
27
31
|
"zod": "latest"
|
|
28
32
|
},
|
|
29
33
|
"devDependencies": {
|
|
30
34
|
"@types/node": "latest",
|
|
35
|
+
"esbuild": "^0.28.2",
|
|
31
36
|
"tsx": "latest",
|
|
32
37
|
"typescript": "latest"
|
|
33
38
|
},
|
|
@@ -46,6 +51,7 @@
|
|
|
46
51
|
},
|
|
47
52
|
"files": [
|
|
48
53
|
"dist",
|
|
54
|
+
"!dist/cli.bundle.mjs",
|
|
49
55
|
".brito/agents",
|
|
50
56
|
"README.md",
|
|
51
57
|
"README.pt-BR.md",
|
|
@@ -64,5 +70,8 @@
|
|
|
64
70
|
"svelte",
|
|
65
71
|
"typescript",
|
|
66
72
|
"python"
|
|
67
|
-
]
|
|
73
|
+
],
|
|
74
|
+
"overrides": {
|
|
75
|
+
"tar": "^7.5.22"
|
|
76
|
+
}
|
|
68
77
|
}
|