brito-cli 0.7.5 → 0.8.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/.brito/agents/reviewer.md +7 -7
- package/LICENSE +75 -75
- package/README.md +298 -259
- package/README.pt-BR.md +301 -262
- package/dist/agent/approval.js +19 -2
- package/dist/agent/claude-cli.js +1 -1
- package/dist/agent/loop.js +30 -7
- package/dist/agent/prompt.js +51 -25
- package/dist/agent/sessions.js +98 -0
- package/dist/cli.js +219 -25
- package/dist/desktop/serve.js +34 -11
- package/dist/engine.js +28 -4
- package/dist/graph/migrate.js +6 -6
- package/dist/local/digest.js +202 -0
- package/dist/local/slm.js +218 -0
- package/dist/mcp/server.js +3 -2
- package/dist/permissions.js +3 -3
- 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 +44 -5
- package/dist/rules.js +102 -0
- package/dist/skills/builtin.js +89 -0
- package/dist/skills/consolidate.js +7 -7
- package/dist/skills/describe.js +3 -2
- package/dist/skills/learn.js +23 -23
- package/dist/skills/store.js +64 -8
- package/dist/skills/trial.js +3 -3
- package/dist/tools/delegate.js +2 -1
- package/dist/tools/digest.js +29 -0
- package/dist/tools/fs.js +51 -8
- package/dist/tools/registry.js +10 -1
- package/dist/tools/skills.js +10 -6
- package/dist/tools/todo.js +22 -0
- package/dist/ui/ui.js +12 -0
- package/dist/util/todos.js +4 -0
- package/package.json +80 -77
- package/dist/setup.js +0 -110
package/dist/cli.js
CHANGED
|
@@ -7,7 +7,10 @@ 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/todo.js";
|
|
10
11
|
import fs from "node:fs";
|
|
12
|
+
import { randomUUID } from "node:crypto";
|
|
13
|
+
import { latestSession, listSessions, repairHistory, saveSession, titleOf } from "./agent/sessions.js";
|
|
11
14
|
import { PLAN_NOTE, perms } from "./permissions.js";
|
|
12
15
|
import { loadProjectAgents, listAgents } from "./agent/agents.js";
|
|
13
16
|
import { GraphLockedError, GraphStore, openGraphAfterClaude } from "./graph/store.js";
|
|
@@ -15,18 +18,21 @@ import { indexProject } from "./graph/indexer.js";
|
|
|
15
18
|
import { Agent } from "./agent/loop.js";
|
|
16
19
|
import { EFFORTS, MODEL_ALIASES, modelHelp, effortLabel, initialEffort, initialModel, loadProfiles, isEffort, loadReasoningDisplay, parseModelArgs, saveEffort, saveModel, saveReasoningDisplay } from "./model.js";
|
|
17
20
|
import { hasClaudeCli, historyForFallback, ClaudeSession } from "./agent/claude-cli.js";
|
|
18
|
-
import {
|
|
21
|
+
import { NO_ANTHROPIC, fallbackStatus, learnerModel, openEngine } from "./engine.js";
|
|
19
22
|
import { activateFallback, chooseModel, cliShouldFallBack, fallbackWarning, newFallbackState, onceRestore, renderFallback, retryPrompt } from "./provider/fallback.js";
|
|
20
23
|
import { compactAtFor, contextWindowFor, resolveModel } from "./provider/resolve.js";
|
|
21
24
|
import { startApprovalServer } from "./agent/approval.js";
|
|
25
|
+
import { addRule, loadRules, projectSettingsPath } from "./rules.js";
|
|
22
26
|
import { serveMcp } from "./mcp/server.js";
|
|
23
27
|
import "./tools/skills.js";
|
|
24
|
-
import
|
|
28
|
+
import "./tools/digest.js";
|
|
29
|
+
import { adviseSkill, approvePending, lintSkill, listBuiltins, listPending, listSkills, rejectPending, skillsIndex, staleRefs } from "./skills/store.js";
|
|
25
30
|
import { applyProposal, autoConsolidate, describeProposal, proposeConsolidation, pruneCandidates } from "./skills/consolidate.js";
|
|
26
31
|
import { defaultComplete, recordOutcomes, reflect, shouldReflect } from "./skills/learn.js";
|
|
27
32
|
import { expireTrials, skillsMode } from "./skills/trial.js";
|
|
28
33
|
import { claudeCliComplete } from "./skills/learn-cli.js";
|
|
29
|
-
import {
|
|
34
|
+
import { pullModel, removeModel, slmStatus, withSlm } from "./local/slm.js";
|
|
35
|
+
import { buildSystemPrompt, graphStatsLine, memoSystem, systemKey } from "./agent/prompt.js";
|
|
30
36
|
import { allTools } from "./tools/registry.js";
|
|
31
37
|
import { ui, c, fmtTokens } from "./ui/ui.js";
|
|
32
38
|
import { runRepl, switchableBackend } from "./repl.js";
|
|
@@ -64,6 +70,55 @@ program
|
|
|
64
70
|
else
|
|
65
71
|
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
72
|
});
|
|
73
|
+
program
|
|
74
|
+
.command("sessoes")
|
|
75
|
+
.alias("sessions")
|
|
76
|
+
.description("Lista as sessões salvas deste projeto (retome com brito --resume <id> ou brito -c)")
|
|
77
|
+
.option("-d, --dir <dir>", "raiz do projeto")
|
|
78
|
+
.action((o) => {
|
|
79
|
+
const lines = sessionLines(rootOf(o));
|
|
80
|
+
console.log(lines.length ? lines.join("\n") : c.muted("nenhuma sessão salva neste projeto"));
|
|
81
|
+
});
|
|
82
|
+
program
|
|
83
|
+
.command("slm [acao]")
|
|
84
|
+
.description("SLM local embutido (llama.cpp): status | pull (baixa o modelo) | rm (apaga). Usado na reflexão de skills; desligue com BRITO_SLM=off")
|
|
85
|
+
.action(async (acao) => {
|
|
86
|
+
const st = await slmStatus();
|
|
87
|
+
if (acao === "pull") {
|
|
88
|
+
if (!st.lib) {
|
|
89
|
+
console.error(c.err("node-llama-cpp não está instalado (dependência opcional). Rode: npm install node-llama-cpp"));
|
|
90
|
+
process.exit(1);
|
|
91
|
+
}
|
|
92
|
+
if (st.file) {
|
|
93
|
+
console.log(c.ok("✦ ") + `modelo já baixado: ${st.file}`);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
console.log(`baixando ${st.uri} (alguns GB; é retomável com Ctrl+C e rodar de novo)…`);
|
|
97
|
+
let last = -1;
|
|
98
|
+
const file = await pullModel((done, total) => {
|
|
99
|
+
const pct = total ? Math.floor((done / total) * 100) : 0;
|
|
100
|
+
if (pct >= last + 5 || pct === 100) {
|
|
101
|
+
last = pct;
|
|
102
|
+
process.stdout.write(`\r${pct}% (${(done / 1e9).toFixed(2)}/${(total / 1e9).toFixed(2)} GB)`);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
console.log("\n" + c.ok("✦ ") + `pronto: ${file}`);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (acao === "rm") {
|
|
109
|
+
const f = await removeModel();
|
|
110
|
+
console.log(f ? c.ok("✦ ") + `removido: ${f}` : c.muted("nenhum modelo baixado"));
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (acao && acao !== "status") {
|
|
114
|
+
console.error(c.err("uso: brito slm [status|pull|rm]"));
|
|
115
|
+
process.exit(1);
|
|
116
|
+
}
|
|
117
|
+
console.log(` modelo ${st.uri}`);
|
|
118
|
+
console.log(` biblioteca ${st.lib ? c.ok("node-llama-cpp ok") : c.warn("node-llama-cpp ausente (npm install node-llama-cpp)")}`);
|
|
119
|
+
console.log(` arquivo ${st.file ?? c.muted("não baixado: brito slm pull")}`);
|
|
120
|
+
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")}`);
|
|
121
|
+
});
|
|
67
122
|
program
|
|
68
123
|
.command("query <cypher>")
|
|
69
124
|
.description("Executa Cypher no grafo")
|
|
@@ -282,6 +337,37 @@ function modelCommand(get, set, note, veto = () => null, divert) {
|
|
|
282
337
|
};
|
|
283
338
|
}
|
|
284
339
|
/** `/skills`: lista, revisa propostas pendentes e consolida a biblioteca. Vale para os dois backends. */
|
|
340
|
+
/** `/permissions` lista as regras; `allow <regra>` / `deny <regra>` grava no `.brito/settings.json` do projeto. */
|
|
341
|
+
const permissionsCommand = (root) => ({
|
|
342
|
+
desc: "regras de permissão; allow <regra> | deny <regra>",
|
|
343
|
+
run: async ([sub, ...rest]) => {
|
|
344
|
+
if (sub === "allow" || sub === "deny") {
|
|
345
|
+
const rule = rest.join(" ").trim();
|
|
346
|
+
if (!rule) {
|
|
347
|
+
ui.print(c.muted(` uso: /permissions ${sub} bash(npm test:*) · /permissions ${sub} edit_file(src/**)`));
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
try {
|
|
351
|
+
addRule(root, sub, rule);
|
|
352
|
+
ui.info(`${sub}: ${rule} → ${path.relative(root, projectSettingsPath(root))}`);
|
|
353
|
+
}
|
|
354
|
+
catch (e) {
|
|
355
|
+
ui.error(e?.message ?? String(e));
|
|
356
|
+
}
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
const r = loadRules(root);
|
|
360
|
+
if (!r.allow.length && !r.deny.length) {
|
|
361
|
+
ui.print(c.muted(" nenhuma regra. Ex.: /permissions allow bash(npm test:*) · /permissions deny bash(git push:*)"));
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
for (const a of r.allow)
|
|
365
|
+
ui.print(` ${c.soft("allow")} ${a}`);
|
|
366
|
+
for (const d of r.deny)
|
|
367
|
+
ui.print(` ${c.soft("deny ")} ${d}`);
|
|
368
|
+
ui.print(c.muted(" ~/.brito/settings.json · .brito/settings.json · .brito/settings.local.json"));
|
|
369
|
+
},
|
|
370
|
+
});
|
|
285
371
|
const skillsCommand = (root, io, llm) => ({
|
|
286
372
|
desc: "lista skills; pending | approve <nome> | reject <nome> | consolidate",
|
|
287
373
|
run: async ([sub, name]) => {
|
|
@@ -331,7 +417,7 @@ const skillsCommand = (root, io, llm) => ({
|
|
|
331
417
|
}
|
|
332
418
|
ui.print(` ${c.soft(p.name)} ${c.muted(`(${p.scope}) ${p.description}`)}\n${p.body.split("\n").map((l) => ` │ ${l}`).join("\n")}`);
|
|
333
419
|
// 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)
|
|
420
|
+
for (const w of [...lintSkill(p).warnings, ...adviseSkill(p)])
|
|
335
421
|
ui.print(c.warn(` ⚠ ${w}`));
|
|
336
422
|
if (!(await io.ask(`aprovar a skill ${name}?`, { force: true }))) {
|
|
337
423
|
ui.info("mantida como pendente");
|
|
@@ -363,8 +449,10 @@ const skillsCommand = (root, io, llm) => ({
|
|
|
363
449
|
const skills = listSkills(root);
|
|
364
450
|
for (const s of skills) {
|
|
365
451
|
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(", ")}`) : ""}`);
|
|
452
|
+
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
453
|
}
|
|
454
|
+
for (const s of listBuiltins(root))
|
|
455
|
+
ui.print(` ${c.soft("/" + s.name)} ${c.muted(`(embutida) ${s.description}`)}`);
|
|
368
456
|
const prune = pruneCandidates(skills);
|
|
369
457
|
if (prune.length)
|
|
370
458
|
ui.info(`${prune.length} candidata(s) a poda (sem uso há mais de 30 dias): /skills consolidate`);
|
|
@@ -413,6 +501,38 @@ function makeLearner(getTurns, ctx, complete, getModel) {
|
|
|
413
501
|
}
|
|
414
502
|
};
|
|
415
503
|
}
|
|
504
|
+
/** Linhas da listagem de sessões (`brito sessoes`, `/sessions`): id, quando, motor e título. */
|
|
505
|
+
function sessionLines(root, current) {
|
|
506
|
+
return listSessions(root).slice(0, 20).map((s) => {
|
|
507
|
+
const when = s.updatedAt.slice(0, 16).replace("T", " ");
|
|
508
|
+
const mark = s.id === current ? c.accent(" ●") : "";
|
|
509
|
+
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}`;
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
const sessionsCommand = (root, current) => ({
|
|
513
|
+
desc: "sessões salvas deste projeto (retome com brito --resume <id>)",
|
|
514
|
+
run: async () => {
|
|
515
|
+
const lines = sessionLines(root, current());
|
|
516
|
+
if (!lines.length) {
|
|
517
|
+
ui.print(c.muted(" nenhuma sessão salva ainda"));
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
for (const l of lines)
|
|
521
|
+
ui.print(l);
|
|
522
|
+
ui.print(c.muted(" retome com: brito --resume <id> · brito -c retoma a mais recente"));
|
|
523
|
+
},
|
|
524
|
+
});
|
|
525
|
+
/** `--resume` aceita o id inteiro ou o começo dele (como a listagem mostra). */
|
|
526
|
+
function findSession(root, idOrPrefix) {
|
|
527
|
+
const all = listSessions(root);
|
|
528
|
+
const exact = all.find((s) => s.id === idOrPrefix);
|
|
529
|
+
if (exact)
|
|
530
|
+
return exact;
|
|
531
|
+
const hits = all.filter((s) => s.id.startsWith(idOrPrefix));
|
|
532
|
+
if (hits.length > 1)
|
|
533
|
+
throw new Error(`o prefixo ${idOrPrefix} casa com ${hits.length} sessões; use mais caracteres`);
|
|
534
|
+
return hits[0] ?? null;
|
|
535
|
+
}
|
|
416
536
|
/**
|
|
417
537
|
* Monta o loop próprio (`Agent`) e o `ReplBackend` do modo API. Serve à abertura normal e à troca para o qwen quando
|
|
418
538
|
* o claude CLI cai; `perms.mode` é global, então o modo de permissão da sessão segue igual.
|
|
@@ -429,18 +549,36 @@ async function buildApiBackend(root, model, io, o = {}) {
|
|
|
429
549
|
const noteCount = Number(notes);
|
|
430
550
|
const ctx = { root, graph, ask: (q, opts) => io.ask(q, opts), sync, dirty: false, loadedSkills: o.loadedSkills ?? new Set() };
|
|
431
551
|
const fallback = o.fallback ?? newFallbackState(model);
|
|
432
|
-
//
|
|
433
|
-
|
|
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 });
|
|
434
556
|
if (o.messages)
|
|
435
557
|
agent.messages = o.messages;
|
|
436
558
|
if (o.effort)
|
|
437
559
|
agent.effort = o.effort;
|
|
560
|
+
let sessionId = o.session?.id ?? randomUUID();
|
|
561
|
+
if (o.session?.messages)
|
|
562
|
+
agent.messages = repairHistory(o.session.messages);
|
|
563
|
+
if (o.session?.todos)
|
|
564
|
+
ctx.todos = o.session.todos;
|
|
438
565
|
ctx.model = () => agent.model; // subagentes seguem o modelo vivo
|
|
439
566
|
ui.reasoning = loadReasoningDisplay();
|
|
440
567
|
const learnModel = () => learnerModel(agent.model, process.env);
|
|
441
568
|
loadProjectAgents(root);
|
|
442
569
|
/** Depois de tarefas complexas (ou uso de skill), propõe criar/melhorar skills. */
|
|
443
|
-
const learn = makeLearner(() => agent.recentTurns, ctx, defaultComplete, learnModel);
|
|
570
|
+
const learn = makeLearner(() => agent.recentTurns, ctx, withSlm(defaultComplete), learnModel);
|
|
571
|
+
/** Grava a conversa a cada turno (também nos que falharam): `brito -c` retoma de onde parou. */
|
|
572
|
+
const persist = () => {
|
|
573
|
+
if (!agent.messages.length)
|
|
574
|
+
return;
|
|
575
|
+
try {
|
|
576
|
+
saveSession(root, { id: sessionId, engine: "api", model: agent.model, title: titleOf(agent.messages), messages: agent.messages, todos: ctx.todos });
|
|
577
|
+
}
|
|
578
|
+
catch (e) {
|
|
579
|
+
ui.warn(`não consegui salvar a sessão: ${e?.message ?? e}`);
|
|
580
|
+
}
|
|
581
|
+
};
|
|
444
582
|
const turn = async (text, signal) => {
|
|
445
583
|
ui.turnStart();
|
|
446
584
|
let ok = true;
|
|
@@ -453,6 +591,7 @@ async function buildApiBackend(root, model, io, o = {}) {
|
|
|
453
591
|
}
|
|
454
592
|
finally {
|
|
455
593
|
ui.turnEnd(ok);
|
|
594
|
+
persist();
|
|
456
595
|
}
|
|
457
596
|
return ok;
|
|
458
597
|
};
|
|
@@ -463,13 +602,15 @@ async function buildApiBackend(root, model, io, o = {}) {
|
|
|
463
602
|
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
603
|
const backend = {
|
|
465
604
|
model: () => agent.model,
|
|
605
|
+
fallback: () => fallback,
|
|
466
606
|
contextTokens: () => agent.contextTokens,
|
|
467
607
|
usage: () => agent.usage,
|
|
468
608
|
get ctxWindow() { return contextWindowFor(agent.model); },
|
|
469
609
|
cost: () => (Number.isFinite(PRICE_IN) && Number.isFinite(PRICE_OUT) ? (agent.usage.input * PRICE_IN + agent.usage.output * PRICE_OUT) / 1e6 : undefined),
|
|
470
610
|
turn,
|
|
471
611
|
afterTurn: async () => { modelCmd.restoreOnce(fallback.active); await learn(); },
|
|
472
|
-
|
|
612
|
+
// `/clear` começa outra sessão: a anterior continua salva e pode ser retomada com --resume.
|
|
613
|
+
reset: () => { agent.messages = []; agent.contextTokens = 0; agent.recentTurns = []; agent.unsentTurns = []; agent.lastTurn = null; ctx.todos = undefined; sessionId = randomUUID(); },
|
|
473
614
|
onShell: () => { ctx.dirty = true; }, // `!cmd` pode ter mudado o código
|
|
474
615
|
commands: {
|
|
475
616
|
"/model": modelCmd.cmd,
|
|
@@ -487,11 +628,14 @@ async function buildApiBackend(root, model, io, o = {}) {
|
|
|
487
628
|
},
|
|
488
629
|
"/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
630
|
"/learn": { desc: "extrai skills do último turno agora", run: async () => { await learn(true); } },
|
|
490
|
-
"/skills": skillsCommand(root, io, { complete: defaultComplete, model: learnModel }),
|
|
631
|
+
"/skills": skillsCommand(root, io, { complete: withSlm(defaultComplete), model: learnModel }),
|
|
632
|
+
"/permissions": permissionsCommand(root),
|
|
633
|
+
"/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")); } },
|
|
634
|
+
"/sessions": sessionsCommand(root, () => sessionId),
|
|
491
635
|
},
|
|
492
636
|
close: async () => { await graph.close(); },
|
|
493
637
|
};
|
|
494
|
-
return { backend, agent, graph, learn, symbols: () => symCount, notes: noteCount };
|
|
638
|
+
return { backend, agent, graph, learn, symbols: () => symCount, notes: noteCount, sessionId: () => sessionId };
|
|
495
639
|
}
|
|
496
640
|
/** `--once` não troca de motor: o turno seguinte rodaria no outro motor e a volta exigiria trocar de novo. */
|
|
497
641
|
const onceNoSwitch = (m) => `o ${m} roda no claude CLI: --once não troca de motor; use /model ${m}`;
|
|
@@ -501,8 +645,8 @@ let approvalServer = null;
|
|
|
501
645
|
* Motor claude CLI. `sw` é o backend do REPL: `/model <perfil>` troca para o modo API (com a conversa do claude) e
|
|
502
646
|
* `/model <claude>` lá volta para cá. Também serve ao prompt único, que não troca de motor pelo `/model`.
|
|
503
647
|
*/
|
|
504
|
-
async function buildClaudeBackend(root, io, sw, model, openGraph) {
|
|
505
|
-
approvalServer ??= startApprovalServer({ ask: (q, opts) => io.ask(q, opts) });
|
|
648
|
+
async function buildClaudeBackend(root, io, sw, model, openGraph, resume) {
|
|
649
|
+
approvalServer ??= startApprovalServer({ ask: (q, opts) => io.ask(q, opts) }, root);
|
|
506
650
|
// O banco aceita um único processo com acesso de escrita: indexa, lê as stats e solta o banco para o servidor MCP.
|
|
507
651
|
// `openGraph`: o do modo API, que é fechado aqui só depois de indexar (se falhar antes, continua aberto para ele).
|
|
508
652
|
const graph = openGraph ?? await GraphStore.open(root);
|
|
@@ -517,9 +661,22 @@ async function buildClaudeBackend(root, io, sw, model, openGraph) {
|
|
|
517
661
|
mcpCommand: { command: process.execPath, args: [...process.execArgv, process.argv[1], "mcp-serve", "-d", root] },
|
|
518
662
|
});
|
|
519
663
|
loadProjectAgents(root);
|
|
664
|
+
if (resume)
|
|
665
|
+
session.resume(resume.id);
|
|
666
|
+
// Só o id fica aqui: a conversa em si o claude CLI guarda, e o `--resume` dele a recupera.
|
|
667
|
+
let title = resume?.title ?? "";
|
|
668
|
+
const persist = (text) => {
|
|
669
|
+
title ||= text.replace(/\s+/g, " ").trim().slice(0, 80);
|
|
670
|
+
try {
|
|
671
|
+
saveSession(root, { id: session.sessionId, engine: "claude", model: session.model, title });
|
|
672
|
+
}
|
|
673
|
+
catch (e) {
|
|
674
|
+
ui.warn(`não consegui salvar a sessão: ${e?.message ?? e}`);
|
|
675
|
+
}
|
|
676
|
+
};
|
|
520
677
|
// O grafo já foi solto para o servidor MCP; skill_save só usa o store de skills, então `graph` não é tocado.
|
|
521
678
|
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
|
|
679
|
+
const learn = makeLearner(() => session.recentTurns, learnCtx, withSlm(claudeCliComplete), () => process.env.BRITO_LEARN_MODEL ?? ""); // "" = modelo padrão do claude
|
|
523
680
|
if (model) {
|
|
524
681
|
session.requestedModel = resolveModel(model).wireModel;
|
|
525
682
|
session.model = session.requestedModel;
|
|
@@ -568,7 +725,7 @@ async function buildClaudeBackend(root, io, sw, model, openGraph) {
|
|
|
568
725
|
const api = await openApi(status.name, status.label, { engineLabel: () => "claude CLI · assinatura" });
|
|
569
726
|
if (!api)
|
|
570
727
|
return null;
|
|
571
|
-
activateFallback(fallback, `${f.kind} (${f.message})
|
|
728
|
+
activateFallback(fallback, `${f.kind} (${f.message})`, Date.now(), undefined, status.name);
|
|
572
729
|
return { api, retry: () => api.backend.turn(retryPrompt(text), signal) };
|
|
573
730
|
};
|
|
574
731
|
/** `/model <perfil>`: perfis de endpoint rodam no loop próprio do Brito, então o motor troca para o modo API. */
|
|
@@ -600,12 +757,15 @@ async function buildClaudeBackend(root, io, sw, model, openGraph) {
|
|
|
600
757
|
const mcpTools = allTools().map((t) => t.name).filter((n) => /^(graph_|skill_)/.test(n) || n === "remember" || n === "recall");
|
|
601
758
|
const backend = {
|
|
602
759
|
model: () => session.model,
|
|
760
|
+
fallback: () => fallback,
|
|
603
761
|
contextTokens: () => session.contextTokens,
|
|
604
762
|
usage: () => session.usage,
|
|
605
763
|
cost: () => (session.costUsd > 0 ? session.costUsd : undefined),
|
|
606
764
|
get ctxWindow() { return session.contextWindow ?? CTX_WINDOW; },
|
|
607
765
|
turn: async (text, signal) => {
|
|
608
|
-
|
|
766
|
+
const ok = await session.send(text);
|
|
767
|
+
persist(text);
|
|
768
|
+
if (ok)
|
|
609
769
|
return true;
|
|
610
770
|
const fb = await switchToQwen(text, signal);
|
|
611
771
|
if (!fb)
|
|
@@ -616,7 +776,7 @@ async function buildClaudeBackend(root, io, sw, model, openGraph) {
|
|
|
616
776
|
},
|
|
617
777
|
afterTurn: async () => { modelCmd.restoreOnce(); await learn(); },
|
|
618
778
|
abort: () => session.abort(),
|
|
619
|
-
reset: () => session.reset(),
|
|
779
|
+
reset: () => { session.reset(); title = ""; },
|
|
620
780
|
commands: {
|
|
621
781
|
"/model": modelCmd.cmd,
|
|
622
782
|
"/fallback": fallbackCommand(fallback, () => session.model, () => "claude CLI · assinatura"),
|
|
@@ -635,13 +795,15 @@ async function buildClaudeBackend(root, io, sw, model, openGraph) {
|
|
|
635
795
|
},
|
|
636
796
|
"/context": unavailable,
|
|
637
797
|
"/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 ?? "" }),
|
|
798
|
+
"/skills": skillsCommand(root, io, { complete: withSlm(claudeCliComplete), model: () => process.env.BRITO_LEARN_MODEL ?? "" }),
|
|
799
|
+
"/permissions": permissionsCommand(root),
|
|
800
|
+
"/sessions": sessionsCommand(root, () => session.sessionId),
|
|
639
801
|
},
|
|
640
802
|
close: async () => { },
|
|
641
803
|
};
|
|
642
804
|
return { backend, session, learn, switchToQwen, modelCmd, syms: Number(syms), notes: Number(notes), mcpTools };
|
|
643
805
|
}
|
|
644
|
-
async function runViaClaude(root, promptParts, yes, model) {
|
|
806
|
+
async function runViaClaude(root, promptParts, yes, model, resume) {
|
|
645
807
|
const startedAt = Date.now();
|
|
646
808
|
if (yes)
|
|
647
809
|
perms.mode = "bypass";
|
|
@@ -652,7 +814,7 @@ async function runViaClaude(root, promptParts, yes, model) {
|
|
|
652
814
|
await offerNoteMigration(root, (q) => migrationAsk(q), (m) => ui.info(m));
|
|
653
815
|
}
|
|
654
816
|
let sw;
|
|
655
|
-
const claude = await buildClaudeBackend(root, io, () => sw, model);
|
|
817
|
+
const claude = await buildClaudeBackend(root, io, () => sw, model, undefined, resume);
|
|
656
818
|
if (promptParts.length) {
|
|
657
819
|
const text = promptParts.join(" ");
|
|
658
820
|
if (await claude.session.send(text)) {
|
|
@@ -687,6 +849,8 @@ program
|
|
|
687
849
|
.option("-d, --dir <dir>", "raiz do projeto")
|
|
688
850
|
.option("-y, --yes", "aprovar edições e comandos automaticamente")
|
|
689
851
|
.option("-m, --model <nome>", "modelo ou perfil de endpoint (ex.: sonnet, qwen)")
|
|
852
|
+
.option("-c, --continue", "retoma a sessão mais recente deste projeto")
|
|
853
|
+
.option("-r, --resume <id>", "retoma uma sessão salva (id ou começo dele; veja `brito sessoes`)")
|
|
690
854
|
.action(async (promptParts, o) => {
|
|
691
855
|
// Trava anti-recursão: um brito iniciado por dentro de um claude que o próprio brito lançou (BRITO_DEPTH) pode abrir
|
|
692
856
|
// outro claude, e assim por diante. Dois níveis bastam; mais que isso é laço de processos.
|
|
@@ -696,17 +860,47 @@ program
|
|
|
696
860
|
}
|
|
697
861
|
for (const e of loadProfiles().errors)
|
|
698
862
|
ui.warn(e);
|
|
699
|
-
|
|
863
|
+
let resume = null;
|
|
864
|
+
if (o.resume || o.continue) {
|
|
865
|
+
try {
|
|
866
|
+
resume = o.resume ? findSession(rootOf(o), o.resume) : latestSession(rootOf(o));
|
|
867
|
+
}
|
|
868
|
+
catch (e) {
|
|
869
|
+
console.error(c.err(e?.message ?? String(e)));
|
|
870
|
+
process.exit(1);
|
|
871
|
+
}
|
|
872
|
+
if (!resume) {
|
|
873
|
+
console.error(c.err(o.resume ? `sessão não encontrada: ${o.resume} (veja \`brito sessoes\`)` : "nenhuma sessão salva neste projeto"));
|
|
874
|
+
process.exit(1);
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
const startModel = o.model ?? (resume?.engine === "api" ? resume.model : undefined) ?? initialModel();
|
|
700
878
|
const engine = openEngine(startModel, process.env, hasClaudeCli);
|
|
701
879
|
if (engine.kind === "error") {
|
|
702
880
|
console.error(c.err(engine.message));
|
|
703
881
|
process.exit(1);
|
|
704
882
|
}
|
|
883
|
+
// A sessão é retomada no motor que a gravou: a conversa de uma está guardada onde a outra não alcança.
|
|
884
|
+
if (resume?.engine === "claude") {
|
|
885
|
+
if (!hasClaudeCli()) {
|
|
886
|
+
console.error(c.err("esta sessão foi feita no claude CLI, que não está instalado aqui"));
|
|
887
|
+
process.exit(1);
|
|
888
|
+
}
|
|
889
|
+
ui.info(`retomando: ${resume.title || resume.id}`);
|
|
890
|
+
await runViaClaude(rootOf(o), promptParts, !!o.yes, o.model, resume);
|
|
891
|
+
return;
|
|
892
|
+
}
|
|
893
|
+
if (resume && engine.kind === "claude") {
|
|
894
|
+
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`));
|
|
895
|
+
process.exit(1);
|
|
896
|
+
}
|
|
705
897
|
if (engine.kind === "claude") {
|
|
706
898
|
await runViaClaude(rootOf(o), promptParts, !!o.yes, o.model);
|
|
707
899
|
return;
|
|
708
900
|
}
|
|
709
901
|
const root = rootOf(o);
|
|
902
|
+
if (resume)
|
|
903
|
+
ui.info(`retomando: ${resume.title || resume.id} (${resume.messages?.length ?? 0} mensagens)`);
|
|
710
904
|
const startedAt = Date.now();
|
|
711
905
|
{
|
|
712
906
|
const { offerNoteMigration } = await import("./graph/migrate.js");
|
|
@@ -717,12 +911,12 @@ program
|
|
|
717
911
|
perms.mode = "bypass";
|
|
718
912
|
const io = { ask: async (q, _opts) => perms.mode === "bypass" || plainAsk(q) };
|
|
719
913
|
// Claude sem conta nenhuma, com o fallback pronto: a sessão já nasce no qwen (com aviso, nunca em silêncio).
|
|
720
|
-
let opening = {};
|
|
914
|
+
let opening = resume ? { session: resume } : {};
|
|
721
915
|
if (engine.kind === "fallback") {
|
|
722
916
|
ui.warn(`⚠ ${NO_ANTHROPIC} (sem ANTHROPIC_API_KEY e sem o claude CLI) — abrindo no ${engine.label}. /fallback para detalhes.`);
|
|
723
917
|
const fallback = newFallbackState(startModel);
|
|
724
|
-
activateFallback(fallback, NO_ANTHROPIC);
|
|
725
|
-
opening = { fallback, engineLabel: () => NO_ANTHROPIC };
|
|
918
|
+
activateFallback(fallback, NO_ANTHROPIC, Date.now(), undefined, engine.name);
|
|
919
|
+
opening = { ...opening, fallback, engineLabel: () => NO_ANTHROPIC };
|
|
726
920
|
}
|
|
727
921
|
let sw;
|
|
728
922
|
// Sem ANTHROPIC_API_KEY o Claude só roda no claude CLI: `/model <claude>` troca o motor em vez de recusar.
|
|
@@ -754,7 +948,7 @@ program
|
|
|
754
948
|
await claude.modelCmd.cmd.run([m, `--${scope}`]);
|
|
755
949
|
return true;
|
|
756
950
|
};
|
|
757
|
-
const api = await buildApiBackend(root, engine.kind === "fallback" ?
|
|
951
|
+
const api = await buildApiBackend(root, engine.kind === "fallback" ? engine.name : startModel, io, opening);
|
|
758
952
|
if (promptParts.length) {
|
|
759
953
|
if (await api.backend.turn(promptParts.join(" "), new AbortController().signal))
|
|
760
954
|
await api.learn();
|
package/dist/desktop/serve.js
CHANGED
|
@@ -9,15 +9,18 @@ import "../tools/graph.js";
|
|
|
9
9
|
import "../tools/delegate.js";
|
|
10
10
|
import "../tools/plan.js";
|
|
11
11
|
import "../tools/skills.js";
|
|
12
|
+
import "../tools/digest.js";
|
|
13
|
+
import "../tools/todo.js";
|
|
12
14
|
import { Agent } from "../agent/loop.js";
|
|
15
|
+
import { loadSession, repairHistory, saveSession, titleOf } from "../agent/sessions.js";
|
|
13
16
|
import { ClaudeSession, hasClaudeCli, historyForFallback } from "../agent/claude-cli.js";
|
|
14
17
|
import { startApprovalServer } from "../agent/approval.js";
|
|
15
18
|
import { loadProjectAgents } from "../agent/agents.js";
|
|
16
|
-
import { buildSystemPrompt, graphStatsLine } from "../agent/prompt.js";
|
|
19
|
+
import { buildSystemPrompt, graphStatsLine, memoSystem, systemKey } from "../agent/prompt.js";
|
|
17
20
|
import { openGraphAfterClaude } from "../graph/store.js";
|
|
18
21
|
import { indexProject } from "../graph/indexer.js";
|
|
19
22
|
import { initialEffort, initialModel, isEffort, loadProfiles, MODEL_ALIASES } from "../model.js";
|
|
20
|
-
import {
|
|
23
|
+
import { NO_ANTHROPIC, fallbackStatus, openEngine } from "../engine.js";
|
|
21
24
|
import { activateFallback, chooseModel, cliShouldFallBack, fallbackWarning, newFallbackState, renderFallback, retryPrompt } from "../provider/fallback.js";
|
|
22
25
|
import { contextWindowFor, resolveModel } from "../provider/resolve.js";
|
|
23
26
|
import { PLAN_NOTE, perms } from "../permissions.js";
|
|
@@ -89,13 +92,29 @@ export async function serveDesktop(root, resumeId) {
|
|
|
89
92
|
catch { /* sem notas */ }
|
|
90
93
|
const ctx = { root, graph, ask, sync, dirty: false, loadedSkills: o.loadedSkills ?? new Set(), model: () => agent.model };
|
|
91
94
|
const fallback = o.fallback ?? newFallbackState(model);
|
|
92
|
-
const
|
|
95
|
+
const system = memoSystem(() => systemKey(root, perms.mode === "plan", syms + notes === 0), () => buildSystemPrompt(root, graphStatsLine(syms, notes, "now"), skillsIndex(root)) + (perms.mode === "plan" ? PLAN_NOTE : ""));
|
|
96
|
+
const agent = new Agent(system, ctx, { model, sink, fallback });
|
|
93
97
|
if (o.messages)
|
|
94
98
|
agent.messages = o.messages;
|
|
95
99
|
if (o.effort)
|
|
96
100
|
agent.effort = o.effort;
|
|
101
|
+
if (o.session?.messages)
|
|
102
|
+
agent.messages = repairHistory(o.session.messages);
|
|
103
|
+
if (o.session?.todos)
|
|
104
|
+
ctx.todos = o.session.todos;
|
|
97
105
|
let ac = null;
|
|
98
|
-
const id = randomUUID();
|
|
106
|
+
const id = o.session?.id ?? randomUUID();
|
|
107
|
+
// Depois de uma troca a partir do claude CLI quem guarda a conversa é ele (o id é o dele): nada a salvar aqui.
|
|
108
|
+
const persist = () => {
|
|
109
|
+
if (o.back || !agent.messages.length)
|
|
110
|
+
return;
|
|
111
|
+
try {
|
|
112
|
+
saveSession(root, { id, engine: "api", model: agent.model, title: titleOf(agent.messages), messages: agent.messages, todos: ctx.todos });
|
|
113
|
+
}
|
|
114
|
+
catch (e) {
|
|
115
|
+
sink.warn(`não consegui salvar a sessão: ${e?.message ?? e}`);
|
|
116
|
+
}
|
|
117
|
+
};
|
|
99
118
|
return {
|
|
100
119
|
kind: "api", effort: "", fallback,
|
|
101
120
|
// Depois da troca o id segue o do claude: a janela o guarda para retomar a conversa ao reabrir.
|
|
@@ -136,6 +155,7 @@ export async function serveDesktop(root, resumeId) {
|
|
|
136
155
|
finally {
|
|
137
156
|
sink.turnEnd(ok);
|
|
138
157
|
ac = null;
|
|
158
|
+
persist();
|
|
139
159
|
}
|
|
140
160
|
},
|
|
141
161
|
abort: () => ac?.abort(),
|
|
@@ -152,20 +172,23 @@ export async function serveDesktop(root, resumeId) {
|
|
|
152
172
|
};
|
|
153
173
|
};
|
|
154
174
|
if (opening.kind !== "claude") {
|
|
155
|
-
|
|
156
|
-
|
|
175
|
+
// O id que a janela guarda é o da sessão salva em .brito/sessions: reabrir retoma o histórico de verdade.
|
|
176
|
+
const saved = resumeId ? loadSession(root, resumeId) : null;
|
|
177
|
+
const session = saved?.engine === "api" ? saved : undefined;
|
|
178
|
+
if (resumeId && !session)
|
|
179
|
+
emit({ t: "info", msg: "Esta conversa não foi salva no modo API (é de uma versão antiga ou do claude CLI): a tela mostra o que já foi dito, mas o modelo recomeça." });
|
|
157
180
|
if (opening.kind === "fallback") {
|
|
158
181
|
// Claude sem conta nenhuma, com o fallback pronto: a sessão já nasce no qwen (com aviso, nunca em silêncio).
|
|
159
182
|
emit({ t: "warn", msg: `⚠ ${NO_ANTHROPIC} (sem ANTHROPIC_API_KEY e sem o claude CLI) — abrindo no ${opening.label}. Envie /fallback para detalhes.` });
|
|
160
183
|
const fallback = newFallbackState(startModel);
|
|
161
|
-
activateFallback(fallback, NO_ANTHROPIC);
|
|
162
|
-
engine = await buildApiEngine(
|
|
184
|
+
activateFallback(fallback, NO_ANTHROPIC, Date.now(), undefined, opening.name);
|
|
185
|
+
engine = await buildApiEngine(opening.name, { fallback, engineLabel: () => NO_ANTHROPIC, session });
|
|
163
186
|
}
|
|
164
187
|
else
|
|
165
|
-
engine = await buildApiEngine(startModel);
|
|
188
|
+
engine = await buildApiEngine(startModel, { session });
|
|
166
189
|
}
|
|
167
190
|
else {
|
|
168
|
-
const approval = startApprovalServer({ ask });
|
|
191
|
+
const approval = startApprovalServer({ ask }, root);
|
|
169
192
|
closeApproval = approval.close;
|
|
170
193
|
// O banco aceita um único processo com escrita: indexa, lê as stats e solta o banco para o servidor MCP.
|
|
171
194
|
if (!isHome)
|
|
@@ -228,7 +251,7 @@ export async function serveDesktop(root, resumeId) {
|
|
|
228
251
|
sink.error(`não consegui abrir o ${status.label}: ${e?.message ?? e}`);
|
|
229
252
|
return;
|
|
230
253
|
}
|
|
231
|
-
activateFallback(fallback, `${f.kind} (${f.message})
|
|
254
|
+
activateFallback(fallback, `${f.kind} (${f.message})`, Date.now(), undefined, status.name);
|
|
232
255
|
engine = api;
|
|
233
256
|
ready();
|
|
234
257
|
await api.send(retryPrompt(text, resumed));
|
package/dist/engine.js
CHANGED
|
@@ -1,13 +1,35 @@
|
|
|
1
1
|
import { MODEL_ALIASES, loadProfiles } from "./model.js";
|
|
2
2
|
import { missingKeyMessage } from "./provider/openai-compat.js";
|
|
3
|
-
import {
|
|
4
|
-
|
|
3
|
+
import { slmReadySync } from "./local/slm.js";
|
|
4
|
+
import { SLM_MODEL, resolveModel } from "./provider/resolve.js";
|
|
5
|
+
/** O primeiro fallback é sempre este perfil de `providers` (não configurável). */
|
|
5
6
|
export const FALLBACK_PROFILE = "qwen";
|
|
7
|
+
/** Cadeia: Claude → qwen → SLM local (último recurso, sem rede nem conta). */
|
|
8
|
+
export const FALLBACK_CHAIN = [FALLBACK_PROFILE, SLM_MODEL];
|
|
6
9
|
/**
|
|
7
10
|
* Pronto só se o perfil existe, é válido, tem a chave no ambiente e o principal não é ele mesmo. Relê o config a
|
|
8
11
|
* cada chamada, como os perfis.
|
|
9
12
|
*/
|
|
10
|
-
export function fallbackStatus(primary, env) {
|
|
13
|
+
export function fallbackStatus(primary, env, skip = []) {
|
|
14
|
+
const q = skip.includes(FALLBACK_PROFILE) ? { ready: false, reason: "o qwen já foi usado nesta troca" } : qwenStatus(primary, env);
|
|
15
|
+
if (q.ready)
|
|
16
|
+
return q;
|
|
17
|
+
const slm = slmFallbackStatus(primary, env, skip);
|
|
18
|
+
return slm.ready ? slm : q;
|
|
19
|
+
}
|
|
20
|
+
/** O SLM local como fallback: baixado (`brito slm pull`), habilitado e não é o próprio principal. */
|
|
21
|
+
function slmFallbackStatus(primary, env, skip) {
|
|
22
|
+
if (skip.includes(SLM_MODEL))
|
|
23
|
+
return { ready: false, reason: "o SLM já foi usado nesta troca" };
|
|
24
|
+
if (primary.trim().toLowerCase() === SLM_MODEL)
|
|
25
|
+
return { ready: false, reason: "o modelo principal já é o SLM" };
|
|
26
|
+
if (env.BRITO_SLM_FALLBACK === "off")
|
|
27
|
+
return { ready: false, reason: "BRITO_SLM_FALLBACK=off" };
|
|
28
|
+
if (!slmReadySync())
|
|
29
|
+
return { ready: false, reason: "SLM local indisponível (rode `brito slm pull`)" };
|
|
30
|
+
return { ready: true, name: SLM_MODEL, label: resolveModel(SLM_MODEL).label };
|
|
31
|
+
}
|
|
32
|
+
function qwenStatus(primary, env) {
|
|
11
33
|
const { profiles, errors, rejected } = loadProfiles();
|
|
12
34
|
const p = profiles[FALLBACK_PROFILE];
|
|
13
35
|
const label = p?.label;
|
|
@@ -36,6 +58,8 @@ export function pickEngine(model, env, hasCli) {
|
|
|
36
58
|
const why = loadProfiles().errors.find((e) => e.toLowerCase().includes(`"${name}"`)) ?? "";
|
|
37
59
|
return { kind: "error", message: `o perfil "${name}" em ~/.brito/config.json está inválido (${why}); corrija antes de usar` };
|
|
38
60
|
}
|
|
61
|
+
if (r.local)
|
|
62
|
+
return slmReadySync() ? { kind: "api" } : { kind: "error", message: "o SLM local não está pronto: rode `brito slm pull`" };
|
|
39
63
|
if (r.kind === "openai")
|
|
40
64
|
return env[r.profile.apiKeyEnv]?.trim() ? { kind: "api" } : { kind: "error", message: missingKeyMessage(r.profile) };
|
|
41
65
|
if (env.ANTHROPIC_API_KEY)
|
|
@@ -56,7 +80,7 @@ export function openEngine(model, env, hasCli) {
|
|
|
56
80
|
if (e.kind !== "error" || resolveModel(model).kind !== "anthropic" || loadProfiles().rejected.includes(model.trim().toLowerCase()))
|
|
57
81
|
return e;
|
|
58
82
|
const s = fallbackStatus(model, env);
|
|
59
|
-
return s.ready ? { kind: "fallback", label: s.label } : { kind: "error", message: `${e.message}\nFallback (qwen) indisponível: ${s.reason}.` };
|
|
83
|
+
return s.ready ? { kind: "fallback", label: s.label, name: s.name } : { kind: "error", message: `${e.message}\nFallback (qwen) indisponível: ${s.reason}.` };
|
|
60
84
|
}
|
|
61
85
|
/**
|
|
62
86
|
* Modelo de um subagente. BRITO_EXPLORE_MODEL manda no `explore`. Com o agente principal num endpoint, subagente
|
package/dist/graph/migrate.js
CHANGED
|
@@ -3,12 +3,12 @@ import os from "node:os";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { spawnSync } from "node:child_process";
|
|
5
5
|
import { GraphStore } from "./store.js";
|
|
6
|
-
const READER = `const k = require("kuzu");
|
|
7
|
-
const db = new k.Database(process.argv[2], 0, true, true);
|
|
8
|
-
const c = new k.Connection(db);
|
|
9
|
-
c.query("MATCH (n:Note) RETURN n.id AS id, n.kind AS kind, n.text AS text, n.refs AS refs, n.ts AS ts").then((r) => r.getAll()).then((rows) => {
|
|
10
|
-
process.stdout.write(JSON.stringify(rows, (_, v) => (typeof v === "bigint" ? Number(v) : v)));
|
|
11
|
-
process.exit(0);
|
|
6
|
+
const READER = `const k = require("kuzu");
|
|
7
|
+
const db = new k.Database(process.argv[2], 0, true, true);
|
|
8
|
+
const c = new k.Connection(db);
|
|
9
|
+
c.query("MATCH (n:Note) RETURN n.id AS id, n.kind AS kind, n.text AS text, n.refs AS refs, n.ts AS ts").then((r) => r.getAll()).then((rows) => {
|
|
10
|
+
process.stdout.write(JSON.stringify(rows, (_, v) => (typeof v === "bigint" ? Number(v) : v)));
|
|
11
|
+
process.exit(0);
|
|
12
12
|
});`;
|
|
13
13
|
/** Copia as notas (memória) do grafo antigo (.brito/graph.kuzu) para o novo (.brito/graph.lbug).
|
|
14
14
|
* O Kùzu só é instalado numa pasta temporária, durante esta chamada: o pacote principal não depende mais dele. */
|