brito-cli 0.3.2 → 0.4.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 +13 -8
- package/README.pt-BR.md +6 -5
- package/dist/agent/claude-cli.js +11 -4
- package/dist/agent/prompt.js +2 -1
- package/dist/cli.js +19 -5
- package/dist/repl.js +2 -1
- package/dist/skills/consolidate.js +29 -2
- package/dist/skills/learn-cli.js +3 -1
- package/dist/skills/learn.js +38 -2
- package/dist/skills/store.js +19 -7
- package/dist/skills/trial.js +71 -0
- package/dist/tools/bash.js +2 -1
- package/dist/tools/fs.js +4 -1
- package/dist/util/platform.js +57 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -14,9 +14,9 @@ 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
|
|
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).
|
|
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
21
|
- **Subagents.** `explore`, `plan` and `general` run in isolated contexts (read-only ones in parallel); add your own in `.brito/agents/*.md`.
|
|
22
22
|
- **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.
|
|
@@ -29,9 +29,13 @@ flowchart LR
|
|
|
29
29
|
cg <-->|"graph_search · graph_callers<br/>graph_impact · graph_context"| agent{{"Brito agent"}}
|
|
30
30
|
mem[("Memory<br/>decisions · conventions")] <-->|"recall / remember"| agent
|
|
31
31
|
agent -->|"after a complex task"| learn["Reflection"]
|
|
32
|
-
learn -->|"
|
|
33
|
-
|
|
32
|
+
learn -->|"creates"| trial["Skill in trial"]
|
|
33
|
+
trial -->|"3 clean uses"| skills[("Skills<br/>.brito/skills")]
|
|
34
|
+
trial -->|"2 stumbles or 21 idle days"| trash["Archived (.trash)"]
|
|
35
|
+
learn -->|"risky or changes yours"| pending["Pending<br/>/skills approve"]
|
|
36
|
+
pending --> skills
|
|
34
37
|
skills -->|"skill_load"| agent
|
|
38
|
+
trial -->|"skill_load"| agent
|
|
35
39
|
```
|
|
36
40
|
|
|
37
41
|
- **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 +43,12 @@ flowchart LR
|
|
|
39
43
|
- **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
44
|
- **Journal**: Brito remembers across sessions which tasks repeated, which is the best hint that a procedure is general and worth saving.
|
|
41
45
|
|
|
42
|
-
## Skills
|
|
46
|
+
## Skills that earn their place
|
|
43
47
|
|
|
44
48
|

|
|
45
49
|
|
|
46
|
-
|
|
50
|
+
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.
|
|
51
|
+
1. **Review path.** Skills with destructive commands, and changes to skills you approved, appear in `/skills pending`, flagged when they contain destructive commands.
|
|
47
52
|
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
53
|
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
54
|
4. A skill can be project-scoped or **global** (valid in every project); `/skills promote` moves one to global.
|
|
@@ -52,7 +57,7 @@ The lint **rejects** secrets/tokens, absolute user paths, credentials in URLs (`
|
|
|
52
57
|
|
|
53
58
|
## Install
|
|
54
59
|
|
|
55
|
-
Requires Node >= 20. Developed on macOS; Linux should work
|
|
60
|
+
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
61
|
|
|
57
62
|
```bash
|
|
58
63
|
# from npm (the command is `brito`)
|
|
@@ -133,7 +138,7 @@ Brito has **no database drivers and connects to nothing on its own**. When it ne
|
|
|
133
138
|
|
|
134
139
|
## Configuration
|
|
135
140
|
|
|
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).
|
|
141
|
+
`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
142
|
|
|
138
143
|
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
144
|
|
package/README.pt-BR.md
CHANGED
|
@@ -14,9 +14,9 @@ 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,
|
|
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).
|
|
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
21
|
- **Subagentes.** `explore`, `plan` e `general` rodam em contextos isolados (os somente-leitura, em paralelo); crie os seus em `.brito/agents/*.md`.
|
|
22
22
|
- **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.
|
|
@@ -39,11 +39,12 @@ flowchart LR
|
|
|
39
39
|
- **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
40
|
- **Journal**: o Brito lembra entre sessões quais tarefas se repetiram, o melhor indício de que um procedimento é geral e vale guardar.
|
|
41
41
|
|
|
42
|
-
## Skills que
|
|
42
|
+
## Skills que merecem o lugar
|
|
43
43
|
|
|
44
44
|

|
|
45
45
|
|
|
46
|
-
|
|
46
|
+
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.
|
|
47
|
+
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
48
|
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
49
|
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
50
|
4. Uma skill pode ser do projeto ou **global** (vale em todos os projetos); `/skills promote` move uma para global.
|
|
@@ -133,7 +134,7 @@ O Brito **não tem drivers de banco e não se conecta a nada sozinho**. Quando p
|
|
|
133
134
|
|
|
134
135
|
## Configuração
|
|
135
136
|
|
|
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).
|
|
137
|
+
`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
138
|
|
|
138
139
|
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
140
|
|
package/dist/agent/claude-cli.js
CHANGED
|
@@ -7,10 +7,12 @@ import { MCP_NAME, MCP_PREFIX, APPROVE_TOOL } from "../mcp/server.js";
|
|
|
7
7
|
import { ui } from "../ui/ui.js";
|
|
8
8
|
import { MODE_ENV, perms } from "../permissions.js";
|
|
9
9
|
import { wireEffort } from "../model.js";
|
|
10
|
+
import { claudeCommand } from "../util/platform.js";
|
|
10
11
|
// Backend alternativo: delega ao `claude` oficial, que autentica com o /login do usuário.
|
|
11
12
|
// O brito nunca lê nem envia o token da assinatura. Grafo, memória e skills chegam ao claude via MCP.
|
|
12
13
|
export function hasClaudeCli() {
|
|
13
|
-
|
|
14
|
+
const c = claudeCommand(["--version"]);
|
|
15
|
+
return spawnSync(c.command, c.args, { stdio: "ignore" }).status === 0;
|
|
14
16
|
}
|
|
15
17
|
/** Nome das tools do Brito → nome que o claude enxerga. */
|
|
16
18
|
const CLAUDE_TOOL = { read_file: "Read", glob: "Glob", grep: "Grep", write_file: "Write", edit_file: "Edit", bash: "Bash" };
|
|
@@ -94,7 +96,10 @@ export class ClaudeSession {
|
|
|
94
96
|
this.o = o;
|
|
95
97
|
}
|
|
96
98
|
reset() { this.id = randomUUID(); this.started = false; this.contextTokens = 0; this.recentTurns = []; }
|
|
97
|
-
abort() { this.child
|
|
99
|
+
abort() { if (!this.child)
|
|
100
|
+
return; this.aborted = true; this.child.kill("SIGINT"); }
|
|
101
|
+
/** No Windows `kill("SIGINT")` termina o processo sem sinal nem código 130: lembramos que fomos nós que interrompemos. */
|
|
102
|
+
aborted = false;
|
|
98
103
|
args(text) {
|
|
99
104
|
const { root, approvalSock, stats, mcpCommand } = this.o;
|
|
100
105
|
const mcp = mcpConfig(mcpCommand, approvalSock, perms.mode);
|
|
@@ -127,7 +132,8 @@ export class ClaudeSession {
|
|
|
127
132
|
send(text) {
|
|
128
133
|
return new Promise((resolve) => {
|
|
129
134
|
ui.turnStart();
|
|
130
|
-
const
|
|
135
|
+
const cmd = claudeCommand(this.args(text));
|
|
136
|
+
const child = (this.child = spawn(cmd.command, cmd.args, {
|
|
131
137
|
cwd: this.o.root, stdio: ["ignore", "pipe", "pipe"],
|
|
132
138
|
// `/reasoning none`: o claude desliga o raciocínio com MAX_THINKING_TOKENS=0 (não há `--effort none`)
|
|
133
139
|
env: wireEffort(this.effort).thinking ? process.env : { ...process.env, MAX_THINKING_TOKENS: "0" },
|
|
@@ -197,7 +203,8 @@ export class ClaudeSession {
|
|
|
197
203
|
child.on("error", (e) => { ok = false; ui.error(`não consegui executar o claude: ${e.message}`); });
|
|
198
204
|
child.on("close", (code, signal) => {
|
|
199
205
|
this.child = null;
|
|
200
|
-
interrupted = signal === "SIGINT" || code === 130;
|
|
206
|
+
interrupted = signal === "SIGINT" || code === 130 || this.aborted;
|
|
207
|
+
this.aborted = false;
|
|
201
208
|
if (code !== 0 && !interrupted) {
|
|
202
209
|
ok = false;
|
|
203
210
|
if (stderr.trim())
|
package/dist/agent/prompt.js
CHANGED
|
@@ -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) {
|
package/dist/cli.js
CHANGED
|
@@ -20,14 +20,16 @@ import { hasClaudeCli, ClaudeSession } from "./agent/claude-cli.js";
|
|
|
20
20
|
import { serveMcp } from "./mcp/server.js";
|
|
21
21
|
import "./tools/skills.js";
|
|
22
22
|
import { approvePending, lintSkill, listPending, listSkills, rejectPending, skillsIndex, staleRefs } from "./skills/store.js";
|
|
23
|
-
import { applyProposal, describeProposal, proposeConsolidation, pruneCandidates } from "./skills/consolidate.js";
|
|
24
|
-
import { defaultComplete, reflect, shouldReflect } from "./skills/learn.js";
|
|
23
|
+
import { applyProposal, autoConsolidate, describeProposal, proposeConsolidation, pruneCandidates } from "./skills/consolidate.js";
|
|
24
|
+
import { defaultComplete, recordOutcomes, reflect, shouldReflect } from "./skills/learn.js";
|
|
25
|
+
import { expireTrials, skillsMode } from "./skills/trial.js";
|
|
25
26
|
import { claudeCliComplete } from "./skills/learn-cli.js";
|
|
26
27
|
import { buildSystemPrompt, graphStatsLine } from "./agent/prompt.js";
|
|
27
28
|
import { allTools } from "./tools/registry.js";
|
|
28
29
|
import { ui, c, fmtTokens } from "./ui/ui.js";
|
|
29
30
|
import { runRepl } from "./repl.js";
|
|
30
|
-
const
|
|
31
|
+
const pkgVersion = JSON.parse(fs.readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
|
|
32
|
+
const program = new Command().name("brito").description("CLI de agentes de desenvolvimento com grafo de conhecimento").version(pkgVersion, "-v, --version", "mostra a versão").enablePositionalOptions();
|
|
31
33
|
const rootOf = (o) => path.resolve(o.dir ?? process.cwd());
|
|
32
34
|
program
|
|
33
35
|
.command("index")
|
|
@@ -78,7 +80,8 @@ async function plainAsk(q) {
|
|
|
78
80
|
* processo do servidor MCP e consulta ESTE processo por um socket unix, onde está o terminal.
|
|
79
81
|
*/
|
|
80
82
|
function startApprovalServer(io) {
|
|
81
|
-
|
|
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`);
|
|
82
85
|
try {
|
|
83
86
|
fs.unlinkSync(sock);
|
|
84
87
|
}
|
|
@@ -353,7 +356,7 @@ const skillsCommand = (root, io, llm) => ({
|
|
|
353
356
|
const skills = listSkills(root);
|
|
354
357
|
for (const s of skills) {
|
|
355
358
|
const stale = staleRefs(root, s);
|
|
356
|
-
ui.print(` ${c.soft(s.name)} ${c.muted(`(${s.scope}) v${s.version} · ${s.uses} usos · último uso ${s.lastUsed} · ${s.description}`)}${stale.length ? " " + c.warn(`⚠ refs obsoletas: ${stale.join(", ")}`) : ""}`);
|
|
359
|
+
ui.print(` ${c.soft(s.name)} ${c.muted(`(${s.scope}) v${s.version} · ${s.uses} usos${s.origin === "auto" ? ` · ${s.status === "trial" ? "em teste" : "automática"} ${s.wins}✓/${s.losses}✗` : ""} · último uso ${s.lastUsed} · ${s.description}`)}${stale.length ? " " + c.warn(`⚠ refs obsoletas: ${stale.join(", ")}`) : ""}`);
|
|
357
360
|
}
|
|
358
361
|
const prune = pruneCandidates(skills);
|
|
359
362
|
if (prune.length)
|
|
@@ -367,6 +370,10 @@ function makeLearner(getTurns, ctx, complete, model) {
|
|
|
367
370
|
return async (force = false) => {
|
|
368
371
|
const turns = getTurns();
|
|
369
372
|
const turn = turns[turns.length - 1] ?? null;
|
|
373
|
+
// O resultado do turno vira evidência para as skills usadas nele (promoção/arquivamento), antes de refletir.
|
|
374
|
+
if (!force)
|
|
375
|
+
for (const n of [...recordOutcomes(turn, ctx), ...(skillsMode() === "off" ? [] : expireTrials(ctx.root))])
|
|
376
|
+
ui.print(c.ok("✦ ") + n);
|
|
370
377
|
if (!force && !shouldReflect(turn, ctx)) {
|
|
371
378
|
ctx.loadedSkills?.clear();
|
|
372
379
|
return;
|
|
@@ -384,6 +391,13 @@ function makeLearner(getTurns, ctx, complete, model) {
|
|
|
384
391
|
ui.print(c.ok("✦ ") + n);
|
|
385
392
|
if (!notes.length)
|
|
386
393
|
ui.info("✦ nenhuma skill nova");
|
|
394
|
+
if (skillsMode() === "auto") {
|
|
395
|
+
try {
|
|
396
|
+
for (const n of await autoConsolidate(ctx.root, model, complete))
|
|
397
|
+
ui.print(c.ok("✦ ") + n);
|
|
398
|
+
}
|
|
399
|
+
catch { /* manutenção é opcional */ }
|
|
400
|
+
}
|
|
387
401
|
}
|
|
388
402
|
catch (e) {
|
|
389
403
|
ui.turnEnd(false);
|
package/dist/repl.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { shellFor } from "./util/platform.js";
|
|
1
2
|
import readline from "node:readline";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import { exec } from "node:child_process";
|
|
@@ -81,7 +82,7 @@ export async function runRepl(b, o) {
|
|
|
81
82
|
return r;
|
|
82
83
|
};
|
|
83
84
|
// ---------------------------------------------------------------- comandos
|
|
84
|
-
const shell = (cmd) => new Promise((done) => exec(cmd, { cwd: o.root, maxBuffer: 10 * 1024 * 1024 }, (err, out, errOut) => {
|
|
85
|
+
const shell = (cmd) => new Promise((done) => exec(cmd, { cwd: o.root, shell: shellFor(), maxBuffer: 10 * 1024 * 1024 }, (err, out, errOut) => {
|
|
85
86
|
ui.print((out + (errOut ? c.err(errOut) : "") + (err && !out && !errOut ? c.err(String(err.message)) : "")).replace(/\n$/, ""));
|
|
86
87
|
done();
|
|
87
88
|
}));
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
1
3
|
import { z } from "zod";
|
|
2
4
|
import { toApiTools } from "../tools/registry.js";
|
|
3
5
|
import { describeSave } from "./describe.js";
|
|
@@ -67,10 +69,35 @@ export function describeProposal(root, p, now = Date.now()) {
|
|
|
67
69
|
return `REMOVER skill ${p.name}${s ? ` (${s.scope}, ${s.uses} usos, último uso há ${daysSince(s.lastUsed, now)} dias; vai para .trash)` : ""}\n motivo: ${p.reason}`;
|
|
68
70
|
}
|
|
69
71
|
/** Aplica direto no store (o usuário já aprovou; não passa pelo atalho de aprovação do modo bypass). */
|
|
70
|
-
export function applyProposal(root, p) {
|
|
72
|
+
export function applyProposal(root, p, auto = false) {
|
|
71
73
|
if (p.kind === "save") {
|
|
72
|
-
const r = saveSkill(root, p.input, p.scope);
|
|
74
|
+
const r = saveSkill(root, p.input, p.scope, { auto });
|
|
73
75
|
return r.created ? `skill ${p.input.name} criada (${r.scope}, v1)` : `skill ${p.input.name} atualizada (${r.scope}, v${r.version})`;
|
|
74
76
|
}
|
|
75
77
|
return deleteSkill(root, p.name) ? `skill ${p.name} removida` : `skill ${p.name} não encontrada`;
|
|
76
78
|
}
|
|
79
|
+
/** Só propostas que tocam skills criadas sozinhas podem ser aplicadas sem o usuário; as demais ficam para `/skills consolidate`. */
|
|
80
|
+
export function autoApplicable(root, proposals) {
|
|
81
|
+
return proposals.filter((p) => {
|
|
82
|
+
const s = getSkill(root, p.kind === "save" ? p.input.name : p.name);
|
|
83
|
+
return p.kind === "save" ? (!s || s.origin === "auto") : s?.origin === "auto";
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
const STAMP = ".last-consolidate";
|
|
87
|
+
const CONSOLIDATE_EVERY_DAYS = 7;
|
|
88
|
+
const CONSOLIDATE_MIN_SKILLS = 6;
|
|
89
|
+
/** Consolidação periódica sem o usuário: no máximo 1x por semana, com ≥6 skills, só sobre skills automáticas. Devolve avisos. */
|
|
90
|
+
export async function autoConsolidate(root, model, complete, now = Date.now()) {
|
|
91
|
+
if (listSkills(root, now).length < CONSOLIDATE_MIN_SKILLS)
|
|
92
|
+
return [];
|
|
93
|
+
const stamp = path.join(root, ".brito", "skills", STAMP);
|
|
94
|
+
try {
|
|
95
|
+
if (now - fs.statSync(stamp).mtimeMs < CONSOLIDATE_EVERY_DAYS * 86_400_000)
|
|
96
|
+
return [];
|
|
97
|
+
}
|
|
98
|
+
catch { /* nunca rodou */ }
|
|
99
|
+
fs.mkdirSync(path.dirname(stamp), { recursive: true });
|
|
100
|
+
fs.writeFileSync(stamp, new Date(now).toISOString());
|
|
101
|
+
const proposals = autoApplicable(root, await proposeConsolidation(root, model, complete, now));
|
|
102
|
+
return proposals.map((p) => applyProposal(root, p, true));
|
|
103
|
+
}
|
package/dist/skills/learn-cli.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
+
import { claudeCommand } from "../util/platform.js";
|
|
2
3
|
import os from "node:os";
|
|
3
4
|
// Backend sem ANTHROPIC_API_KEY: usa o `claude` oficial (login do usuário). Sem tools nem settings: só julga o transcript.
|
|
4
5
|
// O `skill_save` vira saída estruturada ({saves:[…]}) e é convertida de volta em blocos tool_use.
|
|
@@ -30,7 +31,8 @@ export const claudeCliComplete = ({ model, system, user }) => new Promise((resol
|
|
|
30
31
|
"--disable-slash-commands", "--setting-sources", "", "--json-schema", JSON.stringify(SCHEMA)];
|
|
31
32
|
if (model)
|
|
32
33
|
args.push("--model", model);
|
|
33
|
-
const
|
|
34
|
+
const cmd = claudeCommand(args);
|
|
35
|
+
const child = spawn(cmd.command, cmd.args, { cwd: os.tmpdir(), stdio: ["pipe", "pipe", "pipe"] });
|
|
34
36
|
let out = "", err = "";
|
|
35
37
|
child.stdout.on("data", (d) => (out += d));
|
|
36
38
|
child.stderr.on("data", (d) => (err += d));
|
package/dist/skills/learn.js
CHANGED
|
@@ -3,6 +3,7 @@ import { z } from "zod";
|
|
|
3
3
|
import { execTool, toApiTools } from "../tools/registry.js";
|
|
4
4
|
import { perms } from "../permissions.js";
|
|
5
5
|
import { getSkill, listPending, listSkills, savePending, staleRefs } from "./store.js";
|
|
6
|
+
import { saveAuto, skillsMode, recordOutcome } from "./trial.js";
|
|
6
7
|
import { appendJournal, describeEntry, entryFromTurn, similarEntries } from "./journal.js";
|
|
7
8
|
/** Turno longo sem nenhum outro sinal só vale reflexão a partir deste tamanho. */
|
|
8
9
|
const MIN_TOOLS = Number(process.env.BRITO_SKILL_MIN_TOOLS ?? 12);
|
|
@@ -76,12 +77,27 @@ export function extractSignals(turn) {
|
|
|
76
77
|
* `BRITO_LEARN_V2=0` volta ao gatilho antigo (≥6 tools ou skill usada).
|
|
77
78
|
*/
|
|
78
79
|
export function shouldReflect(turn, ctx) {
|
|
79
|
-
if (!turn || !turn.finished)
|
|
80
|
+
if (!turn || !turn.finished || skillsMode() === "off")
|
|
80
81
|
return false;
|
|
81
82
|
if (process.env.BRITO_LEARN_V2 === "0")
|
|
82
83
|
return turn.toolCalls >= 6 || (ctx.loadedSkills?.size ?? 0) > 0;
|
|
83
84
|
return extractSignals(turn).length > 0 || turn.toolCalls >= MIN_TOOLS;
|
|
84
85
|
}
|
|
86
|
+
/** Resultado do turno para as skills carregadas nele: tropeço (erro→conserto ou correção do usuário) é falha. Abortado = neutro. */
|
|
87
|
+
export function judgeTurn(turn) {
|
|
88
|
+
if (!turn.finished)
|
|
89
|
+
return null;
|
|
90
|
+
return extractSignals(turn).length ? "loss" : "win";
|
|
91
|
+
}
|
|
92
|
+
/** Credita o turno às skills carregadas nele; pode promover ou arquivar skills em teste. Devolve avisos. */
|
|
93
|
+
export function recordOutcomes(turn, ctx) {
|
|
94
|
+
if (!turn || skillsMode() === "off")
|
|
95
|
+
return [];
|
|
96
|
+
const verdict = judgeTurn(turn);
|
|
97
|
+
if (!verdict)
|
|
98
|
+
return [];
|
|
99
|
+
return [...(ctx.loadedSkills ?? [])].flatMap((n) => recordOutcome(ctx.root, n, verdict === "win") ?? []);
|
|
100
|
+
}
|
|
85
101
|
export const defaultComplete = async ({ model, system, user, tools, maxTokens }) => {
|
|
86
102
|
const res = await new Anthropic().messages.create({ model, max_tokens: maxTokens, system, tools, messages: [{ role: "user", content: user }] });
|
|
87
103
|
return res.content;
|
|
@@ -172,8 +188,21 @@ export function buildReflectInput(turn, ctx, previous) {
|
|
|
172
188
|
(previous ? `# Turno anterior (só contexto, não é o que está sendo avaliado)\n${renderPrevious(previous)}\n\n` : "") +
|
|
173
189
|
`# Transcript do turno (${turn.toolCalls} chamadas de tool, ${turn.errors} com erro)\n${renderTranscript(turn)}`);
|
|
174
190
|
}
|
|
191
|
+
const SaveArgs = z.object({ name: z.string(), description: z.string(), body: z.string(), refs: z.array(z.string()).default([]), reason: z.string().default(""), scope: z.enum(["project", "global"]).optional() });
|
|
192
|
+
/** Modo autônomo: grava em teste (ou na fila de revisão, se for arriscado) sem perguntar ao usuário. */
|
|
193
|
+
function stageAuto(input, ctx) {
|
|
194
|
+
const parsed = SaveArgs.safeParse(input);
|
|
195
|
+
if (!parsed.success)
|
|
196
|
+
return { saved: false, msg: "skill não salva: input inválido" };
|
|
197
|
+
try {
|
|
198
|
+
return saveAuto(ctx.root, parsed.data);
|
|
199
|
+
}
|
|
200
|
+
catch (e) {
|
|
201
|
+
return { saved: false, msg: `skill não salva: ${e?.message ?? e}` };
|
|
202
|
+
}
|
|
203
|
+
}
|
|
175
204
|
function stage(input, ctx) {
|
|
176
|
-
const parsed =
|
|
205
|
+
const parsed = SaveArgs.safeParse(input);
|
|
177
206
|
if (!parsed.success)
|
|
178
207
|
return `skill não salva: input inválido`;
|
|
179
208
|
try {
|
|
@@ -195,6 +224,13 @@ export async function reflect(turn, ctx, model, complete = defaultComplete, prev
|
|
|
195
224
|
continue;
|
|
196
225
|
saves++;
|
|
197
226
|
const name = String(b.input?.name ?? "");
|
|
227
|
+
if (skillsMode() === "auto") {
|
|
228
|
+
const r = stageAuto(b.input, ctx);
|
|
229
|
+
out.push(r.msg);
|
|
230
|
+
if (r.saved)
|
|
231
|
+
saved.push(name);
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
198
234
|
// Em bypass nada pede aprovação: a proposta vai para a fila de revisão em vez de virar instrução ativa.
|
|
199
235
|
if (perms.mode === "bypass") {
|
|
200
236
|
const msg = stage(b.input, ctx);
|
package/dist/skills/store.js
CHANGED
|
@@ -22,14 +22,18 @@ function parse(text, scope) {
|
|
|
22
22
|
updated: meta.updated ?? today(),
|
|
23
23
|
lastUsed: meta.lastUsed ?? meta.updated ?? today(),
|
|
24
24
|
refs: (meta.refs ?? "").split(",").map((r) => r.trim()).filter(Boolean),
|
|
25
|
+
status: meta.status === "trial" ? "trial" : "active",
|
|
26
|
+
origin: meta.origin === "auto" ? "auto" : "manual",
|
|
27
|
+
wins: Number(meta.wins) || 0,
|
|
28
|
+
losses: Number(meta.losses) || 0,
|
|
25
29
|
body: body.trim(),
|
|
26
30
|
scope,
|
|
27
31
|
};
|
|
28
32
|
}
|
|
29
|
-
function
|
|
33
|
+
export function writeSkill(root, s) {
|
|
30
34
|
const file = fileOf(root, s.scope, s.name);
|
|
31
35
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
32
|
-
fs.writeFileSync(file, stringifyFrontmatter({ name: s.name, description: s.description, version: s.version, uses: s.uses, created: s.created, updated: s.updated, lastUsed: s.lastUsed, refs: s.refs.join(", ") }, s.body));
|
|
36
|
+
fs.writeFileSync(file, stringifyFrontmatter({ name: s.name, description: s.description, version: s.version, uses: s.uses, created: s.created, updated: s.updated, lastUsed: s.lastUsed, refs: s.refs.join(", "), status: s.status, origin: s.origin, wins: s.wins, losses: s.losses }, s.body));
|
|
33
37
|
}
|
|
34
38
|
/** Lê uma skill de um escopo específico. */
|
|
35
39
|
export function getSkillIn(root, name, scope) {
|
|
@@ -131,8 +135,12 @@ export function validate(input) {
|
|
|
131
135
|
export function targetScope(root, name, scope) {
|
|
132
136
|
return scope ?? getSkill(root, name)?.scope ?? "project";
|
|
133
137
|
}
|
|
134
|
-
/**
|
|
135
|
-
|
|
138
|
+
/**
|
|
139
|
+
* Cria ou atualiza. A versão anterior vai para `history/vN.md`.
|
|
140
|
+
* `opts.auto` = gravação sem revisão humana (reflexão autônoma): skill nova nasce em teste; atualizar mantém a origem, e
|
|
141
|
+
* uma skill em teste zera o placar (conteúdo novo = hipótese nova). Sem `auto`, um humano aprovou: vira ativa e manual.
|
|
142
|
+
*/
|
|
143
|
+
export function saveSkill(root, input, scope, opts = {}) {
|
|
136
144
|
validate(input);
|
|
137
145
|
const target = targetScope(root, input.name, scope);
|
|
138
146
|
const prev = getSkillIn(root, input.name, target);
|
|
@@ -150,10 +158,14 @@ export function saveSkill(root, input, scope) {
|
|
|
150
158
|
updated: today(),
|
|
151
159
|
lastUsed: prev?.lastUsed ?? today(),
|
|
152
160
|
refs: input.refs ?? prev?.refs ?? [],
|
|
161
|
+
status: opts.auto ? (prev?.status ?? "trial") : "active",
|
|
162
|
+
origin: opts.auto ? (prev?.origin ?? "auto") : "manual",
|
|
163
|
+
wins: opts.auto && prev?.status === "trial" ? 0 : prev?.wins ?? 0,
|
|
164
|
+
losses: opts.auto && prev?.status === "trial" ? 0 : prev?.losses ?? 0,
|
|
153
165
|
body: input.body.trim(),
|
|
154
166
|
scope: target,
|
|
155
167
|
};
|
|
156
|
-
|
|
168
|
+
writeSkill(root, skill);
|
|
157
169
|
return { created: !prev, version: skill.version, scope: target };
|
|
158
170
|
}
|
|
159
171
|
/** Propostas aguardando revisão (modo bypass): ficam fora do índice, pois `listScope` ignora diretórios com ponto. */
|
|
@@ -204,7 +216,7 @@ export function rejectPending(root, name) {
|
|
|
204
216
|
export function bumpUses(root, name) {
|
|
205
217
|
const s = getSkill(root, name);
|
|
206
218
|
if (s)
|
|
207
|
-
|
|
219
|
+
writeSkill(root, { ...s, uses: s.uses + 1, lastUsed: today() });
|
|
208
220
|
}
|
|
209
221
|
/** Move para `.trash` do próprio escopo em vez de apagar. */
|
|
210
222
|
export function deleteSkill(root, name, scope) {
|
|
@@ -230,5 +242,5 @@ export function promoteSkill(root, name) {
|
|
|
230
242
|
/** Índice compacto para o system prompt (divulgação progressiva: o corpo só entra via skill_load). */
|
|
231
243
|
export function skillsIndex(root, max = 40, now = Date.now()) {
|
|
232
244
|
const all = listSkills(root, now).slice(0, max);
|
|
233
|
-
return all.length ? all.map((s) => `- ${s.name}${s.scope === "global" ? " (global)" : ""}${staleRefs(root, s).length ? " [refs obsoletas]" : ""}: ${s.description}`).join("\n") : "";
|
|
245
|
+
return all.length ? all.map((s) => `- ${s.name}${s.scope === "global" ? " (global)" : ""}${s.status === "trial" ? " (em teste)" : ""}${staleRefs(root, s).length ? " [refs obsoletas]" : ""}: ${s.description}`).join("\n") : "";
|
|
234
246
|
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { daysSince, deleteSkill, getSkill, lintSkill, listSkills, savePending, saveSkill, writeSkill } from "./store.js";
|
|
2
|
+
export const skillsMode = () => (process.env.BRITO_SKILLS === "ask" || process.env.BRITO_SKILLS === "off" ? process.env.BRITO_SKILLS : "auto");
|
|
3
|
+
/** Usos bem-sucedidos para promover (com no máximo 1 tropeço a cada 3 sucessos). */
|
|
4
|
+
export const PROMOTE_WINS = 3;
|
|
5
|
+
/** Falhas que arquivam uma skill em teste sem mais vitórias que falhas. */
|
|
6
|
+
export const ARCHIVE_LOSSES = 2;
|
|
7
|
+
/** Skill em teste que ninguém carregou neste prazo é arquivada. */
|
|
8
|
+
export const TRIAL_DAYS = 21;
|
|
9
|
+
/** Teto de skills novas criadas sozinhas por dia (contido: cada uma vira instrução para o agente). */
|
|
10
|
+
export const DAILY_LIMIT = 3;
|
|
11
|
+
/** Registra o resultado de um turno que usou a skill. Devolve um aviso se ela foi promovida ou arquivada. */
|
|
12
|
+
export function recordOutcome(root, name, win) {
|
|
13
|
+
const s = getSkill(root, name);
|
|
14
|
+
if (!s)
|
|
15
|
+
return null;
|
|
16
|
+
const next = { ...s, wins: s.wins + (win ? 1 : 0), losses: s.losses + (win ? 0 : 1) };
|
|
17
|
+
if (s.origin === "auto") {
|
|
18
|
+
if (s.status === "trial") {
|
|
19
|
+
if (next.wins >= PROMOTE_WINS && next.wins >= 3 * next.losses) {
|
|
20
|
+
writeSkill(root, { ...next, status: "active" });
|
|
21
|
+
return `skill ${name} promovida: ${next.wins} usos bem-sucedidos, ${next.losses} com tropeço`;
|
|
22
|
+
}
|
|
23
|
+
if (next.losses >= ARCHIVE_LOSSES && next.losses >= next.wins) {
|
|
24
|
+
deleteSkill(root, name, s.scope);
|
|
25
|
+
return `skill ${name} arquivada: ${next.losses} usos com tropeço e ${next.wins} bem-sucedidos (recupere em .brito/skills/.trash)`;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
else if (next.losses >= 3 && next.losses > next.wins) {
|
|
29
|
+
deleteSkill(root, name, s.scope);
|
|
30
|
+
return `skill ${name} arquivada: passou a falhar (${next.losses} tropeços, ${next.wins} sucessos)`;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
writeSkill(root, next);
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
/** Arquiva as skills em teste que ninguém usou no prazo. */
|
|
37
|
+
export function expireTrials(root, now = Date.now()) {
|
|
38
|
+
const out = [];
|
|
39
|
+
for (const s of listSkills(root, now)) {
|
|
40
|
+
if (s.origin === "auto" && s.status === "trial" && daysSince(s.lastUsed, now) > TRIAL_DAYS && deleteSkill(root, s.name, s.scope)) {
|
|
41
|
+
out.push(`skill ${s.name} arquivada: em teste há mais de ${TRIAL_DAYS} dias sem uso`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Grava uma skill proposta pela reflexão sem perguntar ao usuário, mas só quando é seguro:
|
|
48
|
+
* - sempre escopo do projeto (global exige decisão humana);
|
|
49
|
+
* - skill de origem manual nunca é reescrita sozinha: a mudança vira proposta pendente;
|
|
50
|
+
* - conteúdo com comando destrutivo também vai para a fila de revisão;
|
|
51
|
+
* - nova skill respeita o teto diário.
|
|
52
|
+
*/
|
|
53
|
+
export function saveAuto(root, input, now = Date.now()) {
|
|
54
|
+
const prev = getSkill(root, input.name);
|
|
55
|
+
if (prev && prev.origin === "manual") {
|
|
56
|
+
const scope = savePending(root, input, prev.scope);
|
|
57
|
+
return { saved: false, msg: `skill ${input.name} (${scope}) é revisada por você: mudança proposta em /skills approve ${input.name}` };
|
|
58
|
+
}
|
|
59
|
+
if (lintSkill(input).warnings.some((w) => w.startsWith("comando destrutivo"))) {
|
|
60
|
+
const scope = savePending(root, input, "project");
|
|
61
|
+
return { saved: false, msg: `skill ${input.name} (${scope}) tem comando destrutivo: aguardando /skills approve ${input.name}` };
|
|
62
|
+
}
|
|
63
|
+
if (!prev) {
|
|
64
|
+
const today = new Date(now).toISOString().slice(0, 10);
|
|
65
|
+
const created = listSkills(root, now).filter((s) => s.origin === "auto" && s.created === today).length;
|
|
66
|
+
if (created >= DAILY_LIMIT)
|
|
67
|
+
return { saved: false, msg: `skill ${input.name} não criada: teto de ${DAILY_LIMIT} skills automáticas por dia` };
|
|
68
|
+
}
|
|
69
|
+
const r = saveSkill(root, input, prev?.scope ?? "project", { auto: true });
|
|
70
|
+
return { saved: true, msg: r.created ? `skill ${input.name} criada em teste (project, v1)` : `skill ${input.name} atualizada (${r.scope}, v${r.version})` };
|
|
71
|
+
}
|
package/dist/tools/bash.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { exec } from "node:child_process";
|
|
3
3
|
import { defineTool } from "./registry.js";
|
|
4
|
+
import { shellFor } from "../util/platform.js";
|
|
4
5
|
defineTool({
|
|
5
6
|
name: "bash",
|
|
6
7
|
description: "Executa um comando shell na raiz do projeto (timeout padrão 120s).",
|
|
@@ -9,7 +10,7 @@ defineTool({
|
|
|
9
10
|
mutates: true,
|
|
10
11
|
describe: (i) => i.command,
|
|
11
12
|
run: ({ command, timeout_ms = 120_000 }, { root }) => new Promise((resolve) => {
|
|
12
|
-
exec(command, { cwd: root, timeout: timeout_ms, maxBuffer: 10 * 1024 * 1024 }, (err, stdout, stderr) => {
|
|
13
|
+
exec(command, { cwd: root, shell: shellFor(), timeout: timeout_ms, maxBuffer: 10 * 1024 * 1024 }, (err, stdout, stderr) => {
|
|
13
14
|
const out = (stdout + (stderr ? `\n[stderr]\n${stderr}` : "")).trim();
|
|
14
15
|
resolve(`${err ? `[exit ${err.code ?? "?"}] ` : ""}${out.slice(-30_000) || "(sem saída)"}`);
|
|
15
16
|
});
|
package/dist/tools/fs.js
CHANGED
|
@@ -5,7 +5,10 @@ import fg from "fast-glob";
|
|
|
5
5
|
import { defineTool } from "./registry.js";
|
|
6
6
|
const abs = (root, p) => {
|
|
7
7
|
const full = path.resolve(root, p);
|
|
8
|
-
|
|
8
|
+
// path.relative cobre dois defeitos do startsWith: irmão com mesmo prefixo (/proj vs /proj-x) e, no Windows,
|
|
9
|
+
// diferença de caixa na letra da unidade ou nos diretórios (C:\ vs c:\), que fazia caminhos válidos serem recusados.
|
|
10
|
+
const rel = path.relative(path.resolve(root), full);
|
|
11
|
+
if (rel === ".." || rel.startsWith(".." + path.sep) || path.isAbsolute(rel))
|
|
9
12
|
throw new Error("Caminho fora do projeto.");
|
|
10
13
|
return full;
|
|
11
14
|
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
let claudeCache;
|
|
5
|
+
/** Alvo real de um shim npm (`claude.cmd`): `"%dp0%\caminho\cli.js"` ou `...\claude.exe`. */
|
|
6
|
+
export function resolveCmdShim(shimPath, content) {
|
|
7
|
+
const target = content.match(/"%dp0%\\([^"]+)"/)?.[1];
|
|
8
|
+
if (!target)
|
|
9
|
+
return null;
|
|
10
|
+
const file = path.win32.join(path.win32.dirname(shimPath), target);
|
|
11
|
+
if (/\.exe$/i.test(file))
|
|
12
|
+
return { command: file, prefix: [] };
|
|
13
|
+
if (/\.(c|m)?js$/i.test(file))
|
|
14
|
+
return { command: process.execPath, prefix: [file] };
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
function findClaude() {
|
|
18
|
+
const found = spawnSync("where", ["claude"], { encoding: "utf8" }).stdout?.split(/\r?\n/).map((l) => l.trim()).filter(Boolean) ?? [];
|
|
19
|
+
const exe = found.find((f) => /\.exe$/i.test(f));
|
|
20
|
+
if (exe)
|
|
21
|
+
return { command: exe, prefix: [] };
|
|
22
|
+
for (const f of found.filter((x) => /\.cmd$/i.test(x))) {
|
|
23
|
+
try {
|
|
24
|
+
const r = resolveCmdShim(f, fs.readFileSync(f, "utf8"));
|
|
25
|
+
if (r && fs.existsSync(r.prefix[0] ?? r.command))
|
|
26
|
+
return r;
|
|
27
|
+
}
|
|
28
|
+
catch { /* shim ilegível: tenta o próximo */ }
|
|
29
|
+
}
|
|
30
|
+
return { command: "claude", prefix: [] };
|
|
31
|
+
}
|
|
32
|
+
/** Comando e argumentos para executar o `claude` com segurança em qualquer sistema. */
|
|
33
|
+
export function claudeCommand(args) {
|
|
34
|
+
if (process.platform !== "win32")
|
|
35
|
+
return { command: "claude", args };
|
|
36
|
+
claudeCache ??= findClaude();
|
|
37
|
+
return { command: claudeCache.command, args: [...claudeCache.prefix, ...args] };
|
|
38
|
+
}
|
|
39
|
+
let shellCache = null;
|
|
40
|
+
/** Shell para `exec`: `BRITO_SHELL` > Git Bash (só no Windows) > padrão do sistema (undefined). */
|
|
41
|
+
export function shellFor() {
|
|
42
|
+
if (process.env.BRITO_SHELL)
|
|
43
|
+
return process.env.BRITO_SHELL;
|
|
44
|
+
if (process.platform !== "win32")
|
|
45
|
+
return undefined;
|
|
46
|
+
if (shellCache !== null)
|
|
47
|
+
return shellCache;
|
|
48
|
+
const roots = [process.env.ProgramFiles, process.env["ProgramFiles(x86)"], process.env.LOCALAPPDATA && path.join(process.env.LOCALAPPDATA, "Programs")].filter((r) => !!r);
|
|
49
|
+
shellCache = roots.map((r) => path.join(r, "Git", "bin", "bash.exe")).find((p) => fs.existsSync(p));
|
|
50
|
+
return shellCache;
|
|
51
|
+
}
|
|
52
|
+
/** Linha para o system prompt dizendo qual shell o comando `bash` usa de fato (só no Windows; nos outros é bash/sh). */
|
|
53
|
+
export function shellNote() {
|
|
54
|
+
if (process.platform !== "win32")
|
|
55
|
+
return "";
|
|
56
|
+
return shellFor() ? "\nShell dos comandos: Git Bash (sintaxe bash)." : "\nShell dos comandos: cmd.exe do Windows (não há bash): use dir/type/findstr e sintaxe do cmd, não ls/cat/grep.";
|
|
57
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "brito-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.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": {
|