brito-cli 0.3.2 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -7,27 +7,28 @@ 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, describeClaudeTool } from "./permissions.js";
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";
23
- import { applyProposal, describeProposal, proposeConsolidation, pruneCandidates } from "./skills/consolidate.js";
24
- import { defaultComplete, reflect, shouldReflect } from "./skills/learn.js";
22
+ import { applyProposal, autoConsolidate, describeProposal, proposeConsolidation, pruneCandidates } from "./skills/consolidate.js";
23
+ import { defaultComplete, recordOutcomes, reflect, shouldReflect } from "./skills/learn.js";
24
+ import { expireTrials, skillsMode } from "./skills/trial.js";
25
25
  import { claudeCliComplete } from "./skills/learn-cli.js";
26
26
  import { buildSystemPrompt, graphStatsLine } from "./agent/prompt.js";
27
27
  import { allTools } from "./tools/registry.js";
28
28
  import { ui, c, fmtTokens } from "./ui/ui.js";
29
29
  import { runRepl } from "./repl.js";
30
- const program = new Command().name("brito").description("CLI de agentes de desenvolvimento com grafo de conhecimento").enablePositionalOptions();
30
+ const pkgVersion = JSON.parse(fs.readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
31
+ 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
32
  const rootOf = (o) => path.resolve(o.dir ?? process.cwd());
32
33
  program
33
34
  .command("index")
@@ -42,6 +43,12 @@ program
42
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)`);
43
44
  await g.close();
44
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); });
45
52
  program
46
53
  .command("query <cypher>")
47
54
  .description("Executa Cypher no grafo")
@@ -73,48 +80,6 @@ async function plainAsk(q) {
73
80
  return /^(s|y)/i.test(a.trim());
74
81
  }
75
82
  /** Sem API key: o `claude` oficial (login da assinatura) faz o modelo; grafo/memória/skills chegam via MCP. */
76
- /**
77
- * Aprovações do claude CLI: o claude chama a tool MCP `approve` (--permission-prompt-tool), que roda no
78
- * processo do servidor MCP e consulta ESTE processo por um socket unix, onde está o terminal.
79
- */
80
- function startApprovalServer(io) {
81
- const sock = path.join(os.tmpdir(), `brito-approve-${process.pid}.sock`);
82
- try {
83
- fs.unlinkSync(sock);
84
- }
85
- catch { /* não existia */ }
86
- const server = net.createServer((conn) => {
87
- let buf = "";
88
- conn.on("data", async (d) => {
89
- buf += d.toString();
90
- const nl = buf.indexOf("\n");
91
- if (nl < 0)
92
- return;
93
- let reply;
94
- try {
95
- const req = JSON.parse(buf.slice(0, nl));
96
- const plan = req.tool_name === "ExitPlanMode";
97
- const ok = await io.ask(describeClaudeTool(req.tool_name, req.input), { force: plan });
98
- if (ok && plan)
99
- perms.mode = "manual";
100
- 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." };
101
- }
102
- catch (e) {
103
- reply = { allow: false, message: `erro no pedido de aprovação: ${e?.message ?? e}` };
104
- }
105
- conn.end(JSON.stringify(reply) + "\n");
106
- });
107
- conn.on("error", () => { });
108
- });
109
- server.listen(sock);
110
- server.unref();
111
- const close = () => { server.close(); try {
112
- fs.unlinkSync(sock);
113
- }
114
- catch { /* já removido */ } };
115
- process.on("exit", close);
116
- return { path: sock, close };
117
- }
118
83
  /** Menus ↑↓+Enter do `/model`: modelo e depois escopo. null = cancelado. */
119
84
  async function pickModel(current) {
120
85
  const models = Object.entries(MODEL_ALIASES);
@@ -353,7 +318,7 @@ const skillsCommand = (root, io, llm) => ({
353
318
  const skills = listSkills(root);
354
319
  for (const s of skills) {
355
320
  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(", ")}`) : ""}`);
321
+ 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
322
  }
358
323
  const prune = pruneCandidates(skills);
359
324
  if (prune.length)
@@ -367,6 +332,10 @@ function makeLearner(getTurns, ctx, complete, model) {
367
332
  return async (force = false) => {
368
333
  const turns = getTurns();
369
334
  const turn = turns[turns.length - 1] ?? null;
335
+ // O resultado do turno vira evidência para as skills usadas nele (promoção/arquivamento), antes de refletir.
336
+ if (!force)
337
+ for (const n of [...recordOutcomes(turn, ctx), ...(skillsMode() === "off" ? [] : expireTrials(ctx.root))])
338
+ ui.print(c.ok("✦ ") + n);
370
339
  if (!force && !shouldReflect(turn, ctx)) {
371
340
  ctx.loadedSkills?.clear();
372
341
  return;
@@ -384,6 +353,13 @@ function makeLearner(getTurns, ctx, complete, model) {
384
353
  ui.print(c.ok("✦ ") + n);
385
354
  if (!notes.length)
386
355
  ui.info("✦ nenhuma skill nova");
356
+ if (skillsMode() === "auto") {
357
+ try {
358
+ for (const n of await autoConsolidate(ctx.root, model, complete))
359
+ ui.print(c.ok("✦ ") + n);
360
+ }
361
+ catch { /* manutenção é opcional */ }
362
+ }
387
363
  }
388
364
  catch (e) {
389
365
  ui.turnEnd(false);
@@ -473,6 +449,12 @@ program
473
449
  .option("-d, --dir <dir>", "raiz do projeto")
474
450
  .option("-y, --yes", "aprovar edições e comandos automaticamente")
475
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
+ }
476
458
  if (!process.env.ANTHROPIC_API_KEY) {
477
459
  if (!hasClaudeCli()) {
478
460
  console.error(c.err("Defina ANTHROPIC_API_KEY ou instale o Claude Code e rode `claude` → /login."));
@@ -559,7 +541,8 @@ program
559
541
  },
560
542
  });
561
543
  });
562
- program.parseAsync().catch((e) => {
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) => {
563
546
  if (e instanceof GraphLockedError) {
564
547
  console.error(c.err("✗ ") + e.message);
565
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
+ }
@@ -1,5 +1,5 @@
1
1
  import Parser from "web-tree-sitter";
2
- import fg from "fast-glob";
2
+ import { glob as fg } from "tinyglobby";
3
3
  import fs from "node:fs";
4
4
  import path from "node:path";
5
5
  import crypto from "node:crypto";
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
  }));
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
+ }
@@ -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
+ }
@@ -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 child = spawn("claude", args, { cwd: os.tmpdir(), stdio: ["pipe", "pipe", "pipe"] });
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));
@@ -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 = 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() }).safeParse(input);
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);