brito-cli 0.7.5 → 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.
- package/README.md +54 -4
- package/README.pt-BR.md +54 -4
- package/dist/agent/approval.js +19 -2
- package/dist/agent/claude-cli.js +10 -4
- package/dist/agent/loop.js +38 -9
- package/dist/agent/prompt.js +27 -1
- package/dist/agent/sessions.js +98 -0
- package/dist/cli.js +296 -26
- package/dist/desktop/panels.js +93 -0
- package/dist/desktop/protocol.js +5 -0
- package/dist/desktop/serve.js +191 -23
- package/dist/engine.js +28 -4
- package/dist/local/digest.js +202 -0
- package/dist/local/slm.js +218 -0
- package/dist/mcp/server.js +36 -3
- package/dist/model.js +21 -0
- package/dist/orchestrator.js +131 -0
- package/dist/permissions.js +4 -0
- package/dist/provider/anthropic-params.js +32 -0
- package/dist/provider/anthropic.js +13 -9
- package/dist/provider/fallback.js +67 -8
- package/dist/provider/index.js +4 -0
- package/dist/provider/local.js +88 -0
- package/dist/provider/resolve.js +8 -0
- package/dist/repl.js +49 -6
- package/dist/rules.js +102 -0
- package/dist/skills/builtin.js +89 -0
- package/dist/skills/describe.js +3 -2
- package/dist/skills/learn.js +2 -2
- package/dist/skills/store.js +64 -8
- package/dist/skills/trial.js +3 -3
- package/dist/tools/delegate.js +31 -22
- package/dist/tools/digest.js +29 -0
- package/dist/tools/fs.js +51 -8
- package/dist/tools/implement.js +50 -0
- package/dist/tools/registry.js +10 -1
- package/dist/tools/skills.js +10 -6
- package/dist/tools/spec.js +47 -0
- package/dist/tools/todo.js +22 -0
- package/dist/ui/ui.js +12 -0
- package/dist/util/todos.js +4 -0
- package/package.json +10 -6
- package/dist/setup.js +0 -110
package/dist/cli.js
CHANGED
|
@@ -7,26 +7,35 @@ 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";
|
|
12
|
+
import "./tools/todo.js";
|
|
10
13
|
import fs from "node:fs";
|
|
14
|
+
import { randomUUID } from "node:crypto";
|
|
15
|
+
import { latestSession, listSessions, repairHistory, saveSession, titleOf } from "./agent/sessions.js";
|
|
11
16
|
import { PLAN_NOTE, perms } from "./permissions.js";
|
|
17
|
+
import { ORCH_NOTE, implStatus, mainTools, orch, orchGuard, restoreOrch } from "./orchestrator.js";
|
|
12
18
|
import { loadProjectAgents, listAgents } from "./agent/agents.js";
|
|
13
19
|
import { GraphLockedError, GraphStore, openGraphAfterClaude } from "./graph/store.js";
|
|
14
20
|
import { indexProject } from "./graph/indexer.js";
|
|
15
21
|
import { Agent } from "./agent/loop.js";
|
|
16
|
-
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";
|
|
17
23
|
import { hasClaudeCli, historyForFallback, ClaudeSession } from "./agent/claude-cli.js";
|
|
18
|
-
import {
|
|
24
|
+
import { NO_ANTHROPIC, fallbackStatus, learnerModel, openEngine } from "./engine.js";
|
|
19
25
|
import { activateFallback, chooseModel, cliShouldFallBack, fallbackWarning, newFallbackState, onceRestore, renderFallback, retryPrompt } from "./provider/fallback.js";
|
|
20
26
|
import { compactAtFor, contextWindowFor, resolveModel } from "./provider/resolve.js";
|
|
21
27
|
import { startApprovalServer } from "./agent/approval.js";
|
|
28
|
+
import { addRule, loadRules, projectSettingsPath } from "./rules.js";
|
|
22
29
|
import { serveMcp } from "./mcp/server.js";
|
|
23
30
|
import "./tools/skills.js";
|
|
24
|
-
import
|
|
31
|
+
import "./tools/digest.js";
|
|
32
|
+
import { adviseSkill, approvePending, lintSkill, listBuiltins, listPending, listSkills, rejectPending, skillsIndex, staleRefs } from "./skills/store.js";
|
|
25
33
|
import { applyProposal, autoConsolidate, describeProposal, proposeConsolidation, pruneCandidates } from "./skills/consolidate.js";
|
|
26
34
|
import { defaultComplete, recordOutcomes, reflect, shouldReflect } from "./skills/learn.js";
|
|
27
35
|
import { expireTrials, skillsMode } from "./skills/trial.js";
|
|
28
36
|
import { claudeCliComplete } from "./skills/learn-cli.js";
|
|
29
|
-
import {
|
|
37
|
+
import { pullModel, removeModel, slmStatus, withSlm } from "./local/slm.js";
|
|
38
|
+
import { buildSystemPrompt, graphStatsLine, memoSystem, systemKey } from "./agent/prompt.js";
|
|
30
39
|
import { allTools } from "./tools/registry.js";
|
|
31
40
|
import { ui, c, fmtTokens } from "./ui/ui.js";
|
|
32
41
|
import { runRepl, switchableBackend } from "./repl.js";
|
|
@@ -64,6 +73,55 @@ program
|
|
|
64
73
|
else
|
|
65
74
|
console.log(`OK: ${r.found} nota(s) no banco antigo, ${r.added} copiada(s), ${r.skipped} já existiam. O .brito/graph.kuzu foi mantido; apague-o quando quiser.`);
|
|
66
75
|
});
|
|
76
|
+
program
|
|
77
|
+
.command("sessoes")
|
|
78
|
+
.alias("sessions")
|
|
79
|
+
.description("Lista as sessões salvas deste projeto (retome com brito --resume <id> ou brito -c)")
|
|
80
|
+
.option("-d, --dir <dir>", "raiz do projeto")
|
|
81
|
+
.action((o) => {
|
|
82
|
+
const lines = sessionLines(rootOf(o));
|
|
83
|
+
console.log(lines.length ? lines.join("\n") : c.muted("nenhuma sessão salva neste projeto"));
|
|
84
|
+
});
|
|
85
|
+
program
|
|
86
|
+
.command("slm [acao]")
|
|
87
|
+
.description("SLM local embutido (llama.cpp): status | pull (baixa o modelo) | rm (apaga). Usado na reflexão de skills; desligue com BRITO_SLM=off")
|
|
88
|
+
.action(async (acao) => {
|
|
89
|
+
const st = await slmStatus();
|
|
90
|
+
if (acao === "pull") {
|
|
91
|
+
if (!st.lib) {
|
|
92
|
+
console.error(c.err("node-llama-cpp não está instalado (dependência opcional). Rode: npm install node-llama-cpp"));
|
|
93
|
+
process.exit(1);
|
|
94
|
+
}
|
|
95
|
+
if (st.file) {
|
|
96
|
+
console.log(c.ok("✦ ") + `modelo já baixado: ${st.file}`);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
console.log(`baixando ${st.uri} (alguns GB; é retomável com Ctrl+C e rodar de novo)…`);
|
|
100
|
+
let last = -1;
|
|
101
|
+
const file = await pullModel((done, total) => {
|
|
102
|
+
const pct = total ? Math.floor((done / total) * 100) : 0;
|
|
103
|
+
if (pct >= last + 5 || pct === 100) {
|
|
104
|
+
last = pct;
|
|
105
|
+
process.stdout.write(`\r${pct}% (${(done / 1e9).toFixed(2)}/${(total / 1e9).toFixed(2)} GB)`);
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
console.log("\n" + c.ok("✦ ") + `pronto: ${file}`);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (acao === "rm") {
|
|
112
|
+
const f = await removeModel();
|
|
113
|
+
console.log(f ? c.ok("✦ ") + `removido: ${f}` : c.muted("nenhum modelo baixado"));
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (acao && acao !== "status") {
|
|
117
|
+
console.error(c.err("uso: brito slm [status|pull|rm]"));
|
|
118
|
+
process.exit(1);
|
|
119
|
+
}
|
|
120
|
+
console.log(` modelo ${st.uri}`);
|
|
121
|
+
console.log(` biblioteca ${st.lib ? c.ok("node-llama-cpp ok") : c.warn("node-llama-cpp ausente (npm install node-llama-cpp)")}`);
|
|
122
|
+
console.log(` arquivo ${st.file ?? c.muted("não baixado: brito slm pull")}`);
|
|
123
|
+
console.log(` estado ${st.disabled ? c.warn("desligado (BRITO_SLM=off)") : st.ready ? c.ok("ativo: a reflexão de skills roda localmente") : c.muted("inativo")}`);
|
|
124
|
+
});
|
|
67
125
|
program
|
|
68
126
|
.command("query <cypher>")
|
|
69
127
|
.description("Executa Cypher no grafo")
|
|
@@ -282,6 +340,37 @@ function modelCommand(get, set, note, veto = () => null, divert) {
|
|
|
282
340
|
};
|
|
283
341
|
}
|
|
284
342
|
/** `/skills`: lista, revisa propostas pendentes e consolida a biblioteca. Vale para os dois backends. */
|
|
343
|
+
/** `/permissions` lista as regras; `allow <regra>` / `deny <regra>` grava no `.brito/settings.json` do projeto. */
|
|
344
|
+
const permissionsCommand = (root) => ({
|
|
345
|
+
desc: "regras de permissão; allow <regra> | deny <regra>",
|
|
346
|
+
run: async ([sub, ...rest]) => {
|
|
347
|
+
if (sub === "allow" || sub === "deny") {
|
|
348
|
+
const rule = rest.join(" ").trim();
|
|
349
|
+
if (!rule) {
|
|
350
|
+
ui.print(c.muted(` uso: /permissions ${sub} bash(npm test:*) · /permissions ${sub} edit_file(src/**)`));
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
try {
|
|
354
|
+
addRule(root, sub, rule);
|
|
355
|
+
ui.info(`${sub}: ${rule} → ${path.relative(root, projectSettingsPath(root))}`);
|
|
356
|
+
}
|
|
357
|
+
catch (e) {
|
|
358
|
+
ui.error(e?.message ?? String(e));
|
|
359
|
+
}
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
const r = loadRules(root);
|
|
363
|
+
if (!r.allow.length && !r.deny.length) {
|
|
364
|
+
ui.print(c.muted(" nenhuma regra. Ex.: /permissions allow bash(npm test:*) · /permissions deny bash(git push:*)"));
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
for (const a of r.allow)
|
|
368
|
+
ui.print(` ${c.soft("allow")} ${a}`);
|
|
369
|
+
for (const d of r.deny)
|
|
370
|
+
ui.print(` ${c.soft("deny ")} ${d}`);
|
|
371
|
+
ui.print(c.muted(" ~/.brito/settings.json · .brito/settings.json · .brito/settings.local.json"));
|
|
372
|
+
},
|
|
373
|
+
});
|
|
285
374
|
const skillsCommand = (root, io, llm) => ({
|
|
286
375
|
desc: "lista skills; pending | approve <nome> | reject <nome> | consolidate",
|
|
287
376
|
run: async ([sub, name]) => {
|
|
@@ -331,7 +420,7 @@ const skillsCommand = (root, io, llm) => ({
|
|
|
331
420
|
}
|
|
332
421
|
ui.print(` ${c.soft(p.name)} ${c.muted(`(${p.scope}) ${p.description}`)}\n${p.body.split("\n").map((l) => ` │ ${l}`).join("\n")}`);
|
|
333
422
|
// Avisos do lint (ex.: comando destrutivo) ficam à vista de quem aprova, não só de quem propõe.
|
|
334
|
-
for (const w of lintSkill(p).warnings)
|
|
423
|
+
for (const w of [...lintSkill(p).warnings, ...adviseSkill(p)])
|
|
335
424
|
ui.print(c.warn(` ⚠ ${w}`));
|
|
336
425
|
if (!(await io.ask(`aprovar a skill ${name}?`, { force: true }))) {
|
|
337
426
|
ui.info("mantida como pendente");
|
|
@@ -363,8 +452,10 @@ const skillsCommand = (root, io, llm) => ({
|
|
|
363
452
|
const skills = listSkills(root);
|
|
364
453
|
for (const s of skills) {
|
|
365
454
|
const stale = staleRefs(root, s);
|
|
366
|
-
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(", ")}`) : ""}`);
|
|
455
|
+
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}✗` : ""}${s.invoke === "manual" ? " · manual (/" + s.name + ")" : ""}${s.model ? ` · modelo: ${s.model}` : ""}${s.lastLoss ? ` · último tropeço: ${s.lastLoss}` : ""} · último uso ${s.lastUsed} · ${s.description}`)}${stale.length ? " " + c.warn(`⚠ refs obsoletas: ${stale.join(", ")}`) : ""}`);
|
|
367
456
|
}
|
|
457
|
+
for (const s of listBuiltins(root))
|
|
458
|
+
ui.print(` ${c.soft("/" + s.name)} ${c.muted(`(embutida) ${s.description}`)}`);
|
|
368
459
|
const prune = pruneCandidates(skills);
|
|
369
460
|
if (prune.length)
|
|
370
461
|
ui.info(`${prune.length} candidata(s) a poda (sem uso há mais de 30 dias): /skills consolidate`);
|
|
@@ -413,6 +504,106 @@ function makeLearner(getTurns, ctx, complete, getModel) {
|
|
|
413
504
|
}
|
|
414
505
|
};
|
|
415
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
|
+
}
|
|
575
|
+
/** Linhas da listagem de sessões (`brito sessoes`, `/sessions`): id, quando, motor e título. */
|
|
576
|
+
function sessionLines(root, current) {
|
|
577
|
+
return listSessions(root).slice(0, 20).map((s) => {
|
|
578
|
+
const when = s.updatedAt.slice(0, 16).replace("T", " ");
|
|
579
|
+
const mark = s.id === current ? c.accent(" ●") : "";
|
|
580
|
+
return ` ${c.soft(s.id.slice(0, 8))} ${c.muted(when)} ${c.dim(s.engine === "claude" ? "claude" : "api ")} ${s.title || c.dim("(sem título)")}${mark}`;
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
const sessionsCommand = (root, current) => ({
|
|
584
|
+
desc: "sessões salvas deste projeto (retome com brito --resume <id>)",
|
|
585
|
+
run: async () => {
|
|
586
|
+
const lines = sessionLines(root, current());
|
|
587
|
+
if (!lines.length) {
|
|
588
|
+
ui.print(c.muted(" nenhuma sessão salva ainda"));
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
for (const l of lines)
|
|
592
|
+
ui.print(l);
|
|
593
|
+
ui.print(c.muted(" retome com: brito --resume <id> · brito -c retoma a mais recente"));
|
|
594
|
+
},
|
|
595
|
+
});
|
|
596
|
+
/** `--resume` aceita o id inteiro ou o começo dele (como a listagem mostra). */
|
|
597
|
+
function findSession(root, idOrPrefix) {
|
|
598
|
+
const all = listSessions(root);
|
|
599
|
+
const exact = all.find((s) => s.id === idOrPrefix);
|
|
600
|
+
if (exact)
|
|
601
|
+
return exact;
|
|
602
|
+
const hits = all.filter((s) => s.id.startsWith(idOrPrefix));
|
|
603
|
+
if (hits.length > 1)
|
|
604
|
+
throw new Error(`o prefixo ${idOrPrefix} casa com ${hits.length} sessões; use mais caracteres`);
|
|
605
|
+
return hits[0] ?? null;
|
|
606
|
+
}
|
|
416
607
|
/**
|
|
417
608
|
* Monta o loop próprio (`Agent`) e o `ReplBackend` do modo API. Serve à abertura normal e à troca para o qwen quando
|
|
418
609
|
* o claude CLI cai; `perms.mode` é global, então o modo de permissão da sessão segue igual.
|
|
@@ -429,20 +620,39 @@ async function buildApiBackend(root, model, io, o = {}) {
|
|
|
429
620
|
const noteCount = Number(notes);
|
|
430
621
|
const ctx = { root, graph, ask: (q, opts) => io.ask(q, opts), sync, dirty: false, loadedSkills: o.loadedSkills ?? new Set() };
|
|
431
622
|
const fallback = o.fallback ?? newFallbackState(model);
|
|
432
|
-
//
|
|
433
|
-
|
|
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 });
|
|
434
627
|
if (o.messages)
|
|
435
628
|
agent.messages = o.messages;
|
|
436
629
|
if (o.effort)
|
|
437
630
|
agent.effort = o.effort;
|
|
631
|
+
let sessionId = o.session?.id ?? randomUUID();
|
|
632
|
+
if (o.session?.messages)
|
|
633
|
+
agent.messages = repairHistory(o.session.messages);
|
|
634
|
+
if (o.session?.todos)
|
|
635
|
+
ctx.todos = o.session.todos;
|
|
438
636
|
ctx.model = () => agent.model; // subagentes seguem o modelo vivo
|
|
439
637
|
ui.reasoning = loadReasoningDisplay();
|
|
440
638
|
const learnModel = () => learnerModel(agent.model, process.env);
|
|
441
639
|
loadProjectAgents(root);
|
|
442
640
|
/** Depois de tarefas complexas (ou uso de skill), propõe criar/melhorar skills. */
|
|
443
|
-
const learn = makeLearner(() => agent.recentTurns, ctx, defaultComplete, learnModel);
|
|
641
|
+
const learn = makeLearner(() => agent.recentTurns, ctx, withSlm(defaultComplete), learnModel);
|
|
642
|
+
/** Grava a conversa a cada turno (também nos que falharam): `brito -c` retoma de onde parou. */
|
|
643
|
+
const persist = () => {
|
|
644
|
+
if (!agent.messages.length)
|
|
645
|
+
return;
|
|
646
|
+
try {
|
|
647
|
+
saveSession(root, { id: sessionId, engine: "api", model: agent.model, title: titleOf(agent.messages), messages: agent.messages, todos: ctx.todos });
|
|
648
|
+
}
|
|
649
|
+
catch (e) {
|
|
650
|
+
ui.warn(`não consegui salvar a sessão: ${e?.message ?? e}`);
|
|
651
|
+
}
|
|
652
|
+
};
|
|
444
653
|
const turn = async (text, signal) => {
|
|
445
654
|
ui.turnStart();
|
|
655
|
+
orchGuard(agent.model, (m) => ui.warn(m)); // `/model qwen` ou a troca do claude CLI para o qwen
|
|
446
656
|
let ok = true;
|
|
447
657
|
try {
|
|
448
658
|
await agent.send(text, signal);
|
|
@@ -453,6 +663,7 @@ async function buildApiBackend(root, model, io, o = {}) {
|
|
|
453
663
|
}
|
|
454
664
|
finally {
|
|
455
665
|
ui.turnEnd(ok);
|
|
666
|
+
persist();
|
|
456
667
|
}
|
|
457
668
|
return ok;
|
|
458
669
|
};
|
|
@@ -463,18 +674,21 @@ async function buildApiBackend(root, model, io, o = {}) {
|
|
|
463
674
|
const modelCmd = modelCommand(() => agent.model, (m) => { agent.model = m; chooseModel(fallback, m); }, "(troque com /model; subagentes explore usam BRITO_EXPLORE_MODEL)", noApiKey, o.divert);
|
|
464
675
|
const backend = {
|
|
465
676
|
model: () => agent.model,
|
|
677
|
+
fallback: () => fallback,
|
|
466
678
|
contextTokens: () => agent.contextTokens,
|
|
467
679
|
usage: () => agent.usage,
|
|
468
680
|
get ctxWindow() { return contextWindowFor(agent.model); },
|
|
469
681
|
cost: () => (Number.isFinite(PRICE_IN) && Number.isFinite(PRICE_OUT) ? (agent.usage.input * PRICE_IN + agent.usage.output * PRICE_OUT) / 1e6 : undefined),
|
|
470
682
|
turn,
|
|
471
683
|
afterTurn: async () => { modelCmd.restoreOnce(fallback.active); await learn(); },
|
|
472
|
-
|
|
684
|
+
// `/clear` começa outra sessão: a anterior continua salva e pode ser retomada com --resume.
|
|
685
|
+
reset: () => { agent.messages = []; agent.contextTokens = 0; agent.recentTurns = []; agent.unsentTurns = []; agent.lastTurn = null; ctx.todos = undefined; sessionId = randomUUID(); },
|
|
473
686
|
onShell: () => { ctx.dirty = true; }, // `!cmd` pode ter mudado o código
|
|
474
687
|
commands: {
|
|
475
688
|
"/model": modelCmd.cmd,
|
|
476
689
|
"/fallback": fallbackCommand(fallback, () => agent.model, o.engineLabel ?? ((m) => (resolveModel(m).kind === "openai" ? "endpoint" : "API Anthropic"))),
|
|
477
690
|
"/reasoning": reasoningCommand(() => agent.effort, (e) => { agent.effort = e; }),
|
|
691
|
+
"/orquestrar": orchestratorCommand(() => agent.model),
|
|
478
692
|
"/tools": { desc: "ferramentas disponíveis", run: async () => { for (const t of allTools())
|
|
479
693
|
ui.print(` ${ui.toolLabel(t.name)}${c.dim(t.dangerous ? " (pede aprovação)" : "")}`); } },
|
|
480
694
|
"/context": {
|
|
@@ -487,11 +701,14 @@ async function buildApiBackend(root, model, io, o = {}) {
|
|
|
487
701
|
},
|
|
488
702
|
"/index": { desc: "reconstrói o grafo do zero", run: async () => { ui.info("reconstruindo grafo…"); ui.print(c.muted(JSON.stringify(await indexProject(root, graph, { full: true })))); } },
|
|
489
703
|
"/learn": { desc: "extrai skills do último turno agora", run: async () => { await learn(true); } },
|
|
490
|
-
"/skills": skillsCommand(root, io, { complete: defaultComplete, model: learnModel }),
|
|
704
|
+
"/skills": skillsCommand(root, io, { complete: withSlm(defaultComplete), model: learnModel }),
|
|
705
|
+
"/permissions": permissionsCommand(root),
|
|
706
|
+
"/todos": { desc: "lista de tarefas da sessão", run: async () => { ctx.todos?.length ? ui.printTodos(ctx.todos) : ui.print(c.muted(" sem lista de tarefas nesta sessão")); } },
|
|
707
|
+
"/sessions": sessionsCommand(root, () => sessionId),
|
|
491
708
|
},
|
|
492
709
|
close: async () => { await graph.close(); },
|
|
493
710
|
};
|
|
494
|
-
return { backend, agent, graph, learn, symbols: () => symCount, notes: noteCount };
|
|
711
|
+
return { backend, agent, graph, learn, symbols: () => symCount, notes: noteCount, sessionId: () => sessionId };
|
|
495
712
|
}
|
|
496
713
|
/** `--once` não troca de motor: o turno seguinte rodaria no outro motor e a volta exigiria trocar de novo. */
|
|
497
714
|
const onceNoSwitch = (m) => `o ${m} roda no claude CLI: --once não troca de motor; use /model ${m}`;
|
|
@@ -501,8 +718,8 @@ let approvalServer = null;
|
|
|
501
718
|
* Motor claude CLI. `sw` é o backend do REPL: `/model <perfil>` troca para o modo API (com a conversa do claude) e
|
|
502
719
|
* `/model <claude>` lá volta para cá. Também serve ao prompt único, que não troca de motor pelo `/model`.
|
|
503
720
|
*/
|
|
504
|
-
async function buildClaudeBackend(root, io, sw, model, openGraph) {
|
|
505
|
-
approvalServer ??= startApprovalServer({ ask: (q, opts) => io.ask(q, opts) });
|
|
721
|
+
async function buildClaudeBackend(root, io, sw, model, openGraph, resume) {
|
|
722
|
+
approvalServer ??= startApprovalServer({ ask: (q, opts) => io.ask(q, opts) }, root);
|
|
506
723
|
// O banco aceita um único processo com acesso de escrita: indexa, lê as stats e solta o banco para o servidor MCP.
|
|
507
724
|
// `openGraph`: o do modo API, que é fechado aqui só depois de indexar (se falhar antes, continua aberto para ele).
|
|
508
725
|
const graph = openGraph ?? await GraphStore.open(root);
|
|
@@ -517,9 +734,22 @@ async function buildClaudeBackend(root, io, sw, model, openGraph) {
|
|
|
517
734
|
mcpCommand: { command: process.execPath, args: [...process.execArgv, process.argv[1], "mcp-serve", "-d", root] },
|
|
518
735
|
});
|
|
519
736
|
loadProjectAgents(root);
|
|
737
|
+
if (resume)
|
|
738
|
+
session.resume(resume.id);
|
|
739
|
+
// Só o id fica aqui: a conversa em si o claude CLI guarda, e o `--resume` dele a recupera.
|
|
740
|
+
let title = resume?.title ?? "";
|
|
741
|
+
const persist = (text) => {
|
|
742
|
+
title ||= text.replace(/\s+/g, " ").trim().slice(0, 80);
|
|
743
|
+
try {
|
|
744
|
+
saveSession(root, { id: session.sessionId, engine: "claude", model: session.model, title });
|
|
745
|
+
}
|
|
746
|
+
catch (e) {
|
|
747
|
+
ui.warn(`não consegui salvar a sessão: ${e?.message ?? e}`);
|
|
748
|
+
}
|
|
749
|
+
};
|
|
520
750
|
// O grafo já foi solto para o servidor MCP; skill_save só usa o store de skills, então `graph` não é tocado.
|
|
521
751
|
const learnCtx = { root, graph: undefined, ask: (q, opts) => io.ask(q, opts), sync: async () => { }, dirty: false, loadedSkills: session.loadedSkills };
|
|
522
|
-
const learn = makeLearner(() => session.recentTurns, learnCtx, claudeCliComplete, () => process.env.BRITO_LEARN_MODEL ?? ""); // "" = modelo padrão do claude
|
|
752
|
+
const learn = makeLearner(() => session.recentTurns, learnCtx, withSlm(claudeCliComplete), () => process.env.BRITO_LEARN_MODEL ?? ""); // "" = modelo padrão do claude
|
|
523
753
|
if (model) {
|
|
524
754
|
session.requestedModel = resolveModel(model).wireModel;
|
|
525
755
|
session.model = session.requestedModel;
|
|
@@ -568,7 +798,7 @@ async function buildClaudeBackend(root, io, sw, model, openGraph) {
|
|
|
568
798
|
const api = await openApi(status.name, status.label, { engineLabel: () => "claude CLI · assinatura" });
|
|
569
799
|
if (!api)
|
|
570
800
|
return null;
|
|
571
|
-
activateFallback(fallback, `${f.kind} (${f.message})
|
|
801
|
+
activateFallback(fallback, `${f.kind} (${f.message})`, Date.now(), undefined, status.name);
|
|
572
802
|
return { api, retry: () => api.backend.turn(retryPrompt(text), signal) };
|
|
573
803
|
};
|
|
574
804
|
/** `/model <perfil>`: perfis de endpoint rodam no loop próprio do Brito, então o motor troca para o modo API. */
|
|
@@ -600,12 +830,15 @@ async function buildClaudeBackend(root, io, sw, model, openGraph) {
|
|
|
600
830
|
const mcpTools = allTools().map((t) => t.name).filter((n) => /^(graph_|skill_)/.test(n) || n === "remember" || n === "recall");
|
|
601
831
|
const backend = {
|
|
602
832
|
model: () => session.model,
|
|
833
|
+
fallback: () => fallback,
|
|
603
834
|
contextTokens: () => session.contextTokens,
|
|
604
835
|
usage: () => session.usage,
|
|
605
836
|
cost: () => (session.costUsd > 0 ? session.costUsd : undefined),
|
|
606
837
|
get ctxWindow() { return session.contextWindow ?? CTX_WINDOW; },
|
|
607
838
|
turn: async (text, signal) => {
|
|
608
|
-
|
|
839
|
+
const ok = await session.send(text);
|
|
840
|
+
persist(text);
|
|
841
|
+
if (ok)
|
|
609
842
|
return true;
|
|
610
843
|
const fb = await switchToQwen(text, signal);
|
|
611
844
|
if (!fb)
|
|
@@ -616,11 +849,12 @@ async function buildClaudeBackend(root, io, sw, model, openGraph) {
|
|
|
616
849
|
},
|
|
617
850
|
afterTurn: async () => { modelCmd.restoreOnce(); await learn(); },
|
|
618
851
|
abort: () => session.abort(),
|
|
619
|
-
reset: () => session.reset(),
|
|
852
|
+
reset: () => { session.reset(); title = ""; },
|
|
620
853
|
commands: {
|
|
621
854
|
"/model": modelCmd.cmd,
|
|
622
855
|
"/fallback": fallbackCommand(fallback, () => session.model, () => "claude CLI · assinatura"),
|
|
623
856
|
"/effort": effortCommand(() => session.effort, (e) => { session.effort = e; }),
|
|
857
|
+
"/orquestrar": orchestratorCommand(() => session.requestedModel || session.model),
|
|
624
858
|
"/reasoning": reasoningCommand(() => session.effort, (e) => { session.effort = e; }),
|
|
625
859
|
"/tools": { desc: "ferramentas do grafo/memória/skills", run: async () => { for (const n of mcpTools)
|
|
626
860
|
ui.print(` ${ui.toolLabel(n)}`); } },
|
|
@@ -635,13 +869,15 @@ async function buildClaudeBackend(root, io, sw, model, openGraph) {
|
|
|
635
869
|
},
|
|
636
870
|
"/context": unavailable,
|
|
637
871
|
"/learn": { desc: "extrai skills do último turno agora", run: async () => { await learn(true); } },
|
|
638
|
-
"/skills": skillsCommand(root, io, { complete: claudeCliComplete, model: () => process.env.BRITO_LEARN_MODEL ?? "" }),
|
|
872
|
+
"/skills": skillsCommand(root, io, { complete: withSlm(claudeCliComplete), model: () => process.env.BRITO_LEARN_MODEL ?? "" }),
|
|
873
|
+
"/permissions": permissionsCommand(root),
|
|
874
|
+
"/sessions": sessionsCommand(root, () => session.sessionId),
|
|
639
875
|
},
|
|
640
876
|
close: async () => { },
|
|
641
877
|
};
|
|
642
878
|
return { backend, session, learn, switchToQwen, modelCmd, syms: Number(syms), notes: Number(notes), mcpTools };
|
|
643
879
|
}
|
|
644
|
-
async function runViaClaude(root, promptParts, yes, model) {
|
|
880
|
+
async function runViaClaude(root, promptParts, yes, model, resume) {
|
|
645
881
|
const startedAt = Date.now();
|
|
646
882
|
if (yes)
|
|
647
883
|
perms.mode = "bypass";
|
|
@@ -652,7 +888,7 @@ async function runViaClaude(root, promptParts, yes, model) {
|
|
|
652
888
|
await offerNoteMigration(root, (q) => migrationAsk(q), (m) => ui.info(m));
|
|
653
889
|
}
|
|
654
890
|
let sw;
|
|
655
|
-
const claude = await buildClaudeBackend(root, io, () => sw, model);
|
|
891
|
+
const claude = await buildClaudeBackend(root, io, () => sw, model, undefined, resume);
|
|
656
892
|
if (promptParts.length) {
|
|
657
893
|
const text = promptParts.join(" ");
|
|
658
894
|
if (await claude.session.send(text)) {
|
|
@@ -673,6 +909,7 @@ async function runViaClaude(root, promptParts, yes, model) {
|
|
|
673
909
|
}
|
|
674
910
|
sw = switchableBackend(claude.backend);
|
|
675
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));
|
|
676
913
|
await runRepl(sw, {
|
|
677
914
|
root, yes, startedAt, io,
|
|
678
915
|
banner: {
|
|
@@ -687,6 +924,8 @@ program
|
|
|
687
924
|
.option("-d, --dir <dir>", "raiz do projeto")
|
|
688
925
|
.option("-y, --yes", "aprovar edições e comandos automaticamente")
|
|
689
926
|
.option("-m, --model <nome>", "modelo ou perfil de endpoint (ex.: sonnet, qwen)")
|
|
927
|
+
.option("-c, --continue", "retoma a sessão mais recente deste projeto")
|
|
928
|
+
.option("-r, --resume <id>", "retoma uma sessão salva (id ou começo dele; veja `brito sessoes`)")
|
|
690
929
|
.action(async (promptParts, o) => {
|
|
691
930
|
// Trava anti-recursão: um brito iniciado por dentro de um claude que o próprio brito lançou (BRITO_DEPTH) pode abrir
|
|
692
931
|
// outro claude, e assim por diante. Dois níveis bastam; mais que isso é laço de processos.
|
|
@@ -696,17 +935,47 @@ program
|
|
|
696
935
|
}
|
|
697
936
|
for (const e of loadProfiles().errors)
|
|
698
937
|
ui.warn(e);
|
|
699
|
-
|
|
938
|
+
let resume = null;
|
|
939
|
+
if (o.resume || o.continue) {
|
|
940
|
+
try {
|
|
941
|
+
resume = o.resume ? findSession(rootOf(o), o.resume) : latestSession(rootOf(o));
|
|
942
|
+
}
|
|
943
|
+
catch (e) {
|
|
944
|
+
console.error(c.err(e?.message ?? String(e)));
|
|
945
|
+
process.exit(1);
|
|
946
|
+
}
|
|
947
|
+
if (!resume) {
|
|
948
|
+
console.error(c.err(o.resume ? `sessão não encontrada: ${o.resume} (veja \`brito sessoes\`)` : "nenhuma sessão salva neste projeto"));
|
|
949
|
+
process.exit(1);
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
const startModel = o.model ?? (resume?.engine === "api" ? resume.model : undefined) ?? initialModel();
|
|
700
953
|
const engine = openEngine(startModel, process.env, hasClaudeCli);
|
|
701
954
|
if (engine.kind === "error") {
|
|
702
955
|
console.error(c.err(engine.message));
|
|
703
956
|
process.exit(1);
|
|
704
957
|
}
|
|
958
|
+
// A sessão é retomada no motor que a gravou: a conversa de uma está guardada onde a outra não alcança.
|
|
959
|
+
if (resume?.engine === "claude") {
|
|
960
|
+
if (!hasClaudeCli()) {
|
|
961
|
+
console.error(c.err("esta sessão foi feita no claude CLI, que não está instalado aqui"));
|
|
962
|
+
process.exit(1);
|
|
963
|
+
}
|
|
964
|
+
ui.info(`retomando: ${resume.title || resume.id}`);
|
|
965
|
+
await runViaClaude(rootOf(o), promptParts, !!o.yes, o.model, resume);
|
|
966
|
+
return;
|
|
967
|
+
}
|
|
968
|
+
if (resume && engine.kind === "claude") {
|
|
969
|
+
console.error(c.err(`esta sessão foi feita no modo API (${resume.model}); sem ANTHROPIC_API_KEY ela só abre num perfil de endpoint: brito --resume ${resume.id.slice(0, 8)} --model qwen`));
|
|
970
|
+
process.exit(1);
|
|
971
|
+
}
|
|
705
972
|
if (engine.kind === "claude") {
|
|
706
973
|
await runViaClaude(rootOf(o), promptParts, !!o.yes, o.model);
|
|
707
974
|
return;
|
|
708
975
|
}
|
|
709
976
|
const root = rootOf(o);
|
|
977
|
+
if (resume)
|
|
978
|
+
ui.info(`retomando: ${resume.title || resume.id} (${resume.messages?.length ?? 0} mensagens)`);
|
|
710
979
|
const startedAt = Date.now();
|
|
711
980
|
{
|
|
712
981
|
const { offerNoteMigration } = await import("./graph/migrate.js");
|
|
@@ -717,12 +986,12 @@ program
|
|
|
717
986
|
perms.mode = "bypass";
|
|
718
987
|
const io = { ask: async (q, _opts) => perms.mode === "bypass" || plainAsk(q) };
|
|
719
988
|
// Claude sem conta nenhuma, com o fallback pronto: a sessão já nasce no qwen (com aviso, nunca em silêncio).
|
|
720
|
-
let opening = {};
|
|
989
|
+
let opening = resume ? { session: resume } : {};
|
|
721
990
|
if (engine.kind === "fallback") {
|
|
722
991
|
ui.warn(`⚠ ${NO_ANTHROPIC} (sem ANTHROPIC_API_KEY e sem o claude CLI) — abrindo no ${engine.label}. /fallback para detalhes.`);
|
|
723
992
|
const fallback = newFallbackState(startModel);
|
|
724
|
-
activateFallback(fallback, NO_ANTHROPIC);
|
|
725
|
-
opening = { fallback, engineLabel: () => NO_ANTHROPIC };
|
|
993
|
+
activateFallback(fallback, NO_ANTHROPIC, Date.now(), undefined, engine.name);
|
|
994
|
+
opening = { ...opening, fallback, engineLabel: () => NO_ANTHROPIC };
|
|
726
995
|
}
|
|
727
996
|
let sw;
|
|
728
997
|
// Sem ANTHROPIC_API_KEY o Claude só roda no claude CLI: `/model <claude>` troca o motor em vez de recusar.
|
|
@@ -754,7 +1023,7 @@ program
|
|
|
754
1023
|
await claude.modelCmd.cmd.run([m, `--${scope}`]);
|
|
755
1024
|
return true;
|
|
756
1025
|
};
|
|
757
|
-
const api = await buildApiBackend(root, engine.kind === "fallback" ?
|
|
1026
|
+
const api = await buildApiBackend(root, engine.kind === "fallback" ? engine.name : startModel, io, opening);
|
|
758
1027
|
if (promptParts.length) {
|
|
759
1028
|
if (await api.backend.turn(promptParts.join(" "), new AbortController().signal))
|
|
760
1029
|
await api.learn();
|
|
@@ -762,6 +1031,7 @@ program
|
|
|
762
1031
|
return;
|
|
763
1032
|
}
|
|
764
1033
|
sw = switchableBackend(api.backend);
|
|
1034
|
+
restoreOrch(api.agent.model, (m) => ui.warn(m), (m) => ui.info(m));
|
|
765
1035
|
await runRepl(sw, {
|
|
766
1036
|
root, yes: !!o.yes, startedAt, io,
|
|
767
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);
|