brito-cli 0.10.0 → 0.11.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 +2 -1
- package/README.pt-BR.md +2 -0
- package/dist/cli.js +19 -2
- package/dist/desktop/serve.js +68 -7
- package/dist/graph/indexer.js +7 -1
- package/dist/local/slm.js +10 -1
- package/dist/local/stats.js +27 -1
- package/dist/provider/fallback.js +1 -1
- package/dist/repl.js +2 -1
- package/dist/route/decide.js +42 -0
- package/dist/route/log.js +118 -0
- package/dist/route/stats.js +53 -0
- package/dist/ui/ui.js +4 -2
- package/dist/usage/command.js +3 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ Brito is an agent for software development. Use it in the terminal (`brito-cli`
|
|
|
12
12
|
|
|
13
13
|
## What's new in 0.10
|
|
14
14
|
|
|
15
|
-
- **Read your project inside the desktop app.** Open the **Files** panel → **Project** tab: folder tree plus a reader for Markdown (front matter, task lists, tables, Obsidian callouts, `[[wikilinks]]`), syntax-highlighted code and
|
|
15
|
+
- **Read your project inside the desktop app.** Open the **Files** panel → **Project** tab: folder tree plus a reader for Markdown (front matter, task lists, tables, Obsidian callouts, `[[wikilinks]]`), syntax-highlighted code, images, **PDF, Word (.docx) and PowerPoint (.pptx)**. ` ```mermaid ` blocks render as diagrams. Paths the agent mentions in the chat (`src/cli.ts:120`) are clickable. It is read-only, and it only reads inside the project folders you added.
|
|
16
16
|
- **Bring your Claude Code setup.** Run `brito importar claude-code` to see what would come over (skills, subagents, `deny` rules). Nothing is written until you add `--aplicar`. Imported skills start "in trial" and go through the safety lint; `--desfazer` undoes it. Add `--incluir-allow` for `allow` rules and `--memoria` for the project memory. Credentials are never read.
|
|
17
17
|
- **Know which model writes good skills.** Every automatic skill now records its author. `/skills stats` shows, per model (local SLM vs. Claude), created / promoted / archived skills and the promotion rate, so you can decide whether a cheaper model is good enough.
|
|
18
18
|
|
|
@@ -93,6 +93,7 @@ The lint **rejects** secrets/tokens, absolute user paths, credentials in URLs (`
|
|
|
93
93
|
|
|
94
94
|
- **Fallback when Claude runs out**: chain Claude → `qwen` profile → local SLM, with the full agent loop and tools (same permissions). It is temporary: after a window (the reset time reported by the error, else 10 min, or 60 min for quota/auth; `BRITO_FALLBACK_MINUTES` overrides) the next turn tries Claude again. `/fallback` shows when. A 4B model errs more with tools: use it to keep going, not for big tasks. `BRITO_SLM_FALLBACK=off` removes it from the chain; `BRITO_SLM_CTX` sets its context.
|
|
95
95
|
- **Context compaction on the local SLM**: when the conversation is compacted, the middle is summarized by the local SLM (chunked map-reduce) instead of Claude, saving tokens; on any failure it falls back to Claude. `/context` shows compactions and estimated tokens saved. `BRITO_SLM_COMPACT=off` disables it.
|
|
96
|
+
- **Route decider (shadow mode)**: the local SLM classifies each request as `claude`, `slm` or `tool` after the turn and logs it next to what the turn really needed; it changes nothing yet. `/route` (or `/route all`) shows how much it would delegate and its expensive errors. Logs in `~/.brito/routing/`; `BRITO_ROUTE=off` disables it.
|
|
96
97
|
|
|
97
98
|
`BRITO_SLM=off` disables it; `BRITO_SLM_MODEL` points to another GGUF (`hf:` URI or file path).
|
|
98
99
|
|
package/README.pt-BR.md
CHANGED
|
@@ -88,6 +88,8 @@ O lint **rejeita** segredos/tokens, caminhos absolutos do usuário, credenciais
|
|
|
88
88
|
- **Tool `digest`**: batedor e refinador de textos longos. O modelo local escolhe os blocos mais ligados ao seu objetivo e propõe trechos; o código confere cada citação contra a fonte e descarta o que não existe, e o modelo principal recebe um rascunho pequeno em markdown com citações verificadas e números de página.
|
|
89
89
|
|
|
90
90
|
- **Fallback quando o Claude acaba**: cadeia Claude → perfil `qwen` → SLM local, com o loop completo do agente e as tools (mesmas permissões). É temporário: passada a janela (o horário de reset que o erro informa, senão 10 min, ou 60 min para cota/auth; `BRITO_FALLBACK_MINUTES` sobrepõe) o próximo turno tenta o Claude de novo. O `/fallback` mostra quando. Um modelo de 4B erra mais com tools: use para continuar, não para tarefas grandes. `BRITO_SLM_FALLBACK=off` tira o SLM da cadeia; `BRITO_SLM_CTX` define o contexto.
|
|
91
|
+
- **Compactação de contexto no SLM local**: ao compactar a conversa, o meio é resumido pelo SLM local (map-reduce em fatias) em vez do Claude, poupando tokens; em qualquer falha volta ao Claude. O `/context` mostra as compactações e os tokens poupados estimados. `BRITO_SLM_COMPACT=off` desliga.
|
|
92
|
+
- **Decisor de destino (modo sombra)**: o SLM local classifica cada pedido como `claude`, `slm` ou `tool` depois do turno e grava ao lado do que o turno realmente exigiu; ainda não muda nada. `/route` (ou `/route all`) mostra quanto ele delegaria e os erros caros. Registros em `~/.brito/routing/`; `BRITO_ROUTE=off` desliga.
|
|
91
93
|
|
|
92
94
|
`BRITO_SLM=off` desliga; `BRITO_SLM_MODEL` aponta para outro GGUF (URI `hf:` ou caminho de arquivo).
|
|
93
95
|
|
package/dist/cli.js
CHANGED
|
@@ -36,6 +36,9 @@ import { expireTrials, skillStats, skillsMode } from "./skills/trial.js";
|
|
|
36
36
|
import { claudeCliComplete } from "./skills/learn-cli.js";
|
|
37
37
|
import { pullModel, removeModel, slmStatus, withSlm } from "./local/slm.js";
|
|
38
38
|
import { slmStatsLine } from "./local/stats.js";
|
|
39
|
+
import { shadowRoute, readRoutes } from "./route/log.js";
|
|
40
|
+
import { renderRouteStats } from "./route/stats.js";
|
|
41
|
+
import { routeEnabled } from "./route/decide.js";
|
|
39
42
|
import { usageCommand } from "./usage/command.js";
|
|
40
43
|
import { buildSystemPrompt, graphStatsLine, memoSystem, systemKey } from "./agent/prompt.js";
|
|
41
44
|
import { allTools } from "./tools/registry.js";
|
|
@@ -133,7 +136,7 @@ program
|
|
|
133
136
|
});
|
|
134
137
|
program
|
|
135
138
|
.command("slm [acao]")
|
|
136
|
-
.description("
|
|
139
|
+
.description("Modelo nativo embutido (llama.cpp, roda na sua máquina, sem tokens): status | pull (baixa) | rm (apaga). Faz reflexão de skills, compactação e digest; desligue com BRITO_SLM=off")
|
|
137
140
|
.action(async (acao) => {
|
|
138
141
|
const st = await slmStatus();
|
|
139
142
|
if (acao === "pull") {
|
|
@@ -523,12 +526,24 @@ const skillsCommand = (root, io, llm) => ({
|
|
|
523
526
|
ui.info(`${pending.length} proposta(s) pendente(s): /skills pending`);
|
|
524
527
|
},
|
|
525
528
|
});
|
|
529
|
+
/** `/route [all]`: o que o decisor de destino (claude/slm/tool) teria feito nos turnos já registrados, em modo sombra. */
|
|
530
|
+
const routeCommand = (root) => ({
|
|
531
|
+
desc: "decisor claude/slm/tool em modo sombra: estatísticas dos turnos (all = todos os repositórios)",
|
|
532
|
+
run: async (args) => {
|
|
533
|
+
const all = args[0] === "all";
|
|
534
|
+
for (const l of renderRouteStats(readRoutes(all ? undefined : root), all ? "todos os repositórios" : "este repositório", routeEnabled()))
|
|
535
|
+
ui.print(c.muted(` ${l}`));
|
|
536
|
+
},
|
|
537
|
+
});
|
|
526
538
|
/** Reflexão pós-turno que propõe skills (gatilho automático ou `/learn`). Independe do backend: só muda a origem do turno e o modelo. */
|
|
527
539
|
function makeLearner(getTurns, ctx, complete, getModel) {
|
|
528
540
|
return async (force = false) => {
|
|
529
541
|
const model = getModel();
|
|
530
542
|
const turns = getTurns();
|
|
531
543
|
const turn = turns[turns.length - 1] ?? null;
|
|
544
|
+
// Modo sombra do decisor de destino: registra o que ele escolheria e o que o turno exigiu. Não bloqueia o REPL.
|
|
545
|
+
if (!force)
|
|
546
|
+
void shadowRoute(ctx.root, turn);
|
|
532
547
|
// O resultado do turno vira evidência para as skills usadas nele (promoção/arquivamento), antes de refletir.
|
|
533
548
|
if (!force)
|
|
534
549
|
for (const n of [...recordOutcomes(turn, ctx), ...(skillsMode() === "off" ? [] : expireTrials(ctx.root))])
|
|
@@ -572,7 +587,7 @@ function makeLearner(getTurns, ctx, complete, getModel) {
|
|
|
572
587
|
function orchestratorCommand(main) {
|
|
573
588
|
const usage = " uso: /orquestrar on|off [modelo <perfil>] [--global] · /orquestrar modelo <perfil> [--global]";
|
|
574
589
|
return {
|
|
575
|
-
desc: "modo orquestrador: o modelo principal planeja e revisa
|
|
590
|
+
desc: "modo orquestrador: o modelo principal planeja e revisa; um perfil de endpoint (qwen) escreve o código, poupando tokens do Claude",
|
|
576
591
|
run: async (args) => {
|
|
577
592
|
const global = args.includes("--global");
|
|
578
593
|
const words = args.filter((a) => !a.startsWith("--"));
|
|
@@ -765,6 +780,7 @@ async function buildApiBackend(root, model, io, o = {}) {
|
|
|
765
780
|
"/usage": usageCommand(() => ({ ...agent.usage, costUsd: backend.cost?.() })),
|
|
766
781
|
"/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 })))); } },
|
|
767
782
|
"/learn": { desc: "extrai skills do último turno agora", run: async () => { await learn(true); } },
|
|
783
|
+
"/route": routeCommand(root),
|
|
768
784
|
"/skills": skillsCommand(root, io, { complete: withSlm(defaultComplete), model: learnModel }),
|
|
769
785
|
"/permissions": permissionsCommand(root),
|
|
770
786
|
"/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")); } },
|
|
@@ -934,6 +950,7 @@ async function buildClaudeBackend(root, io, sw, model, openGraph, resume) {
|
|
|
934
950
|
"/context": unavailable,
|
|
935
951
|
"/usage": usageCommand(() => ({ ...session.usage, costUsd: session.costUsd })),
|
|
936
952
|
"/learn": { desc: "extrai skills do último turno agora", run: async () => { await learn(true); } },
|
|
953
|
+
"/route": routeCommand(root),
|
|
937
954
|
"/skills": skillsCommand(root, io, { complete: withSlm(claudeCliComplete), model: () => process.env.BRITO_LEARN_MODEL ?? "" }),
|
|
938
955
|
"/permissions": permissionsCommand(root),
|
|
939
956
|
"/sessions": sessionsCommand(root, () => session.sessionId),
|
package/dist/desktop/serve.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { fetchPlanUsage } from "../usage/plan.js";
|
|
2
|
+
import { slmUsage } from "../local/stats.js";
|
|
2
3
|
import fs from "node:fs";
|
|
3
4
|
import os from "node:os";
|
|
4
5
|
import path from "node:path";
|
|
@@ -26,12 +27,18 @@ import { buildSystemPrompt, graphStatsLine, memoSystem, systemKey } from "../age
|
|
|
26
27
|
import { openGraphAfterClaude } from "../graph/store.js";
|
|
27
28
|
import { indexProject } from "../graph/indexer.js";
|
|
28
29
|
import { initialEffort, initialModel, isEffort, loadProfiles, MODEL_ALIASES } from "../model.js";
|
|
29
|
-
import { NO_ANTHROPIC, fallbackStatus, openEngine } from "../engine.js";
|
|
30
|
+
import { NO_ANTHROPIC, fallbackStatus, learnerModel, openEngine } from "../engine.js";
|
|
30
31
|
import { engineChip, activateFallback, chooseModel, cliShouldFallBack, fallbackWarning, newFallbackState, renderFallback, retryPrompt } from "../provider/fallback.js";
|
|
31
32
|
import { contextWindowFor, resolveModel } from "../provider/resolve.js";
|
|
32
33
|
import { PLAN_NOTE, perms } from "../permissions.js";
|
|
33
34
|
import { ORCH_NOTE, implStatus, mainTools, orch, orchGuard, restoreOrch } from "../orchestrator.js";
|
|
34
|
-
import { getSkill, listBuiltins, listSkills, skillsIndex } from "../skills/store.js";
|
|
35
|
+
import { getSkill, listBuiltins, listPending, listSkills, skillsIndex } from "../skills/store.js";
|
|
36
|
+
import { defaultComplete, recordOutcomes, reflect, shouldReflect } from "../skills/learn.js";
|
|
37
|
+
import { claudeCliComplete } from "../skills/learn-cli.js";
|
|
38
|
+
import { autoConsolidate } from "../skills/consolidate.js";
|
|
39
|
+
import { expireTrials, skillsMode } from "../skills/trial.js";
|
|
40
|
+
import { withSlm } from "../local/slm.js";
|
|
41
|
+
import { shadowRoute } from "../route/log.js";
|
|
35
42
|
/**
|
|
36
43
|
* Backend do app desktop, controlado por JSON em linhas.
|
|
37
44
|
* stdin → {t:"send",text} {t:"abort"} {t:"approve",id,allow,scope?:"session"|"project"} {t:"config",model?,effort?,mode?,orch?} {t:"reset"} {t:"query"} {t:"action"} {t:"quit"}
|
|
@@ -53,6 +60,7 @@ const BUILTIN_COMMANDS = [
|
|
|
53
60
|
{ name: "clear", description: "limpa a conversa", kind: "builtin" },
|
|
54
61
|
{ name: "fallback", description: "estado do fallback Claude → modelo local", kind: "builtin" },
|
|
55
62
|
{ name: "help", description: "lista os comandos", kind: "builtin" },
|
|
63
|
+
{ name: "learn", description: "extrai skills do último turno agora", kind: "builtin" },
|
|
56
64
|
];
|
|
57
65
|
/** Arquivos do projeto para o `@`: git ls-files (respeita .gitignore); sem git, uma varredura rasa. */
|
|
58
66
|
let fileCache = null;
|
|
@@ -114,6 +122,50 @@ const sink = {
|
|
|
114
122
|
warn: (msg) => emit({ t: "warn", msg }),
|
|
115
123
|
error: (msg) => emit({ t: "error", msg }),
|
|
116
124
|
};
|
|
125
|
+
/**
|
|
126
|
+
* Reflexão pós-turno que propõe skills (gatilho automático ou `/learn`), igual à do terminal, mas avisando a janela
|
|
127
|
+
* com eventos `skill` (refletindo → resultado + nº de propostas pendentes) em vez de imprimir.
|
|
128
|
+
*/
|
|
129
|
+
function makeLearner(getTurns, ctx, complete, getModel) {
|
|
130
|
+
let seen = null; // turno já analisado: um turno abortado/falho não reanalisa o anterior
|
|
131
|
+
const done = (notes) => emit({ t: "skill", phase: "done", notes, pending: listPending(ctx.root).length });
|
|
132
|
+
return async (force = false) => {
|
|
133
|
+
const turns = getTurns();
|
|
134
|
+
const turn = turns[turns.length - 1] ?? null;
|
|
135
|
+
if (!force && turn === seen)
|
|
136
|
+
return;
|
|
137
|
+
if (!force)
|
|
138
|
+
void shadowRoute(ctx.root, turn); // modo sombra do decisor de destino; não bloqueia
|
|
139
|
+
const model = getModel();
|
|
140
|
+
try {
|
|
141
|
+
const outcomes = force ? [] : [...recordOutcomes(turn, ctx), ...(skillsMode() === "off" ? [] : expireTrials(ctx.root))];
|
|
142
|
+
if (!force && !shouldReflect(turn, ctx)) {
|
|
143
|
+
seen = turn;
|
|
144
|
+
ctx.loadedSkills?.clear();
|
|
145
|
+
if (outcomes.length)
|
|
146
|
+
done(outcomes);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if (!turn) {
|
|
150
|
+
emit({ t: "skill", phase: "done", notes: ["Nada para refletir ainda."], pending: listPending(ctx.root).length });
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
seen = turn;
|
|
154
|
+
emit({ t: "skill", phase: "reflecting" });
|
|
155
|
+
const notes = await reflect(turn, ctx, model, complete, turns[turns.length - 2]);
|
|
156
|
+
if (skillsMode() === "auto") {
|
|
157
|
+
try {
|
|
158
|
+
notes.push(...await autoConsolidate(ctx.root, model, complete));
|
|
159
|
+
}
|
|
160
|
+
catch { /* manutenção é opcional */ }
|
|
161
|
+
}
|
|
162
|
+
done([...outcomes, ...(notes.length ? notes : ["nenhuma skill nova"])]);
|
|
163
|
+
}
|
|
164
|
+
catch (e) {
|
|
165
|
+
emit({ t: "skill", phase: "error", notes: [`reflexão falhou: ${e?.message ?? e}`], pending: listPending(ctx.root).length });
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
}
|
|
117
169
|
const apiLabel = (m) => (resolveModel(m).kind === "openai" ? "endpoint" : "API Anthropic");
|
|
118
170
|
export async function serveDesktop(root, resumeId) {
|
|
119
171
|
const pending = new Map();
|
|
@@ -175,6 +227,7 @@ export async function serveDesktop(root, resumeId) {
|
|
|
175
227
|
if (o.session?.todos)
|
|
176
228
|
ctx.todos = o.session.todos;
|
|
177
229
|
let ac = null;
|
|
230
|
+
const learn = makeLearner(() => agent.recentTurns, ctx, withSlm(defaultComplete), () => learnerModel(agent.model, process.env));
|
|
178
231
|
const id = o.session?.id ?? randomUUID();
|
|
179
232
|
// Depois de uma troca a partir do claude CLI quem guarda a conversa é ele (o id é o dele): nada a salvar aqui.
|
|
180
233
|
const persist = () => {
|
|
@@ -188,7 +241,7 @@ export async function serveDesktop(root, resumeId) {
|
|
|
188
241
|
}
|
|
189
242
|
};
|
|
190
243
|
return {
|
|
191
|
-
kind: "api", effort: "", fallback,
|
|
244
|
+
kind: "api", effort: "", fallback, learn,
|
|
192
245
|
// Depois da troca o id segue o do claude: a janela o guarda para retomar a conversa ao reabrir.
|
|
193
246
|
get sessionId() { return o.back?.engine.sessionId ?? id; },
|
|
194
247
|
get model() { return agent.model; }, set model(m) { agent.model = m; },
|
|
@@ -282,6 +335,9 @@ export async function serveDesktop(root, resumeId) {
|
|
|
282
335
|
// Dentro do Electron o executável é o próprio app: este env o faz agir como Node nos filhos (claude → mcp-serve).
|
|
283
336
|
env: process.versions.electron || process.env.ELECTRON_RUN_AS_NODE ? { ELECTRON_RUN_AS_NODE: "1" } : undefined,
|
|
284
337
|
});
|
|
338
|
+
// O grafo fica com o servidor MCP; skill_save só usa o store de skills, então `graph` não é tocado.
|
|
339
|
+
const learnCtx = { root, graph: undefined, ask, sync: async () => { }, dirty: false, loadedSkills: session.loadedSkills };
|
|
340
|
+
const learn = makeLearner(() => session.recentTurns, learnCtx, withSlm(claudeCliComplete), () => process.env.BRITO_LEARN_MODEL ?? "");
|
|
285
341
|
if (resumeId)
|
|
286
342
|
session.resume(resumeId);
|
|
287
343
|
let resumed = !!resumeId; // até o primeiro reset
|
|
@@ -290,7 +346,7 @@ export async function serveDesktop(root, resumeId) {
|
|
|
290
346
|
/** Fechamento do grafo do modo API na volta para cá (ver `back.release`). */
|
|
291
347
|
let graphReleased = Promise.resolve();
|
|
292
348
|
const claude = {
|
|
293
|
-
kind: "claude", fallback,
|
|
349
|
+
kind: "claude", fallback, learn,
|
|
294
350
|
get sessionId() { return session.sessionId; },
|
|
295
351
|
get model() { return session.model; },
|
|
296
352
|
get effort() { return session.effort; }, set effort(e) { session.effort = e; },
|
|
@@ -380,11 +436,16 @@ export async function serveDesktop(root, resumeId) {
|
|
|
380
436
|
}
|
|
381
437
|
busy = true;
|
|
382
438
|
try {
|
|
383
|
-
|
|
439
|
+
if (cmd === "/learn")
|
|
440
|
+
await engine.learn(true);
|
|
441
|
+
else {
|
|
442
|
+
await engine.send(text);
|
|
443
|
+
await engine.learn();
|
|
444
|
+
}
|
|
384
445
|
}
|
|
385
446
|
finally {
|
|
386
447
|
busy = false;
|
|
387
|
-
emit({ t: "usage", ...engine.stats(), model: engine.model, sessionId: engine.sessionId, fallback: engine.fallback.active, chip: chipOf(engine.model, engine.fallback) });
|
|
448
|
+
emit({ t: "usage", ...engine.stats(), slm: slmUsage(), model: engine.model, sessionId: engine.sessionId, fallback: engine.fallback.active, chip: chipOf(engine.model, engine.fallback) });
|
|
388
449
|
}
|
|
389
450
|
break;
|
|
390
451
|
}
|
|
@@ -409,7 +470,7 @@ export async function serveDesktop(root, resumeId) {
|
|
|
409
470
|
data = [...BUILTIN_COMMANDS, ...skills].filter((c) => !arg || c.name.includes(arg));
|
|
410
471
|
}
|
|
411
472
|
else if (m.q === "usage")
|
|
412
|
-
data = { plan: await fetchPlanUsage({ force: arg === "force" }), session: engine.stats(), model: engine.model };
|
|
473
|
+
data = { plan: await fetchPlanUsage({ force: arg === "force" }), session: engine.stats(), slm: slmUsage(), model: engine.model };
|
|
413
474
|
else if (m.q === "models")
|
|
414
475
|
data = { aliases: Object.keys(MODEL_ALIASES), profiles: Object.keys(loadProfiles().profiles) };
|
|
415
476
|
}
|
package/dist/graph/indexer.js
CHANGED
|
@@ -223,7 +223,13 @@ async function scan(root, aliases) {
|
|
|
223
223
|
const files = await sourceFiles(root);
|
|
224
224
|
const out = new Map();
|
|
225
225
|
for (const rel of files) {
|
|
226
|
-
|
|
226
|
+
let src;
|
|
227
|
+
try {
|
|
228
|
+
src = fs.readFileSync(path.join(root, rel), "utf8");
|
|
229
|
+
}
|
|
230
|
+
catch {
|
|
231
|
+
continue;
|
|
232
|
+
} // ilegível (OneDrive, travado, permissão): ignora
|
|
227
233
|
if (src.length > 1_000_000)
|
|
228
234
|
continue;
|
|
229
235
|
out.set(rel, { src, hash: crypto.createHash("sha1").update(src).digest("hex") + ":" + INDEXER_VERSION + ":" + aliases.forFile(rel).signature });
|
package/dist/local/slm.js
CHANGED
|
@@ -3,6 +3,7 @@ import fs from "node:fs";
|
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { globalHome } from "../skills/store.js";
|
|
6
|
+
import { recordSlmCall } from "./stats.js";
|
|
6
7
|
/**
|
|
7
8
|
* SLM embutido (llama.cpp via `node-llama-cpp`, dependência OPCIONAL): roda dentro do processo, sem servidor.
|
|
8
9
|
* Hoje serve às tarefas auxiliares (reflexão que propõe skills). Só entra em ação depois de `brito slm pull`
|
|
@@ -104,12 +105,16 @@ export async function slmGenerate(o) {
|
|
|
104
105
|
const lib = await loadLib();
|
|
105
106
|
const { model, llama } = await ensureLoaded();
|
|
106
107
|
const context = await model.createContext({ contextSize: { max: 8192 } });
|
|
108
|
+
const t0 = Date.now();
|
|
109
|
+
let out = "";
|
|
107
110
|
try {
|
|
108
111
|
const session = new lib.LlamaChatSession({ contextSequence: context.getSequence(), systemPrompt: o.system });
|
|
109
112
|
const grammar = o.schema ? await llama.createGrammarForJsonSchema(o.schema) : undefined;
|
|
110
|
-
|
|
113
|
+
out = await session.prompt(o.user, { grammar, maxTokens: o.maxTokens ?? 1500, temperature: o.temperature ?? 0.2, signal: o.signal });
|
|
114
|
+
return out;
|
|
111
115
|
}
|
|
112
116
|
finally {
|
|
117
|
+
recordSlmCall(o.system.length + o.user.length, out.length, Date.now() - t0);
|
|
113
118
|
await context.dispose();
|
|
114
119
|
scheduleUnload();
|
|
115
120
|
}
|
|
@@ -122,6 +127,8 @@ export async function slmChat(o) {
|
|
|
122
127
|
const lib = await loadLib();
|
|
123
128
|
const { model } = await ensureLoaded();
|
|
124
129
|
const context = await model.createContext({ contextSize: { max: slmChatCtx() } });
|
|
130
|
+
const t0 = Date.now();
|
|
131
|
+
let outChars = 0;
|
|
125
132
|
try {
|
|
126
133
|
const chat = new lib.LlamaChat({ contextSequence: context.getSequence() });
|
|
127
134
|
const functions = o.functions && Object.keys(o.functions).length
|
|
@@ -133,9 +140,11 @@ export async function slmChat(o) {
|
|
|
133
140
|
o.onText?.(t); },
|
|
134
141
|
});
|
|
135
142
|
const calls = (res.functionCalls ?? []).map((c) => ({ name: String(c.functionName), input: c.params ?? {} }));
|
|
143
|
+
outChars = String(res.response ?? "").length + JSON.stringify(calls).length;
|
|
136
144
|
return { text: String(res.response ?? ""), calls, truncated: res.metadata?.stopReason === "maxTokens" };
|
|
137
145
|
}
|
|
138
146
|
finally {
|
|
147
|
+
recordSlmCall(JSON.stringify(o.history).length + (o.functions ? JSON.stringify(o.functions).length : 0), outChars, Date.now() - t0);
|
|
139
148
|
await context.dispose();
|
|
140
149
|
scheduleUnload();
|
|
141
150
|
}
|
package/dist/local/stats.js
CHANGED
|
@@ -6,8 +6,34 @@ export const slmStats = {
|
|
|
6
6
|
compactFallbacks: 0,
|
|
7
7
|
/** estimativa: tokens de entrada que o modelo principal deixou de processar (texto original − resumo) */
|
|
8
8
|
tokensSaved: 0,
|
|
9
|
+
/** TODA geração feita no SLM local (compactação, digest, reflexão, roteador, fallback): é o "gasto" local */
|
|
10
|
+
calls: 0,
|
|
11
|
+
/** estimativa (~4 caracteres/token) do que o SLM leu e escreveu */
|
|
12
|
+
tokensIn: 0,
|
|
13
|
+
tokensOut: 0,
|
|
14
|
+
ms: 0,
|
|
9
15
|
};
|
|
10
|
-
export const resetSlmStats = () => {
|
|
16
|
+
export const resetSlmStats = () => {
|
|
17
|
+
slmStats.compactCalls = 0;
|
|
18
|
+
slmStats.compactFallbacks = 0;
|
|
19
|
+
slmStats.tokensSaved = 0;
|
|
20
|
+
slmStats.calls = 0;
|
|
21
|
+
slmStats.tokensIn = 0;
|
|
22
|
+
slmStats.tokensOut = 0;
|
|
23
|
+
slmStats.ms = 0;
|
|
24
|
+
};
|
|
25
|
+
const est = (chars) => Math.ceil(chars / 4);
|
|
26
|
+
/** Registra uma geração do SLM (chamada por `slmGenerate`/`slmChat`; mesmo se falhar, o custo local existiu). */
|
|
27
|
+
export function recordSlmCall(inChars, outChars, ms) {
|
|
28
|
+
slmStats.calls++;
|
|
29
|
+
slmStats.tokensIn += est(inChars);
|
|
30
|
+
slmStats.tokensOut += est(outChars);
|
|
31
|
+
slmStats.ms += ms;
|
|
32
|
+
}
|
|
33
|
+
/** Resumo do gasto local para a barra de status/painel; `null` enquanto o SLM não foi usado. */
|
|
34
|
+
export function slmUsage() {
|
|
35
|
+
return slmStats.calls ? { calls: slmStats.calls, tokensIn: slmStats.tokensIn, tokensOut: slmStats.tokensOut, ms: slmStats.ms, tokensSaved: slmStats.tokensSaved } : null;
|
|
36
|
+
}
|
|
11
37
|
/** Linha para o `/context`; `null` quando o SLM não fez nada. */
|
|
12
38
|
export function slmStatsLine() {
|
|
13
39
|
const { compactCalls: c, compactFallbacks: f, tokensSaved: t } = slmStats;
|
|
@@ -176,7 +176,7 @@ const hhmm = (ms) => { const d = new Date(ms); return `${String(d.getHours()).pa
|
|
|
176
176
|
*/
|
|
177
177
|
export function engineChip(model, fb) {
|
|
178
178
|
const r = resolveModel(model);
|
|
179
|
-
const where = r.kind === "anthropic" ? "☁ nuvem" : r.local ? "💻
|
|
179
|
+
const where = r.kind === "anthropic" ? "☁ Claude (nuvem, usa tokens)" : r.local ? "💻 nativo (na sua máquina, sem tokens)" : "🌐 endpoint (servidor próprio)";
|
|
180
180
|
const text = `${where} · ${r.label}`;
|
|
181
181
|
if (!fb?.active)
|
|
182
182
|
return { text, fallback: false };
|
package/dist/repl.js
CHANGED
|
@@ -6,6 +6,7 @@ import { randomBytes } from "node:crypto";
|
|
|
6
6
|
import { Dock } from "./ui/dock.js";
|
|
7
7
|
import os from "node:os";
|
|
8
8
|
import { ui, c, fmtTokens } from "./ui/ui.js";
|
|
9
|
+
import { slmUsage } from "./local/stats.js";
|
|
9
10
|
import { cols } from "./ui/theme.js";
|
|
10
11
|
import { engineChip } from "./provider/fallback.js";
|
|
11
12
|
/** Nome do usuário da máquina (fallback para variáveis de ambiente). */
|
|
@@ -103,7 +104,7 @@ export async function runRepl(b, o) {
|
|
|
103
104
|
mode: modeChip(),
|
|
104
105
|
engine: engineChip(b.model(), b.fallback?.()),
|
|
105
106
|
model: b.model(), ctxTokens: b.contextTokens(), ctxWindow: b.ctxWindow,
|
|
106
|
-
input: b.usage().input, output: b.usage().output, cacheRead: b.usage().cacheRead, startedAt: o.startedAt, cost: b.cost?.(),
|
|
107
|
+
input: b.usage().input, output: b.usage().output, cacheRead: b.usage().cacheRead, startedAt: o.startedAt, cost: b.cost?.(), slm: slmUsage(),
|
|
107
108
|
extra: `${skillCount} ${skillCount === 1 ? "skill" : "skills"}`,
|
|
108
109
|
});
|
|
109
110
|
if (interactive && process.stdout.isTTY)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { slmGenerate, slmStatus } from "../local/slm.js";
|
|
2
|
+
export const ROUTE_NAMES = ["slm", "claude", "tool"];
|
|
3
|
+
export const ROUTE_DESC = {
|
|
4
|
+
slm: "Condensar, resumir, extrair, traduzir, reformatar ou rotular um texto que já veio no pedido (log, diff, saída de comando), sem raciocinar sobre o código nem alterá-lo",
|
|
5
|
+
claude: "Raciocinar, depurar, projetar, implementar, refatorar ou revisar código; decisões de arquitetura; tarefas de várias etapas",
|
|
6
|
+
tool: "Consultar o projeto ou executar um comando direto: localizar símbolos, quem chama quem, listar ou abrir arquivos, rodar testes, build ou git",
|
|
7
|
+
};
|
|
8
|
+
/** `BRITO_ROUTE=off` desliga o registro em modo sombra. */
|
|
9
|
+
export const routeEnabled = () => process.env.BRITO_ROUTE !== "off";
|
|
10
|
+
const SCHEMA = { type: "object", properties: { route: { enum: [...ROUTE_NAMES] } }, required: ["route"] };
|
|
11
|
+
const SYSTEM = `Você é um roteador. Escolha o destino de um pedido feito a um agente de programação.\n${ROUTE_NAMES.map((r) => `- ${r}: ${ROUTE_DESC[r]}`).join("\n")}\nResponda só com o JSON {"route": "..."}.`;
|
|
12
|
+
/** O pedido é só classificado: o começo basta e protege a janela do SLM. */
|
|
13
|
+
const MAX_CHARS = 1500;
|
|
14
|
+
/** Lê a saída do SLM; qualquer coisa fora do enum vira `null`. */
|
|
15
|
+
export function parseRoute(out) {
|
|
16
|
+
try {
|
|
17
|
+
const r = JSON.parse(out)?.route;
|
|
18
|
+
return ROUTE_NAMES.includes(r) ? r : null;
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Decide o destino. `ready` e `gen` são injetáveis para teste; por padrão usa o SLM local se ele estiver baixado e
|
|
26
|
+
* habilitado. Nunca lança: o pior caso é `{ route: "claude", source: "default" }`.
|
|
27
|
+
*/
|
|
28
|
+
export async function decideRoute(text, opts = {}) {
|
|
29
|
+
const t0 = performance.now();
|
|
30
|
+
const dflt = () => ({ route: "claude", source: "default", ms: Math.round(performance.now() - t0) });
|
|
31
|
+
try {
|
|
32
|
+
const ready = await (opts.ready ?? (async () => (await slmStatus()).ready))();
|
|
33
|
+
if (!ready)
|
|
34
|
+
return dflt();
|
|
35
|
+
const out = await (opts.gen ?? slmGenerate)({ system: SYSTEM, user: text.slice(0, MAX_CHARS), schema: SCHEMA, maxTokens: 20, temperature: 0 });
|
|
36
|
+
const route = parseRoute(out);
|
|
37
|
+
return route ? { route, source: "qwen", ms: Math.round(performance.now() - t0) } : dflt();
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return dflt();
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { globalHome } from "../skills/store.js";
|
|
5
|
+
import { decideRoute, routeEnabled } from "./decide.js";
|
|
6
|
+
const TEXT_MAX = 400;
|
|
7
|
+
// ------------------------------------------------------------------ rótulo implícito
|
|
8
|
+
/** Tools que, se usadas, mostram que o pedido exigia o Claude. */
|
|
9
|
+
const CLAUDE_TOOLS = new Set(["write_file", "edit_file", "implement", "delegate", "skill_save", "skill_delete", "spec_save", "exit_plan", "todo_write"]);
|
|
10
|
+
/** Comandos de shell que equivalem a uma consulta/execução direta (ler estado, rodar testes/build). */
|
|
11
|
+
const DIRECT_BASH = /^\s*(git\s+(status|log|diff|branch|show|rev-parse|remote)|ls|cat|head|tail|pwd|wc|node\s+(-v|--version)|npm\s+(test|run\s+(test|build|typecheck|lint)|ls|view)|npx\s+(tsc|tsx\s+--test)|tsc|yarn\s+(test|build|typecheck)|pnpm\s+(test|build|typecheck))\b/;
|
|
12
|
+
/** Chamadas de tool do turno, na ordem. */
|
|
13
|
+
export function toolUses(turn) {
|
|
14
|
+
const out = [];
|
|
15
|
+
for (const m of turn.messages) {
|
|
16
|
+
if (m.role !== "assistant" || typeof m.content === "string")
|
|
17
|
+
continue;
|
|
18
|
+
for (const b of m.content)
|
|
19
|
+
if (b.type === "tool_use")
|
|
20
|
+
out.push({ name: String(b.name), input: b.input });
|
|
21
|
+
}
|
|
22
|
+
return out;
|
|
23
|
+
}
|
|
24
|
+
/** Texto do pedido do usuário (primeira mensagem do turno). */
|
|
25
|
+
export function turnText(turn) {
|
|
26
|
+
const m = turn.messages.find((x) => x.role === "user");
|
|
27
|
+
if (!m)
|
|
28
|
+
return "";
|
|
29
|
+
if (typeof m.content === "string")
|
|
30
|
+
return m.content;
|
|
31
|
+
return m.content.flatMap((b) => (b.type === "text" ? [b.text] : [])).join(" ");
|
|
32
|
+
}
|
|
33
|
+
export function implicitLabel(uses) {
|
|
34
|
+
if (!uses.length)
|
|
35
|
+
return "none";
|
|
36
|
+
for (const u of uses) {
|
|
37
|
+
if (CLAUDE_TOOLS.has(u.name))
|
|
38
|
+
return "claude";
|
|
39
|
+
if (u.name === "bash") {
|
|
40
|
+
const cmd = String(u.input?.command ?? "");
|
|
41
|
+
if (!DIRECT_BASH.test(cmd) || /[;&|><]/.test(cmd.replace(/2>&1/g, "")))
|
|
42
|
+
return "claude";
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return "tool";
|
|
46
|
+
}
|
|
47
|
+
// ------------------------------------------------------------------ arquivo
|
|
48
|
+
export const routeDir = () => path.join(globalHome(), "routing");
|
|
49
|
+
/** Um arquivo por repositório: nome legível + hash do caminho (dois projetos com o mesmo nome não se misturam). */
|
|
50
|
+
export function routeFile(root) {
|
|
51
|
+
const slug = path.basename(root).replace(/[^\w.-]+/g, "_") || "repo";
|
|
52
|
+
return path.join(routeDir(), `${slug}-${crypto.createHash("sha1").update(path.resolve(root)).digest("hex").slice(0, 8)}.jsonl`);
|
|
53
|
+
}
|
|
54
|
+
export function appendRoute(root, e) {
|
|
55
|
+
fs.mkdirSync(routeDir(), { recursive: true });
|
|
56
|
+
fs.appendFileSync(routeFile(root), JSON.stringify(e) + "\n");
|
|
57
|
+
}
|
|
58
|
+
function parseLines(file) {
|
|
59
|
+
let raw;
|
|
60
|
+
try {
|
|
61
|
+
raw = fs.readFileSync(file, "utf8");
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
return raw.split("\n").flatMap((l) => {
|
|
67
|
+
if (!l.trim())
|
|
68
|
+
return [];
|
|
69
|
+
try {
|
|
70
|
+
return [JSON.parse(l)];
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return [];
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
/** Entradas de um repositório, ou de todos (`root` indefinido). */
|
|
78
|
+
export function readRoutes(root) {
|
|
79
|
+
if (root)
|
|
80
|
+
return parseLines(routeFile(root));
|
|
81
|
+
let files;
|
|
82
|
+
try {
|
|
83
|
+
files = fs.readdirSync(routeDir()).filter((f) => f.endsWith(".jsonl"));
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return [];
|
|
87
|
+
}
|
|
88
|
+
return files.flatMap((f) => parseLines(path.join(routeDir(), f)));
|
|
89
|
+
}
|
|
90
|
+
// ------------------------------------------------------------------ modo sombra
|
|
91
|
+
/**
|
|
92
|
+
* Registra a decisão sombra de um turno já concluído. Roda DEPOIS do turno (não atrasa a resposta) e nunca lança.
|
|
93
|
+
* Turno sem texto ou interrompido (não finalizado) é ignorado: não há rótulo confiável.
|
|
94
|
+
*/
|
|
95
|
+
const logged = new WeakSet();
|
|
96
|
+
export async function shadowRoute(root, turn, decide = decideRoute) {
|
|
97
|
+
try {
|
|
98
|
+
if (!routeEnabled() || !turn?.finished || logged.has(turn))
|
|
99
|
+
return null;
|
|
100
|
+
logged.add(turn);
|
|
101
|
+
const text = turnText(turn).replace(/\s+/g, " ").trim();
|
|
102
|
+
// Comandos do próprio REPL e mensagens vazias não são pedidos ao agente.
|
|
103
|
+
if (!text || text.startsWith("/"))
|
|
104
|
+
return null;
|
|
105
|
+
const d = await decide(text);
|
|
106
|
+
const uses = toolUses(turn);
|
|
107
|
+
const entry = {
|
|
108
|
+
ts: new Date().toISOString(), repo: path.basename(path.resolve(root)), text: text.slice(0, TEXT_MAX),
|
|
109
|
+
decision: d.route, source: d.source, ms: d.ms, implicit: implicitLabel(uses),
|
|
110
|
+
tools: [...new Set(uses.map((u) => u.name))].slice(0, 12), errors: turn.errors,
|
|
111
|
+
};
|
|
112
|
+
appendRoute(root, entry);
|
|
113
|
+
return entry;
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
const zero = (keys) => Object.fromEntries(keys.map((k) => [k, 0]));
|
|
2
|
+
const R = ["slm", "claude", "tool"];
|
|
3
|
+
const I = ["slm", "claude", "tool", "none"];
|
|
4
|
+
export function summarize(entries) {
|
|
5
|
+
const dec = entries.filter((e) => e.source === "qwen");
|
|
6
|
+
const s = {
|
|
7
|
+
total: entries.length, decided: dec.length, byDecision: zero(R), byImplicit: zero(I),
|
|
8
|
+
matrix: Object.fromEntries(R.map((r) => [r, zero(I)])),
|
|
9
|
+
risky: 0, wasted: 0, labeled: 0, hits: 0, comparable: 0, delegable: 0, msMedian: 0,
|
|
10
|
+
};
|
|
11
|
+
for (const e of dec) {
|
|
12
|
+
s.byDecision[e.decision]++;
|
|
13
|
+
s.byImplicit[e.implicit]++;
|
|
14
|
+
s.matrix[e.decision][e.implicit]++;
|
|
15
|
+
if (e.decision !== "claude")
|
|
16
|
+
s.delegable++;
|
|
17
|
+
if (e.implicit !== "none")
|
|
18
|
+
s.labeled++;
|
|
19
|
+
if (e.decision !== "claude" && e.implicit === "claude")
|
|
20
|
+
s.risky++;
|
|
21
|
+
if (e.decision === "claude" && e.implicit === "tool")
|
|
22
|
+
s.wasted++;
|
|
23
|
+
// `slm` não tem rótulo implícito (resumir texto dado não deixa rastro nas tools): fica fora da taxa de acerto
|
|
24
|
+
if (e.decision !== "slm" && e.implicit !== "none") {
|
|
25
|
+
s.comparable++;
|
|
26
|
+
if (e.decision === e.implicit)
|
|
27
|
+
s.hits++;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
const ms = dec.map((e) => e.ms).sort((a, b) => a - b);
|
|
31
|
+
s.msMedian = ms.length ? ms[Math.floor(ms.length / 2)] : 0;
|
|
32
|
+
return s;
|
|
33
|
+
}
|
|
34
|
+
const pct = (a, b) => (b ? `${Math.round((a / b) * 100)}%` : "—");
|
|
35
|
+
/** Linhas do `/route`. */
|
|
36
|
+
export function renderRouteStats(entries, scope, enabled) {
|
|
37
|
+
const s = summarize(entries);
|
|
38
|
+
const head = `modo sombra ${enabled ? "ligado" : "desligado (BRITO_ROUTE=off)"} · ${scope}: nada aqui muda o que o Brito faz`;
|
|
39
|
+
if (!s.total)
|
|
40
|
+
return [head, " sem turnos registrados ainda"];
|
|
41
|
+
const out = [head, ` turnos registrados ${s.total} (decididos pelo SLM: ${s.decided}${s.total - s.decided ? `; sem SLM pronto: ${s.total - s.decided}` : ""})`];
|
|
42
|
+
if (!s.decided)
|
|
43
|
+
return [...out, " rode `brito slm pull` para o decisor entrar em ação"];
|
|
44
|
+
out.push(` teria mandado claude ${s.byDecision.claude} · tool ${s.byDecision.tool} · slm ${s.byDecision.slm} (delegaria ${pct(s.delegable, s.decided)})`);
|
|
45
|
+
out.push(` o turno exigiu claude ${s.byImplicit.claude} · só consulta ${s.byImplicit.tool} · sem tools ${s.byImplicit.none}`);
|
|
46
|
+
out.push(` erro caro ${s.risky} turno(s) que o decisor delegaria mas escreveram/planejaram/delegaram${s.decided - s.byDecision.claude ? ` (${pct(s.risky, s.delegable)} do que ele delegaria)` : ""}`);
|
|
47
|
+
out.push(` custo sem ganho ${s.wasted} turno(s) que foram ao Claude mas só consultaram`);
|
|
48
|
+
out.push(` acerto estimado ${pct(s.hits, s.comparable)} em ${s.comparable} turnos comparáveis (slm não tem rótulo implícito)`);
|
|
49
|
+
out.push(` latência mediana ${s.msMedian} ms por decisão`);
|
|
50
|
+
const need = 30 - s.decided;
|
|
51
|
+
out.push(need > 0 ? ` amostra pequena: faltam ~${need} decisões para a taxa ser confiável` : " amostra suficiente para avaliar se o decisor pode passar a agir");
|
|
52
|
+
return out;
|
|
53
|
+
}
|
package/dist/ui/ui.js
CHANGED
|
@@ -475,6 +475,7 @@ class UI {
|
|
|
475
475
|
[0, label("contexto") + c.muted(`${fmtTokens(s.ctxTokens)}/${fmtTokens(s.ctxWindow)}`) + ` [${bar}] ${colour(`${pct}%`)}`],
|
|
476
476
|
...(hit !== undefined ? [[2, label("cache") + hitColour(`◎ ${Math.round(hit)}%`)]] : []),
|
|
477
477
|
[3, label("tokens") + c.muted(`↑${fmtTokens(s.input)} ↓${fmtTokens(s.output)}`)],
|
|
478
|
+
...(s.slm ? [[3, c.light("💻 ") + label("slm") + c.muted(`${s.slm.calls}× ↑${fmtTokens(s.slm.tokensIn)} ↓${fmtTokens(s.slm.tokensOut)}`)]] : []),
|
|
478
479
|
...(s.cost !== undefined ? [[1, label("custo") + c.muted(`$${s.cost.toFixed(2)}`)]] : []),
|
|
479
480
|
...(this.activeSkill ? [[1, c.light("🎓 ") + label("skill") + c.bold(c.soft(this.activeSkill))]] : []),
|
|
480
481
|
[4, label("tempo") + c.muted(mins < 1 ? "<1m" : `${mins}m`) + (active >= 1000 ? c.dim(` (ativo ${fmtDur(active)})`) : "")],
|
|
@@ -496,8 +497,9 @@ class UI {
|
|
|
496
497
|
["contexto", "tokens que o modelo está lendo agora / tamanho máximo da janela. Perto de 100% a conversa precisa ser compactada."],
|
|
497
498
|
["cache", "% do prompt reaproveitado do cache (◎). Alto (verde) = respostas mais rápidas e baratas. Some enquanto não há dado."],
|
|
498
499
|
["tokens", "total da sessão: ↑ enviados ao modelo (inclui cache) · ↓ gerados por ele."],
|
|
499
|
-
["custo", "gasto estimado da sessão em dólares."],
|
|
500
|
-
["
|
|
500
|
+
["custo", "gasto estimado da sessão em dólares (só o Claude; o SLM local roda de graça no seu computador)."],
|
|
501
|
+
["slm", "💻 trabalho feito pelo modelo local: nº de chamadas × tokens lidos ↑ e gerados ↓ (estimativa). Não gasta plano nem dinheiro. Só aparece depois do primeiro uso."],
|
|
502
|
+
["motor", "quem está respondendo: 💻 nativo = modelo embutido que roda na sua máquina (sem tokens, sem custo); ☁ Claude = nuvem, consome tokens; 🌐 endpoint = servidor seu (ex.: Qwen). ⚠ FALLBACK = troca automática ativa, com a hora em que o Claude é tentado de novo."],
|
|
501
503
|
["tempo", "há quanto tempo a sessão está aberta · (ativo) soma do tempo gasto em todos os turnos."],
|
|
502
504
|
["📁", "projeto (pasta) em que o Brito está trabalhando — também aparece no título da janela do terminal."],
|
|
503
505
|
["skill", "skill carregada (skill_load) no turno em andamento."],
|
package/dist/usage/command.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ui, c, fmtTokens } from "../ui/ui.js";
|
|
2
|
+
import { slmUsage } from "../local/stats.js";
|
|
2
3
|
import { fetchPlanUsage, renderPlanLines } from "./plan.js";
|
|
3
4
|
/** `/usage` do terminal: limites do plano (como o do Claude Code) + tokens/custo desta sessão. */
|
|
4
5
|
export function usageCommand(session) {
|
|
@@ -15,6 +16,8 @@ export function usageCommand(session) {
|
|
|
15
16
|
ui.print(` ${c.muted(`Plano: ${r.reason}`)}`);
|
|
16
17
|
const s = session();
|
|
17
18
|
ui.print(` ${c.soft("Sessão")} ${c.muted(`↑${fmtTokens(s.input)} ↓${fmtTokens(s.output)} · cache ${fmtTokens(s.cacheRead)}${s.costUsd ? ` · US$ ${s.costUsd.toFixed(2)}` : ""}`)}`);
|
|
19
|
+
const l = slmUsage();
|
|
20
|
+
ui.print(` ${c.soft("SLM local")} ${c.muted(l ? `${l.calls} chamadas · ↑${fmtTokens(l.tokensIn)} ↓${fmtTokens(l.tokensOut)} (estimado) · ${(l.ms / 1000).toFixed(1)}s · sem custo` : "não usado nesta sessão")}`);
|
|
18
21
|
},
|
|
19
22
|
};
|
|
20
23
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "brito-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "Coding-agent CLI with a code knowledge graph, memory and reviewable skills / CLI de agente de código com grafo de conhecimento, memória e skills revisáveis",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|