brito-cli 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -7,6 +7,7 @@ import { skillsIndex } from "../skills/store.js";
7
7
  import { MCP_NAME, MCP_PREFIX, APPROVE_TOOL } from "../mcp/server.js";
8
8
  import { ui as terminalUi } from "../ui/ui.js";
9
9
  import { MODE_ENV, perms } from "../permissions.js";
10
+ import { CLAUDE_EDIT_TOOLS, ORCH_MCP_TIMEOUT, ORCH_NOTE, ORCH_NOTE_CLAUDE, orch, orchEnv } from "../orchestrator.js";
10
11
  import { DEFAULT_MODEL, wireEffort } from "../model.js";
11
12
  import { subagentModel } from "../engine.js";
12
13
  import { claudeCommand } from "../util/platform.js";
@@ -211,11 +212,13 @@ export class ClaudeSession {
211
212
  aborted = false;
212
213
  args(text, imageDirs = []) {
213
214
  const { root, approvalSock, stats, mcpCommand } = this.o;
214
- const mcp = mcpConfig(mcpCommand, approvalSock, perms.mode, this.o.env);
215
+ // O modo orquestrador vale por turno, como o de permissão: o servidor MCP do turno sobe com ele no env.
216
+ const mcp = mcpConfig(mcpCommand, approvalSock, perms.mode, { ...this.o.env, ...orchEnv() });
215
217
  const readOnlyMcp = ["graph_search", "graph_callers", "graph_callees", "graph_impact", "graph_context", "graph_query", "graph_index", "remember", "recall", "skill_load", "digest"];
216
218
  const system = buildSystemPrompt(root, stats, skillsIndex(root)) +
217
219
  `\n\n# Nesta sessão\nAs tools do Brito chegam com o prefixo \`${MCP_PREFIX}\` (ex.: ${MCP_PREFIX}graph_context). Use-as como descrito acima. ` +
218
- `read_file/grep/glob/edit_file/bash correspondem a Read/Grep/Glob/Edit/Bash, e \`delegate\` corresponde à tool Agent/Task com os subagentes configurados (explore, plan, general e os do projeto).`;
220
+ `read_file/grep/glob/edit_file/bash correspondem a Read/Grep/Glob/Edit/Bash, e \`delegate\` corresponde à tool Agent/Task com os subagentes configurados (explore, plan, general e os do projeto).` +
221
+ (orch.on ? ORCH_NOTE + ORCH_NOTE_CLAUDE : "");
219
222
  const args = [
220
223
  "-p", text,
221
224
  this.started ? "--resume" : "--session-id", this.id,
@@ -234,6 +237,8 @@ export class ClaudeSession {
234
237
  // O modo vale por turno: Shift+Tab no REPL muda o próximo turno.
235
238
  const mode = { manual: "default", auto: "acceptEdits", plan: "plan", bypass: "bypassPermissions" }[perms.mode];
236
239
  args.push("--permission-mode", mode);
240
+ if (orch.on)
241
+ args.push("--disallowedTools", ...CLAUDE_EDIT_TOOLS);
237
242
  if (perms.mode !== "bypass") {
238
243
  args.push("--allowedTools", ...readOnlyMcp.map((n) => MCP_PREFIX + n), "--permission-prompt-tool", `${MCP_PREFIX}${APPROVE_TOOL}`);
239
244
  }
@@ -255,7 +260,8 @@ export class ClaudeSession {
255
260
  const child = (this.child = spawn(cmd.command, cmd.args, {
256
261
  cwd: this.o.root, stdio: ["ignore", "pipe", "pipe"],
257
262
  // `/reasoning none`: o claude desliga o raciocínio com MAX_THINKING_TOKENS=0 (não há `--effort none`)
258
- env: { ...process.env, ...this.o.env, BRITO_DEPTH: String(Number(process.env.BRITO_DEPTH ?? 0) + 1), ...(wireEffort(this.effort).thinking ? {} : { MAX_THINKING_TOKENS: "0" }) },
263
+ env: { ...process.env, ...this.o.env, BRITO_DEPTH: String(Number(process.env.BRITO_DEPTH ?? 0) + 1),
264
+ ...(orch.on && !process.env.MCP_TOOL_TIMEOUT ? { MCP_TOOL_TIMEOUT: ORCH_MCP_TIMEOUT } : {}), ...(wireEffort(this.effort).thinking ? {} : { MAX_THINKING_TOKENS: "0" }) },
259
265
  }));
260
266
  let buf = "", stderr = "", ok = true, interrupted = false;
261
267
  const failure = { stderr: "", code: null };
@@ -7,6 +7,7 @@ import { compactAtFor, resolveModel } from "../provider/resolve.js";
7
7
  import { fallbackStatus } from "../engine.js";
8
8
  import { activateFallback, classifyAnthropicError, fallbackExpired, fallbackUntil, fallbackWarning, returnToPrimary, shortError, triggersFallback } from "../provider/fallback.js";
9
9
  import { fmtBytes, findImages, imageBlock, splitOversize } from "../util/images.js";
10
+ import { orchGuard } from "../orchestrator.js";
10
11
  export const MODEL = initialModel();
11
12
  export class Agent {
12
13
  system;
@@ -117,7 +118,8 @@ export class Agent {
117
118
  const t0 = Date.now();
118
119
  if (!this.opts.quiet)
119
120
  this.ui.toolStart(u.id, u.name, u.input);
120
- const denied = this.opts.tools && !this.opts.tools.has(u.name);
121
+ const allow = this.allowed();
122
+ const denied = allow && !allow.has(u.name);
121
123
  const r = denied ? { text: `Tool não permitida para este agente: ${u.name}`, isError: true } : await execTool(u.name, u.input, this.ctx);
122
124
  if (this.opts.quiet)
123
125
  this.log(terminalUi.toolLine(u.name, u.input, Date.now() - t0, r.isError));
@@ -152,7 +154,7 @@ export class Agent {
152
154
  // write_file grande não é mais cortado no meio.
153
155
  maxTokens: target.kind === "openai" ? (thinking ? 16_384 : 8192) : 32_000,
154
156
  system: typeof this.system === "function" ? this.system() : this.system,
155
- tools: toApiTools(this.opts.tools),
157
+ tools: toApiTools(this.allowed()),
156
158
  messages: this.messages,
157
159
  thinking,
158
160
  effort: this.effort,
@@ -179,10 +181,14 @@ export class Agent {
179
181
  this.ui.warn(fallbackWarning(kind, why, status.label, until));
180
182
  activateFallback(fb, `${kind} (${why})`, Date.now(), until, status.name);
181
183
  this.model = status.name;
184
+ orchGuard(this.model, (m) => this.ui.warn(m)); // o qwen virou o principal: não há mais a quem delegar
182
185
  await this.compactIfNeeded();
183
186
  return this.callModel(hooks);
184
187
  }
185
188
  }
189
+ allowed() {
190
+ return typeof this.opts.tools === "function" ? this.opts.tools() : this.opts.tools;
191
+ }
186
192
  /**
187
193
  * Fallback temporário: passada a janela, o próximo turno tenta o Claude de novo (se falhar, `callModel` reativa o
188
194
  * fallback com uma janela nova). Só com ANTHROPIC_API_KEY: sem ela o Claude não roda neste loop (volta via /model).
@@ -23,22 +23,22 @@ export function buildSystemPrompt(root, graphStats, skills = "") {
23
23
  break;
24
24
  }
25
25
  }
26
- return `Você é Brito, um agente de engenharia de software que trabalha em CLI. Responda em português do Brasil, de forma direta.
27
-
28
- # Como trabalhar
29
- - Você tem um GRAFO DE CONHECIMENTO do projeto (símbolos, chamadas, imports) e uma MEMÓRIA em grafo (notas). ${graphStats}
30
- - Para entender código: comece por graph_search / graph_context / graph_callers / graph_callees / graph_impact. Só leia arquivos (read_file) nas linhas que o grafo apontou. Use grep apenas para texto que não é símbolo.
31
- - Antes de alterar uma função ou arquivo, rode graph_callers / graph_impact e considere o que pode quebrar.
32
- - Após editar muitos arquivos, rode graph_index para atualizar o grafo.
33
- - Ao tomar uma decisão não óbvia, descobrir um bug ou uma convenção, grave com remember (com refs para arquivos/símbolos). Consulte recall no início de tarefas relevantes.
34
- - Edições e comandos shell pedem aprovação do usuário. Prefira edit_file a write_file. Verifique seu trabalho (typecheck/testes) via bash quando existir.
35
- - Para investigações amplas ou sub-tarefas grandes, use delegate: o subagente trabalha em contexto isolado e devolve só o resultado. Vários delegate somente-leitura (explore/plan) no mesmo turno rodam em paralelo. Passe \`focus\` com os símbolos relevantes. Não delegue o que resolve com 1–2 chamadas de graph_*.
36
- - SKILLS: procedimentos reutilizáveis aprendidos neste projeto. Se alguma se aplica à tarefa, chame skill_load ANTES de começar e siga-a. Se ela estiver errada ou incompleta, corrija com skill_save. Ao terminar tarefas complexas, o sistema pode propor novas skills; não crie skill para fatos de uma tarefa única.
37
- - Em tarefas com 3+ passos, mantenha a lista com todo_write: um item in_progress por vez, marcado completed assim que terminar. Leia um arquivo (read_file) antes de editá-lo; se ele mudou desde a leitura, leia de novo.
38
- - Seja conciso. Não repita conteúdo de arquivos na resposta.
39
-
40
- # Ambiente
41
- Diretório do projeto: ${root}
26
+ return `Você é Brito, um agente de engenharia de software que trabalha em CLI. Responda em português do Brasil, de forma direta.
27
+
28
+ # Como trabalhar
29
+ - Você tem um GRAFO DE CONHECIMENTO do projeto (símbolos, chamadas, imports) e uma MEMÓRIA em grafo (notas). ${graphStats}
30
+ - Para entender código: comece por graph_search / graph_context / graph_callers / graph_callees / graph_impact. Só leia arquivos (read_file) nas linhas que o grafo apontou. Use grep apenas para texto que não é símbolo.
31
+ - Antes de alterar uma função ou arquivo, rode graph_callers / graph_impact e considere o que pode quebrar.
32
+ - Após editar muitos arquivos, rode graph_index para atualizar o grafo.
33
+ - Ao tomar uma decisão não óbvia, descobrir um bug ou uma convenção, grave com remember (com refs para arquivos/símbolos). Consulte recall no início de tarefas relevantes.
34
+ - Edições e comandos shell pedem aprovação do usuário. Prefira edit_file a write_file. Verifique seu trabalho (typecheck/testes) via bash quando existir.
35
+ - Para investigações amplas ou sub-tarefas grandes, use delegate: o subagente trabalha em contexto isolado e devolve só o resultado. Vários delegate somente-leitura (explore/plan) no mesmo turno rodam em paralelo. Passe \`focus\` com os símbolos relevantes. Não delegue o que resolve com 1–2 chamadas de graph_*.
36
+ - SKILLS: procedimentos reutilizáveis aprendidos neste projeto. Se alguma se aplica à tarefa, chame skill_load ANTES de começar e siga-a. Se ela estiver errada ou incompleta, corrija com skill_save. Ao terminar tarefas complexas, o sistema pode propor novas skills; não crie skill para fatos de uma tarefa única.
37
+ - Em tarefas com 3+ passos, mantenha a lista com todo_write: um item in_progress por vez, marcado completed assim que terminar. Leia um arquivo (read_file) antes de editá-lo; se ele mudou desde a leitura, leia de novo.
38
+ - Seja conciso. Não repita conteúdo de arquivos na resposta.
39
+
40
+ # Ambiente
41
+ Diretório do projeto: ${root}
42
42
  Data: ${new Date().toISOString().slice(0, 10)}${shellNote()}${skills ? `\n\n# Skills disponíveis (use skill_load)\n${skills}` : ""}${projectDoc}`;
43
43
  }
44
44
  import { listSkills, skillsIndex } from "../skills/store.js";
@@ -68,14 +68,14 @@ export function systemKey(root, planMode, graphEmpty) {
68
68
  return `${planMode}|${graphEmpty}|${skills}`;
69
69
  }
70
70
  export function buildSubagentPrompt(root, def) {
71
- return `${def.prompt}
72
-
73
- # Regras de subagente
74
- - Você foi chamado por outro agente e NÃO vê a conversa dele: tudo que precisa está na tarefa.
75
- - Use o grafo antes de ler arquivos: graph_search, graph_context, graph_callers, graph_callees, graph_impact. Leia só as linhas que ele apontar.
76
- - Sua última mensagem é a única coisa que o chamador recebe. Seja objetivo e use caminhos \`arquivo:linha\`; sem preâmbulo.
77
-
78
- # Ambiente
79
- Diretório do projeto: ${root}
71
+ return `${def.prompt}
72
+
73
+ # Regras de subagente
74
+ - Você foi chamado por outro agente e NÃO vê a conversa dele: tudo que precisa está na tarefa.
75
+ - Use o grafo antes de ler arquivos: graph_search, graph_context, graph_callers, graph_callees, graph_impact. Leia só as linhas que ele apontar.
76
+ - Sua última mensagem é a única coisa que o chamador recebe. Seja objetivo e use caminhos \`arquivo:linha\`; sem preâmbulo.
77
+
78
+ # Ambiente
79
+ Diretório do projeto: ${root}
80
80
  Data: ${new Date().toISOString().slice(0, 10)}${skillsIndex(root) ? `\n\n# Skills disponíveis (use skill_load)\n${skillsIndex(root)}` : ""}`;
81
81
  }
package/dist/cli.js CHANGED
@@ -7,16 +7,19 @@ import "./tools/bash.js";
7
7
  import "./tools/graph.js";
8
8
  import "./tools/delegate.js";
9
9
  import "./tools/plan.js";
10
+ import "./tools/implement.js";
11
+ import "./tools/spec.js";
10
12
  import "./tools/todo.js";
11
13
  import fs from "node:fs";
12
14
  import { randomUUID } from "node:crypto";
13
15
  import { latestSession, listSessions, repairHistory, saveSession, titleOf } from "./agent/sessions.js";
14
16
  import { PLAN_NOTE, perms } from "./permissions.js";
17
+ import { ORCH_NOTE, implStatus, mainTools, orch, orchGuard, restoreOrch } from "./orchestrator.js";
15
18
  import { loadProjectAgents, listAgents } from "./agent/agents.js";
16
19
  import { GraphLockedError, GraphStore, openGraphAfterClaude } from "./graph/store.js";
17
20
  import { indexProject } from "./graph/indexer.js";
18
21
  import { Agent } from "./agent/loop.js";
19
- import { EFFORTS, MODEL_ALIASES, modelHelp, effortLabel, initialEffort, initialModel, loadProfiles, isEffort, loadReasoningDisplay, parseModelArgs, saveEffort, saveModel, saveReasoningDisplay } from "./model.js";
22
+ import { EFFORTS, MODEL_ALIASES, modelHelp, effortLabel, initialEffort, initialModel, loadProfiles, loadSavedOrch, isEffort, loadReasoningDisplay, parseModelArgs, saveEffort, saveImplModel, saveModel, saveOrch, saveReasoningDisplay } from "./model.js";
20
23
  import { hasClaudeCli, historyForFallback, ClaudeSession } from "./agent/claude-cli.js";
21
24
  import { NO_ANTHROPIC, fallbackStatus, learnerModel, openEngine } from "./engine.js";
22
25
  import { activateFallback, chooseModel, cliShouldFallBack, fallbackWarning, newFallbackState, onceRestore, renderFallback, retryPrompt } from "./provider/fallback.js";
@@ -501,6 +504,74 @@ function makeLearner(getTurns, ctx, complete, getModel) {
501
504
  }
502
505
  };
503
506
  }
507
+ /**
508
+ * `/orquestrar [on|off] [modelo <perfil>] [--global]`: o principal planeja e revisa, o perfil implementa pela tool
509
+ * `implement`. `on`/`off` e `modelo` combinam no mesmo comando; `--global` grava o que foi pedido no config
510
+ * (`orchestrator` e `implModel`), para valer ao abrir o Brito de novo.
511
+ */
512
+ function orchestratorCommand(main) {
513
+ const usage = " uso: /orquestrar on|off [modelo <perfil>] [--global] · /orquestrar modelo <perfil> [--global]";
514
+ return {
515
+ desc: "modo orquestrador: o modelo principal planeja e revisa, um perfil (qwen) implementa",
516
+ run: async (args) => {
517
+ const global = args.includes("--global");
518
+ const words = args.filter((a) => !a.startsWith("--"));
519
+ const st = () => implStatus(main(), orch.model, process.env);
520
+ if (!words.length) {
521
+ const s = st();
522
+ ui.print(` ${c.soft("modo")} ${c.muted(`${orch.on ? "ligado" : "desligado"}${loadSavedOrch() ? " · liga ao abrir (salvo)" : ""}`)}`);
523
+ ui.print(` ${c.soft("implementador")} ${c.muted(`${orch.model} · ${s.ready ? `${s.label} ✓ pronto` : `✗ ${s.reason}`}`)}`);
524
+ ui.print(c.muted(usage));
525
+ return;
526
+ }
527
+ const toggle = words.find((w) => w === "on" || w === "off");
528
+ const mi = words.indexOf("modelo");
529
+ const name = mi >= 0 ? words[mi + 1] : undefined;
530
+ const known = new Set([toggle, "modelo", name].filter(Boolean));
531
+ if ((mi >= 0 && !name) || words.some((w) => !known.has(w)) || words.filter((w) => w === "on" || w === "off").length > 1) {
532
+ ui.warn(`argumento desconhecido: ${args.join(" ")}`);
533
+ ui.print(c.muted(usage));
534
+ return;
535
+ }
536
+ // O implementador primeiro: `on modelo X` liga já com o X.
537
+ if (name) {
538
+ const key = name.trim().toLowerCase();
539
+ if (resolveModel(key).kind !== "openai") {
540
+ ui.error(`"${name}" não é um perfil de endpoint em providers no ~/.brito/config.json: o implementador é um perfil (ex.: qwen); o principal se troca com /model`);
541
+ return;
542
+ }
543
+ orch.model = key;
544
+ if (global)
545
+ saveImplModel(key);
546
+ ui.info(`implementador: ${key}${global ? " (salvo)" : ""}`);
547
+ }
548
+ if (toggle === "off") {
549
+ orch.on = false;
550
+ if (global)
551
+ saveOrch(false);
552
+ ui.info(`modo orquestrador desligado${global ? " (salvo)" : ""}: o modelo principal volta a editar`);
553
+ return;
554
+ }
555
+ if (toggle === "on") {
556
+ const s = st();
557
+ if (!s.ready) {
558
+ ui.error(`modo orquestrador não ligou: ${s.reason}`);
559
+ return;
560
+ }
561
+ orch.on = true;
562
+ if (global)
563
+ saveOrch(true);
564
+ ui.info(`modo orquestrador ligado${global ? " (salvo: liga ao abrir o Brito)" : ""}: ${main()} planeja e revisa, ${s.label} implementa`);
565
+ return;
566
+ }
567
+ const s = st();
568
+ if (orch.on && !s.ready) {
569
+ orch.on = false;
570
+ ui.warn(`modo orquestrador desligado: ${s.reason}`);
571
+ }
572
+ },
573
+ };
574
+ }
504
575
  /** Linhas da listagem de sessões (`brito sessoes`, `/sessions`): id, quando, motor e título. */
505
576
  function sessionLines(root, current) {
506
577
  return listSessions(root).slice(0, 20).map((s) => {
@@ -549,10 +620,10 @@ async function buildApiBackend(root, model, io, o = {}) {
549
620
  const noteCount = Number(notes);
550
621
  const ctx = { root, graph, ask: (q, opts) => io.ask(q, opts), sync, dirty: false, loadedSkills: o.loadedSkills ?? new Set() };
551
622
  const fallback = o.fallback ?? newFallbackState(model);
552
- // Refeito só quando muda o modo plano, o grafo deixa de estar vazio ou o conjunto de skills muda: um system
553
- // estável mantém o cache de prompt da conversa (veja memoSystem).
554
- const system = memoSystem(() => systemKey(root, perms.mode === "plan", symCount + noteCount === 0), () => buildSystemPrompt(root, graphStatsLine(symCount, noteCount, "now"), skillsIndex(root)) + (perms.mode === "plan" ? PLAN_NOTE : ""));
555
- const agent = new Agent(system, ctx, { model, fallback });
623
+ // Refeito só quando muda o modo plano, o modo orquestrador, o grafo deixa de estar vazio ou o conjunto de skills muda:
624
+ // um system estável mantém o cache de prompt da conversa (veja memoSystem).
625
+ const system = memoSystem(() => systemKey(root, perms.mode === "plan", symCount + noteCount === 0) + `|${orch.on}`, () => buildSystemPrompt(root, graphStatsLine(symCount, noteCount, "now"), skillsIndex(root)) + (perms.mode === "plan" ? PLAN_NOTE : "") + (orch.on ? ORCH_NOTE : ""));
626
+ const agent = new Agent(system, ctx, { model, fallback, tools: mainTools });
556
627
  if (o.messages)
557
628
  agent.messages = o.messages;
558
629
  if (o.effort)
@@ -581,6 +652,7 @@ async function buildApiBackend(root, model, io, o = {}) {
581
652
  };
582
653
  const turn = async (text, signal) => {
583
654
  ui.turnStart();
655
+ orchGuard(agent.model, (m) => ui.warn(m)); // `/model qwen` ou a troca do claude CLI para o qwen
584
656
  let ok = true;
585
657
  try {
586
658
  await agent.send(text, signal);
@@ -616,6 +688,7 @@ async function buildApiBackend(root, model, io, o = {}) {
616
688
  "/model": modelCmd.cmd,
617
689
  "/fallback": fallbackCommand(fallback, () => agent.model, o.engineLabel ?? ((m) => (resolveModel(m).kind === "openai" ? "endpoint" : "API Anthropic"))),
618
690
  "/reasoning": reasoningCommand(() => agent.effort, (e) => { agent.effort = e; }),
691
+ "/orquestrar": orchestratorCommand(() => agent.model),
619
692
  "/tools": { desc: "ferramentas disponíveis", run: async () => { for (const t of allTools())
620
693
  ui.print(` ${ui.toolLabel(t.name)}${c.dim(t.dangerous ? " (pede aprovação)" : "")}`); } },
621
694
  "/context": {
@@ -781,6 +854,7 @@ async function buildClaudeBackend(root, io, sw, model, openGraph, resume) {
781
854
  "/model": modelCmd.cmd,
782
855
  "/fallback": fallbackCommand(fallback, () => session.model, () => "claude CLI · assinatura"),
783
856
  "/effort": effortCommand(() => session.effort, (e) => { session.effort = e; }),
857
+ "/orquestrar": orchestratorCommand(() => session.requestedModel || session.model),
784
858
  "/reasoning": reasoningCommand(() => session.effort, (e) => { session.effort = e; }),
785
859
  "/tools": { desc: "ferramentas do grafo/memória/skills", run: async () => { for (const n of mcpTools)
786
860
  ui.print(` ${ui.toolLabel(n)}`); } },
@@ -835,6 +909,7 @@ async function runViaClaude(root, promptParts, yes, model, resume) {
835
909
  }
836
910
  sw = switchableBackend(claude.backend);
837
911
  ui.info("modo claude CLI: usa o login da sua conta; tools do grafo/memória/skills via MCP.");
912
+ restoreOrch(claude.session.requestedModel || claude.session.model, (m) => ui.warn(m), (m) => ui.info(m));
838
913
  await runRepl(sw, {
839
914
  root, yes, startedAt, io,
840
915
  banner: {
@@ -956,6 +1031,7 @@ program
956
1031
  return;
957
1032
  }
958
1033
  sw = switchableBackend(api.backend);
1034
+ restoreOrch(api.agent.model, (m) => ui.warn(m), (m) => ui.info(m));
959
1035
  await runRepl(sw, {
960
1036
  root, yes: !!o.yes, startedAt, io,
961
1037
  banner: {
@@ -0,0 +1,93 @@
1
+ import os from "node:os";
2
+ import path from "node:path";
3
+ import { listAgents } from "../agent/agents.js";
4
+ import { GraphStore } from "../graph/store.js";
5
+ import { indexProject } from "../graph/indexer.js";
6
+ import { approvePending, deleteSkill, getSkill, listBuiltins, listPending, listSkills, promoteSkill, rejectPending } from "../skills/store.js";
7
+ /**
8
+ * Dados dos painéis do app (Skills, Agentes, Grafo, Memória). O GraphStore só segura o banco enquanto há consulta
9
+ * (leitura abre somente leitura), então um segundo handle aqui convive com o do motor e com o servidor MCP do claude.
10
+ */
11
+ export class Panels {
12
+ root;
13
+ graphRoot;
14
+ isHome;
15
+ graph = null;
16
+ constructor(root, graphRoot, isHome) {
17
+ this.root = root;
18
+ this.graphRoot = graphRoot;
19
+ this.isHome = isHome;
20
+ }
21
+ async g() { return (this.graph ??= await GraphStore.open(this.graphRoot)); }
22
+ async close() { await this.graph?.close(); this.graph = null; }
23
+ skills() {
24
+ const root = this.root;
25
+ const active = [...listBuiltins(root), ...listSkills(root)].map((s) => ({
26
+ name: s.name, description: s.description, scope: s.scope, status: s.status, invoke: s.invoke, uses: s.uses, builtin: !!s.builtin, origin: s.origin,
27
+ }));
28
+ const pending = listPending(root).map((p) => ({ name: p.name, description: p.description, reason: p.reason, scope: p.scope }));
29
+ return { active, pending };
30
+ }
31
+ skill(name) {
32
+ const s = getSkill(this.root, name);
33
+ return s ? { name: s.name, description: s.description, body: s.body, refs: s.refs, scope: s.scope, model: s.model, wins: s.wins, losses: s.losses } : null;
34
+ }
35
+ skillAction(op, name) {
36
+ const root = this.root;
37
+ switch (op) {
38
+ case "approve":
39
+ approvePending(root, name);
40
+ return `skill ${name} aprovada`;
41
+ case "reject": return rejectPending(root, name) ? `proposta ${name} descartada` : `sem proposta ${name}`;
42
+ case "delete": return deleteSkill(root, name) ? `skill ${name} removida (foi para .trash)` : `não foi possível remover ${name}`;
43
+ case "promote":
44
+ promoteSkill(root, name);
45
+ return `skill ${name} agora é global`;
46
+ default: throw new Error(`ação desconhecida: ${op}`);
47
+ }
48
+ }
49
+ agents() {
50
+ return listAgents().map((a) => ({ name: a.name, description: a.description, tools: a.tools === "all" ? "todas" : a.tools.join(", "), model: a.model ?? "", source: a.source }));
51
+ }
52
+ async symbols(q) {
53
+ const g = await this.g();
54
+ const rows = q
55
+ ? await g.run("MATCH (s:Symbol) WHERE toLower(s.name) CONTAINS $q RETURN s.id AS id, s.name AS name, s.kind AS kind, s.file AS file, s.startLine AS line ORDER BY s.name LIMIT 40", { q: q.toLowerCase() })
56
+ : await g.run("MATCH (s:Symbol) RETURN s.id AS id, s.name AS name, s.kind AS kind, s.file AS file, s.startLine AS line ORDER BY s.file, s.startLine LIMIT 40");
57
+ const stats = await this.stats();
58
+ return { rows, stats };
59
+ }
60
+ async stats() {
61
+ const g = await this.g();
62
+ const n = async (c) => Number((await g.run(c))[0]?.n ?? 0);
63
+ return { symbols: await n("MATCH (s:Symbol) RETURN count(s) AS n"), files: await n("MATCH (f:File) RETURN count(f) AS n"), notes: await n("MATCH (s:Note) RETURN count(s) AS n") };
64
+ }
65
+ async symbol(id) {
66
+ const g = await this.g();
67
+ const cols = "x.id AS id, x.name AS name, x.kind AS kind, x.file AS file, x.startLine AS line";
68
+ const callers = await g.run(`MATCH (x:Symbol)-[:CALLS]->(s:Symbol {id: $id}) RETURN ${cols} LIMIT 30`, { id });
69
+ const callees = await g.run(`MATCH (s:Symbol {id: $id})-[:CALLS]->(x:Symbol) RETURN ${cols} LIMIT 30`, { id });
70
+ return { callers, callees };
71
+ }
72
+ async reindex() {
73
+ if (this.isHome)
74
+ return { symbols: 0, skipped: true };
75
+ const g = await this.g();
76
+ const r = await indexProject(this.root, g);
77
+ return { symbols: r.symbols };
78
+ }
79
+ async notes(q) {
80
+ const g = await this.g();
81
+ const base = "MATCH (n:Note) ";
82
+ const rows = q
83
+ ? await g.run(base + "WHERE toLower(n.text) CONTAINS $q OR toLower(n.kind) CONTAINS $q RETURN n.id AS id, n.kind AS kind, n.text AS text, n.refs AS refs, n.ts AS ts ORDER BY n.ts DESC LIMIT 100", { q: q.toLowerCase() })
84
+ : await g.run(base + "RETURN n.id AS id, n.kind AS kind, n.text AS text, n.refs AS refs, n.ts AS ts ORDER BY n.ts DESC LIMIT 100");
85
+ return rows.map((r) => ({ ...r, ts: Number(r.ts), refs: r.refs ?? [] }));
86
+ }
87
+ async deleteNote(id) {
88
+ const g = await this.g();
89
+ await g.run("MATCH (n:Note {id: $id}) DELETE n", { id });
90
+ return "nota removida";
91
+ }
92
+ }
93
+ export const graphRootFor = (root) => (path.resolve(root) === path.resolve(os.homedir()) ? path.join(os.homedir(), ".brito", "home-graph") : root);
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Contrato entre o app desktop e `brito gui-serve` (JSON em linhas; stdout leva o prefixo "@@").
3
+ * A janela compara `PROTOCOL` com o `hello` do backend e avisa se o app e a CLI estão desalinhados.
4
+ */
5
+ export const PROTOCOL = 2;