brito-cli 0.8.0 → 0.10.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 +321 -298
- package/README.pt-BR.md +323 -301
- package/dist/agent/claude-cli.js +9 -3
- package/dist/agent/context.js +17 -10
- package/dist/agent/loop.js +8 -2
- package/dist/agent/prompt.js +25 -25
- package/dist/cli.js +148 -7
- package/dist/desktop/panels.js +93 -0
- package/dist/desktop/protocol.js +5 -0
- package/dist/desktop/serve.js +162 -14
- package/dist/graph/migrate.js +6 -6
- package/dist/import/claude-code.js +286 -0
- package/dist/local/compact.js +66 -0
- package/dist/local/slm.js +5 -1
- package/dist/local/stats.js +17 -0
- package/dist/mcp/server.js +35 -3
- package/dist/model.js +21 -0
- package/dist/orchestrator.js +131 -0
- package/dist/permissions.js +7 -3
- package/dist/provider/resolve.js +3 -1
- package/dist/repl.js +5 -1
- package/dist/skills/builtin.js +63 -63
- package/dist/skills/by.js +7 -0
- package/dist/skills/consolidate.js +7 -7
- package/dist/skills/learn.js +26 -24
- package/dist/skills/store.js +21 -2
- package/dist/skills/trial.js +32 -3
- package/dist/tools/delegate.js +30 -22
- package/dist/tools/implement.js +50 -0
- package/dist/tools/spec.js +47 -0
- package/dist/usage/command.js +20 -0
- package/dist/usage/plan.js +111 -0
- package/package.json +81 -80
package/dist/skills/store.js
CHANGED
|
@@ -32,12 +32,13 @@ function parse(text, scope) {
|
|
|
32
32
|
invoke: meta.invoke === "manual" ? "manual" : "auto",
|
|
33
33
|
model: meta.model ?? "",
|
|
34
34
|
lastLoss: meta.lastLoss ?? "",
|
|
35
|
+
by: meta.by ?? "",
|
|
35
36
|
};
|
|
36
37
|
}
|
|
37
38
|
export function writeSkill(root, s) {
|
|
38
39
|
const file = fileOf(root, s.scope, s.name);
|
|
39
40
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
40
|
-
fs.writeFileSync(file, stringifyFrontmatter({ name: s.name, description: s.description, version: s.version, uses: s.uses, created: s.created, updated: s.updated, lastUsed: s.lastUsed, refs: s.refs.join(", "), status: s.status, origin: s.origin, wins: s.wins, losses: s.losses, ...(s.invoke === "manual" ? { invoke: "manual" } : {}), ...(s.model ? { model: s.model } : {}), ...(s.lastLoss ? { lastLoss: s.lastLoss } : {}) }, s.body));
|
|
41
|
+
fs.writeFileSync(file, stringifyFrontmatter({ name: s.name, description: s.description, version: s.version, uses: s.uses, created: s.created, updated: s.updated, lastUsed: s.lastUsed, refs: s.refs.join(", "), status: s.status, origin: s.origin, wins: s.wins, losses: s.losses, ...(s.invoke === "manual" ? { invoke: "manual" } : {}), ...(s.model ? { model: s.model } : {}), ...(s.lastLoss ? { lastLoss: s.lastLoss } : {}), ...(s.by ? { by: s.by } : {}) }, s.body));
|
|
41
42
|
}
|
|
42
43
|
/** Lê uma skill de um escopo específico. */
|
|
43
44
|
export function getSkillIn(root, name, scope) {
|
|
@@ -60,7 +61,7 @@ export function getBuiltin(name) {
|
|
|
60
61
|
if (!b)
|
|
61
62
|
return null;
|
|
62
63
|
const d = "2026-11-07";
|
|
63
|
-
return { name: b.name, description: b.description, version: 1, uses: 0, created: d, updated: d, lastUsed: d, refs: [], status: "active", origin: "manual", wins: 0, losses: 0, body: b.body, scope: "global", invoke: "manual", model: b.model ?? "", lastLoss: "", builtin: true };
|
|
64
|
+
return { name: b.name, description: b.description, version: 1, uses: 0, created: d, updated: d, lastUsed: d, refs: [], status: "active", origin: "manual", wins: 0, losses: 0, body: b.body, scope: "global", invoke: "manual", model: b.model ?? "", lastLoss: "", by: "", builtin: true };
|
|
64
65
|
}
|
|
65
66
|
/** Embutidas ainda não sobrepostas por uma skill do usuário. */
|
|
66
67
|
export function listBuiltins(root) {
|
|
@@ -74,6 +75,23 @@ function listScope(root, scope) {
|
|
|
74
75
|
.filter((d) => d.isDirectory() && !d.name.startsWith("."))
|
|
75
76
|
.flatMap((d) => { const s = getSkillIn(root, d.name, scope); return s ? [s] : []; });
|
|
76
77
|
}
|
|
78
|
+
/** Skills arquivadas (`.trash`), lidas só para estatística: o placar delas é a evidência de que o gerador errou. */
|
|
79
|
+
export function listArchived(root) {
|
|
80
|
+
return ["project", "global"].flatMap((scope) => {
|
|
81
|
+
const dir = path.join(dirOf(root, scope), ".trash");
|
|
82
|
+
if (!fs.existsSync(dir))
|
|
83
|
+
return [];
|
|
84
|
+
return fs.readdirSync(dir, { withFileTypes: true }).filter((d) => d.isDirectory()).flatMap((d) => {
|
|
85
|
+
try {
|
|
86
|
+
const sk = parse(fs.readFileSync(path.join(dir, d.name, "SKILL.md"), "utf8"), scope);
|
|
87
|
+
return sk ? [sk] : [];
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return [];
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
}
|
|
77
95
|
const DAY = 86_400_000;
|
|
78
96
|
export const daysSince = (date, now = Date.now()) => Math.max(0, Math.floor((now - new Date(date).getTime()) / DAY));
|
|
79
97
|
/** Uso conta, mas skill recente também: sem isso, skills novas (uses=0) cairiam do corte do índice. */
|
|
@@ -194,6 +212,7 @@ export function saveSkill(root, input, scope, opts = {}) {
|
|
|
194
212
|
invoke: input.invoke ?? prev?.invoke ?? "auto",
|
|
195
213
|
model: input.model ?? prev?.model ?? "",
|
|
196
214
|
lastLoss: prev?.lastLoss ?? "",
|
|
215
|
+
by: prev?.by || (opts.auto ? opts.by ?? "" : ""),
|
|
197
216
|
};
|
|
198
217
|
writeSkill(root, skill);
|
|
199
218
|
return { created: !prev, version: skill.version, scope: target };
|
package/dist/skills/trial.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { daysSince, deleteSkill, getSkill, lintSkill, listSkills, savePending, saveSkill, writeSkill } from "./store.js";
|
|
1
|
+
import { daysSince, deleteSkill, getSkill, lintSkill, listArchived, listSkills, savePending, saveSkill, writeSkill } from "./store.js";
|
|
2
2
|
export const skillsMode = () => (process.env.BRITO_SKILLS === "ask" || process.env.BRITO_SKILLS === "off" ? process.env.BRITO_SKILLS : "auto");
|
|
3
3
|
/** Usos bem-sucedidos para promover (com no máximo 1 tropeço a cada 3 sucessos). */
|
|
4
4
|
export const PROMOTE_WINS = 3;
|
|
@@ -50,7 +50,7 @@ export function expireTrials(root, now = Date.now()) {
|
|
|
50
50
|
* - conteúdo com comando destrutivo também vai para a fila de revisão;
|
|
51
51
|
* - nova skill respeita o teto diário.
|
|
52
52
|
*/
|
|
53
|
-
export function saveAuto(root, input, now = Date.now()) {
|
|
53
|
+
export function saveAuto(root, input, now = Date.now(), by = "") {
|
|
54
54
|
const prev = getSkill(root, input.name);
|
|
55
55
|
if (prev && prev.origin === "manual") {
|
|
56
56
|
const scope = savePending(root, input, prev.scope);
|
|
@@ -66,6 +66,35 @@ export function saveAuto(root, input, now = Date.now()) {
|
|
|
66
66
|
if (created >= DAILY_LIMIT)
|
|
67
67
|
return { saved: false, msg: `skill ${input.name} não criada: teto de ${DAILY_LIMIT} skills automáticas por dia` };
|
|
68
68
|
}
|
|
69
|
-
const r = saveSkill(root, input, prev?.scope ?? "project", { auto: true });
|
|
69
|
+
const r = saveSkill(root, input, prev?.scope ?? "project", { auto: true, by });
|
|
70
70
|
return { saved: true, msg: r.created ? `skill ${input.name} criada em teste (project, v1)` : `skill ${input.name} atualizada (${r.scope}, v${r.version})` };
|
|
71
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* Qualidade da reflexão por modelo gerador: das skills automáticas, quantas foram promovidas e quantas arquivadas.
|
|
74
|
+
* Só `origin: auto` entra (as manuais não medem o gerador). Autoria vazia = criada antes do registro.
|
|
75
|
+
*/
|
|
76
|
+
export function skillStats(root) {
|
|
77
|
+
const rows = new Map();
|
|
78
|
+
const row = (by) => rows.get(by) ?? (rows.set(by, { by, created: 0, promoted: 0, trial: 0, archived: 0, wins: 0, losses: 0, promotionRate: null }), rows.get(by));
|
|
79
|
+
const add = (sk, kind) => {
|
|
80
|
+
const r = row(sk.by || "(sem registro)");
|
|
81
|
+
r.created++;
|
|
82
|
+
r.wins += sk.wins;
|
|
83
|
+
r.losses += sk.losses;
|
|
84
|
+
if (kind === "active")
|
|
85
|
+
r.promoted++;
|
|
86
|
+
else if (kind === "trial")
|
|
87
|
+
r.trial++;
|
|
88
|
+
else
|
|
89
|
+
r.archived++;
|
|
90
|
+
};
|
|
91
|
+
for (const s of listSkills(root))
|
|
92
|
+
if (s.origin === "auto")
|
|
93
|
+
add(s, s.status === "trial" ? "trial" : "active");
|
|
94
|
+
for (const s of listArchived(root))
|
|
95
|
+
if (s.origin === "auto")
|
|
96
|
+
add(s, "archived");
|
|
97
|
+
for (const r of rows.values())
|
|
98
|
+
r.promotionRate = r.promoted + r.archived ? r.promoted / (r.promoted + r.archived) : null;
|
|
99
|
+
return [...rows.values()].sort((a, b) => b.created - a.created);
|
|
100
|
+
}
|
package/dist/tools/delegate.js
CHANGED
|
@@ -7,12 +7,38 @@ import { subagentModel } from "../engine.js";
|
|
|
7
7
|
import { initialModel } from "../model.js";
|
|
8
8
|
import { ui } from "../ui/ui.js";
|
|
9
9
|
let seq = 0;
|
|
10
|
-
/** Conjunto de tools de um agente; `delegate` nunca
|
|
10
|
+
/** Conjunto de tools de um agente; `delegate` e `implement` nunca são repassadas (sem recursão), nem `spec_save` (a spec é do principal). */
|
|
11
11
|
function toolSet(def) {
|
|
12
12
|
const names = def.tools === "all" ? allTools().map((t) => t.name) : def.tools;
|
|
13
13
|
// Sem recursão e sem aprender: criar/alterar skills é papel do agente principal.
|
|
14
14
|
// A lista de tarefas é do usuário com o agente principal: um subagente não a reescreve.
|
|
15
|
-
return new Set(names.filter((n) => !["delegate", "skill_save", "skill_delete", "exit_plan", "todo_write"].includes(n)));
|
|
15
|
+
return new Set(names.filter((n) => !["delegate", "implement", "spec_save", "skill_save", "skill_delete", "exit_plan", "todo_write"].includes(n)));
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Roda um subagente em contexto isolado e devolve o texto final com o uso. O subgrafo dos símbolos de `focus` vai junto
|
|
19
|
+
* da tarefa: é a vantagem do grafo, o filho já começa sabendo onde está.
|
|
20
|
+
*/
|
|
21
|
+
export async function runSubagent(def, task, focus, ctx, opts = {}) {
|
|
22
|
+
const label = `${def.name}#${++seq}`;
|
|
23
|
+
const log = opts.log ? (s) => opts.log(`[${label}] ${s}`) : (s) => ui.sub(label, s);
|
|
24
|
+
let seed = "";
|
|
25
|
+
for (const sym of focus) {
|
|
26
|
+
const r = await execTool("graph_context", { symbol: sym }, ctx);
|
|
27
|
+
seed += `\n\n### graph_context(${sym})\n${r.text}`;
|
|
28
|
+
}
|
|
29
|
+
log(`iniciado: ${task.slice(0, 100).replace(/\s+/g, " ")}`);
|
|
30
|
+
const sub = new Agent(buildSubagentPrompt(ctx.root, def), ctx, {
|
|
31
|
+
model: subagentModel(def, ctx.model?.() ?? initialModel(), process.env),
|
|
32
|
+
maxSteps: opts.maxSteps ?? 25,
|
|
33
|
+
quiet: true,
|
|
34
|
+
tools: toolSet(def),
|
|
35
|
+
log,
|
|
36
|
+
sink: opts.sink,
|
|
37
|
+
});
|
|
38
|
+
const text = await sub.send(seed ? `${task}\n\n# Contexto do grafo (pré-carregado)${seed}` : task);
|
|
39
|
+
const tokens = Math.round((sub.usage.input + sub.usage.output) / 1000);
|
|
40
|
+
log(`concluído: ${sub.usage.steps} passos, ~${tokens}k tokens`);
|
|
41
|
+
return { label, text, steps: sub.usage.steps, tokens, model: sub.model };
|
|
16
42
|
}
|
|
17
43
|
/** Agente somente-leitura: nenhuma tool dele escreve arquivos ou exige aprovação. */
|
|
18
44
|
function isReadOnly(def) {
|
|
@@ -40,25 +66,7 @@ defineTool({
|
|
|
40
66
|
const def = getAgent(agent);
|
|
41
67
|
if (!def)
|
|
42
68
|
throw new Error(`Agente desconhecido: ${agent}. Disponíveis: ${listAgents().map((a) => a.name).join(", ")}`);
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
// O subgrafo pré-carregado é a vantagem do grafo: o filho já começa sabendo onde está.
|
|
46
|
-
let seed = "";
|
|
47
|
-
for (const sym of focus) {
|
|
48
|
-
const r = await execTool("graph_context", { symbol: sym }, ctx);
|
|
49
|
-
seed += `\n\n### graph_context(${sym})\n${r.text}`;
|
|
50
|
-
}
|
|
51
|
-
log(`iniciado: ${task.slice(0, 100).replace(/\s+/g, " ")}`);
|
|
52
|
-
const sub = new Agent(buildSubagentPrompt(ctx.root, def), ctx, {
|
|
53
|
-
model: subagentModel(def, ctx.model?.() ?? initialModel(), process.env),
|
|
54
|
-
maxSteps: 25,
|
|
55
|
-
quiet: true,
|
|
56
|
-
tools: toolSet(def),
|
|
57
|
-
log,
|
|
58
|
-
});
|
|
59
|
-
const text = await sub.send(seed ? `${task}\n\n# Contexto do grafo (pré-carregado)${seed}` : task);
|
|
60
|
-
const tokens = Math.round((sub.usage.input + sub.usage.output) / 1000);
|
|
61
|
-
log(`concluído: ${sub.usage.steps} passos, ~${tokens}k tokens`);
|
|
62
|
-
return `[subagente ${label} · ${sub.usage.steps} passos · ~${tokens}k tokens]\n\n${text.slice(0, 12_000)}`;
|
|
69
|
+
const r = await runSubagent(def, task, focus, ctx);
|
|
70
|
+
return `[subagente ${r.label} · ${r.steps} passos · ~${r.tokens}k tokens]\n\n${r.text.slice(0, 12_000)}`;
|
|
63
71
|
},
|
|
64
72
|
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { execFile } from "node:child_process";
|
|
3
|
+
import { defineTool, execTool } from "./registry.js";
|
|
4
|
+
import { runSubagent } from "./delegate.js";
|
|
5
|
+
import { IMPLEMENTER, implStatus, orch } from "../orchestrator.js";
|
|
6
|
+
import { initialModel } from "../model.js";
|
|
7
|
+
/** Saída de um git só de leitura; fora de um repositório (ou sem git), `null`. */
|
|
8
|
+
function git(root, args) {
|
|
9
|
+
return new Promise((resolve) => {
|
|
10
|
+
execFile("git", args, { cwd: root, maxBuffer: 4 * 1024 * 1024 }, (err, stdout) => resolve(err ? null : stdout.trim()));
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
/** Progresso e avisos do implementador; o servidor MCP troca o destino (o stdout de lá é o protocolo). */
|
|
14
|
+
export const implementIo = {};
|
|
15
|
+
defineTool({
|
|
16
|
+
name: "implement",
|
|
17
|
+
description: "Só no modo orquestrador: manda UMA task ao modelo implementador (contexto ISOLADO: ele não vê esta conversa). " +
|
|
18
|
+
"Ele edita os arquivos e roda a verificação; a tool devolve o relato dele, o git status/diff e a saída do `verify`, " +
|
|
19
|
+
"rodado aqui, fora do controle dele. Tasks rodam uma por vez.",
|
|
20
|
+
schema: z.object({
|
|
21
|
+
task: z.string().min(10).describe("task autocontida: objetivo, contratos que não podem mudar e critério de pronto"),
|
|
22
|
+
files: z.array(z.string()).max(20).default([]).describe("arquivos que a task deve criar ou alterar"),
|
|
23
|
+
verify: z.string().optional().describe("comando que prova a task (ex.: npm test -- foo); roda depois do implementador"),
|
|
24
|
+
focus: z.array(z.string()).max(5).default([]).describe("símbolos para pré-carregar o subgrafo"),
|
|
25
|
+
}),
|
|
26
|
+
mutates: true,
|
|
27
|
+
async run({ task, files, verify, focus }, ctx) {
|
|
28
|
+
if (!orch.on)
|
|
29
|
+
return "O modo orquestrador está desligado; implemente você mesmo.";
|
|
30
|
+
const status = implStatus(ctx.model?.() ?? initialModel(), orch.model, process.env);
|
|
31
|
+
if (!status.ready)
|
|
32
|
+
throw new Error(`implementador (${orch.model}) indisponível: ${status.reason}`);
|
|
33
|
+
let full = task;
|
|
34
|
+
if (files.length)
|
|
35
|
+
full += `\n\n# Arquivos\n${files.map((f) => `- ${f}`).join("\n")}`;
|
|
36
|
+
if (verify)
|
|
37
|
+
full += `\n\n# Como verificar\n${verify}`;
|
|
38
|
+
const r = await runSubagent(IMPLEMENTER, full, focus, ctx, { maxSteps: 40, ...implementIo });
|
|
39
|
+
// Fatos que não dependem do relato do implementador: o que mudou no disco e o que a verificação diz.
|
|
40
|
+
const st = await git(ctx.root, ["status", "--short"]);
|
|
41
|
+
const diff = st === null ? null : await git(ctx.root, ["diff", "--stat"]);
|
|
42
|
+
let out = `[implementador ${r.label} · ${status.label} · ${r.steps} passos · ~${r.tokens}k tokens]\n\n${r.text.slice(0, 8_000)}`;
|
|
43
|
+
out += st === null ? "\n\n# git\n(não é um repositório git: confira os arquivos lendo-os)" : `\n\n# git status --short\n${st || "(nada mudou)"}\n\n# git diff --stat\n${diff || "(sem diff em arquivos rastreados)"}`;
|
|
44
|
+
if (verify) {
|
|
45
|
+
const v = await execTool("bash", { command: verify }, ctx);
|
|
46
|
+
out += `\n\n# verificação: ${verify}\n${v.isError ? `[não rodou] ${v.text}` : v.text.slice(-6_000)}`;
|
|
47
|
+
}
|
|
48
|
+
return out;
|
|
49
|
+
},
|
|
50
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { defineTool } from "./registry.js";
|
|
5
|
+
import { orch } from "../orchestrator.js";
|
|
6
|
+
/** Pasta, na raiz do projeto, onde o orquestrador grava a spec e as tasks de cada feature para o usuário avaliar. */
|
|
7
|
+
export const SPEC_DIR = "spec";
|
|
8
|
+
export const SPEC_FILES = ["spec.md", "tasks.md"];
|
|
9
|
+
/**
|
|
10
|
+
* Única escrita do principal no modo orquestrador: só `spec/<feature>/spec.md` e `tasks.md`, nada de código. O caminho
|
|
11
|
+
* é montado aqui (slug + nome fixo), então não há como escapar da pasta.
|
|
12
|
+
*/
|
|
13
|
+
defineTool({
|
|
14
|
+
name: "spec_save",
|
|
15
|
+
description: "Só no modo orquestrador: grava (cria ou sobrescreve) a spec ou a lista de tasks de uma feature em " +
|
|
16
|
+
"spec/<feature>/spec.md ou spec/<feature>/tasks.md, para o usuário avaliar e para o implementador ler. " +
|
|
17
|
+
"Mande o arquivo inteiro em `content`.",
|
|
18
|
+
schema: z.object({
|
|
19
|
+
feature: z.string().regex(/^[a-z0-9][a-z0-9-]{0,63}$/, "use um slug: minúsculas, números e hífens (ex.: login-sso)").describe("slug da feature: nome da pasta em spec/"),
|
|
20
|
+
file: z.enum(SPEC_FILES).describe("spec.md (o quê e por quê) ou tasks.md (lista numerada de tasks)"),
|
|
21
|
+
content: z.string().min(10).describe("conteúdo completo do arquivo em markdown"),
|
|
22
|
+
}),
|
|
23
|
+
dangerous: true,
|
|
24
|
+
edit: true,
|
|
25
|
+
mutates: true,
|
|
26
|
+
describe: (i) => `gravar ${SPEC_DIR}/${i.feature}/${i.file} (${i.content.length} chars)`,
|
|
27
|
+
async run({ feature, file, content }, { root }) {
|
|
28
|
+
if (!orch.on)
|
|
29
|
+
return "O modo orquestrador está desligado; spec_save só existe nele.";
|
|
30
|
+
const rel = `${SPEC_DIR}/${feature}/${file}`;
|
|
31
|
+
const full = path.join(root, SPEC_DIR, feature, file);
|
|
32
|
+
const data = content.endsWith("\n") ? content : content + "\n";
|
|
33
|
+
fs.mkdirSync(path.dirname(full), { recursive: true });
|
|
34
|
+
// Cria com "wx" (falha se já existir) em vez de existsSync + write: checagem e escrita numa operação só, sem TOCTOU.
|
|
35
|
+
let existed = false;
|
|
36
|
+
try {
|
|
37
|
+
fs.writeFileSync(full, data, { flag: "wx" });
|
|
38
|
+
}
|
|
39
|
+
catch (e) {
|
|
40
|
+
if (e.code !== "EEXIST")
|
|
41
|
+
throw e;
|
|
42
|
+
existed = true;
|
|
43
|
+
fs.writeFileSync(full, data);
|
|
44
|
+
}
|
|
45
|
+
return `${existed ? "Atualizado" : "Criado"} ${rel}`;
|
|
46
|
+
},
|
|
47
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { ui, c, fmtTokens } from "../ui/ui.js";
|
|
2
|
+
import { fetchPlanUsage, renderPlanLines } from "./plan.js";
|
|
3
|
+
/** `/usage` do terminal: limites do plano (como o do Claude Code) + tokens/custo desta sessão. */
|
|
4
|
+
export function usageCommand(session) {
|
|
5
|
+
return {
|
|
6
|
+
desc: "consumo do plano (5h/semana) e da sessão",
|
|
7
|
+
run: async () => {
|
|
8
|
+
const r = await fetchPlanUsage({ force: true });
|
|
9
|
+
if (r.ok) {
|
|
10
|
+
ui.print(` ${c.soft(`Plano${r.data.plan ? ` ${r.data.plan}` : ""}`)}`);
|
|
11
|
+
for (const l of renderPlanLines(r.data))
|
|
12
|
+
ui.print(` ${c.muted(l)}`);
|
|
13
|
+
}
|
|
14
|
+
else
|
|
15
|
+
ui.print(` ${c.muted(`Plano: ${r.reason}`)}`);
|
|
16
|
+
const s = session();
|
|
17
|
+
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)}` : ""}`)}`);
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { execFileSync } from "node:child_process";
|
|
5
|
+
/**
|
|
6
|
+
* Consumo do plano (assinatura Claude): janela de 5h, semana e uso extra, o mesmo que o `/usage` do Claude Code mostra.
|
|
7
|
+
* Vem de um endpoint interno da Anthropic, autenticado com o token OAuth que o `claude` já guarda (somente leitura:
|
|
8
|
+
* não renovamos nem gravamos o token). Se o endpoint mudar, devolvemos um erro legível — nunca quebramos o resto.
|
|
9
|
+
*/
|
|
10
|
+
const URL_USAGE = "https://api.anthropic.com/api/oauth/usage";
|
|
11
|
+
const CACHE_MS = 60_000;
|
|
12
|
+
function tokenFromFile(file) {
|
|
13
|
+
try {
|
|
14
|
+
const o = JSON.parse(fs.readFileSync(file, "utf8"))?.claudeAiOauth;
|
|
15
|
+
return o?.accessToken ? { token: o.accessToken, plan: o.subscriptionType, expiresAt: o.expiresAt } : null;
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/** Token do `claude`: ~/.claude/.credentials.json (Windows/Linux) ou o Keychain (macOS). */
|
|
22
|
+
export function readOAuth() {
|
|
23
|
+
const dir = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), ".claude");
|
|
24
|
+
const f = tokenFromFile(path.join(dir, ".credentials.json"));
|
|
25
|
+
if (f || process.platform !== "darwin")
|
|
26
|
+
return f;
|
|
27
|
+
try {
|
|
28
|
+
const raw = execFileSync("security", ["find-generic-password", "-s", "Claude Code-credentials", "-w"], { encoding: "utf8", timeout: 5000, stdio: ["ignore", "pipe", "ignore"] });
|
|
29
|
+
const o = JSON.parse(raw)?.claudeAiOauth;
|
|
30
|
+
return o?.accessToken ? { token: o.accessToken, plan: o.subscriptionType, expiresAt: o.expiresAt } : null;
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const LABELS = {
|
|
37
|
+
session: "Sessão (5h)", weekly_all: "Semana (todos os modelos)", weekly_opus: "Semana · Opus", weekly_sonnet: "Semana · Sonnet",
|
|
38
|
+
};
|
|
39
|
+
/** Normaliza a resposta; tolera campos ausentes e ignora o que não reconhece. */
|
|
40
|
+
export function parsePlanUsage(j, plan) {
|
|
41
|
+
const buckets = [];
|
|
42
|
+
const num = (v) => (typeof v === "number" && Number.isFinite(v) ? v : null);
|
|
43
|
+
if (Array.isArray(j?.limits) && j.limits.length) {
|
|
44
|
+
for (const l of j.limits) {
|
|
45
|
+
const p = num(l?.percent);
|
|
46
|
+
if (p === null)
|
|
47
|
+
continue;
|
|
48
|
+
buckets.push({ label: LABELS[l.kind] ?? String(l.kind), percent: p, resetsAt: l.resets_at ?? undefined });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
for (const [key, label] of [["five_hour", LABELS.session], ["seven_day", LABELS.weekly_all], ["seven_day_opus", LABELS.weekly_opus], ["seven_day_sonnet", LABELS.weekly_sonnet]]) {
|
|
53
|
+
const p = num(j?.[key]?.utilization);
|
|
54
|
+
if (p !== null)
|
|
55
|
+
buckets.push({ label, percent: p, resetsAt: j[key].resets_at ?? undefined });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const e = j?.extra_usage;
|
|
59
|
+
const dec = 10 ** (num(e?.decimal_places) ?? 2);
|
|
60
|
+
const extra = e?.is_enabled && num(e?.monthly_limit) !== null
|
|
61
|
+
? { used: (num(e.used_credits) ?? 0) / dec, limit: e.monthly_limit / dec, percent: num(e.utilization) ?? 0, currency: String(e.currency ?? "") }
|
|
62
|
+
: undefined;
|
|
63
|
+
return { plan, buckets, extra };
|
|
64
|
+
}
|
|
65
|
+
let cache = null;
|
|
66
|
+
export async function fetchPlanUsage(opts = {}) {
|
|
67
|
+
if (!opts.force && cache && Date.now() - cache.at < CACHE_MS)
|
|
68
|
+
return cache.result;
|
|
69
|
+
const auth = readOAuth();
|
|
70
|
+
let result;
|
|
71
|
+
if (!auth)
|
|
72
|
+
result = { ok: false, reason: "sem login do Claude por assinatura (rode `claude` e entre com /login); com ANTHROPIC_API_KEY não há limite de plano a mostrar" };
|
|
73
|
+
else if (auth.expiresAt && auth.expiresAt < Date.now())
|
|
74
|
+
result = { ok: false, reason: "o login do claude expirou: abra o `claude` uma vez para renová-lo" };
|
|
75
|
+
else {
|
|
76
|
+
try {
|
|
77
|
+
const r = await (opts.fetchImpl ?? fetch)(URL_USAGE, {
|
|
78
|
+
headers: { Authorization: `Bearer ${auth.token}`, "anthropic-beta": "oauth-2025-04-20", "User-Agent": "brito-cli" },
|
|
79
|
+
signal: AbortSignal.timeout(8000),
|
|
80
|
+
});
|
|
81
|
+
result = r.ok ? { ok: true, data: parsePlanUsage(await r.json(), auth.plan) }
|
|
82
|
+
: { ok: false, reason: r.status === 401 || r.status === 403 ? "o login do claude não vale mais: abra o `claude` para renová-lo" : `a Anthropic respondeu ${r.status}` };
|
|
83
|
+
}
|
|
84
|
+
catch (e) {
|
|
85
|
+
result = { ok: false, reason: `não consegui consultar o plano (${e?.message ?? e})` };
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (result.ok)
|
|
89
|
+
cache = { at: Date.now(), result }; // falhas não ficam em cache
|
|
90
|
+
return result;
|
|
91
|
+
}
|
|
92
|
+
/** "em 2h 10min" / "em 3d 4h" a partir de um ISO. */
|
|
93
|
+
export function resetIn(iso, now = Date.now()) {
|
|
94
|
+
const t = iso ? Date.parse(iso) : NaN;
|
|
95
|
+
if (!Number.isFinite(t))
|
|
96
|
+
return "";
|
|
97
|
+
const min = Math.max(0, Math.round((t - now) / 60_000));
|
|
98
|
+
if (min < 60)
|
|
99
|
+
return `${min}min`;
|
|
100
|
+
if (min < 48 * 60)
|
|
101
|
+
return `${Math.floor(min / 60)}h ${String(min % 60).padStart(2, "0")}min`;
|
|
102
|
+
return `${Math.floor(min / 1440)}d ${Math.floor((min % 1440) / 60)}h`;
|
|
103
|
+
}
|
|
104
|
+
/** Linhas de texto para o terminal (sem cor; quem chama pinta). */
|
|
105
|
+
export function renderPlanLines(d, now = Date.now()) {
|
|
106
|
+
const bar = (p) => { const n = Math.round(Math.min(100, Math.max(0, p)) / 5); return "█".repeat(n) + "░".repeat(20 - n); };
|
|
107
|
+
const out = d.buckets.map((b) => `${b.label.padEnd(28)} [${bar(b.percent)}] ${String(Math.round(b.percent)).padStart(3)}%${b.resetsAt ? ` · reseta em ${resetIn(b.resetsAt, now)}` : ""}`);
|
|
108
|
+
if (d.extra)
|
|
109
|
+
out.push(`${"Uso extra (mês)".padEnd(28)} [${bar(d.extra.percent)}] ${String(Math.round(d.extra.percent)).padStart(3)}% · ${d.extra.used.toFixed(2)} de ${d.extra.limit.toFixed(2)} ${d.extra.currency}`);
|
|
110
|
+
return out.length ? out : ["(a Anthropic não devolveu limites para esta conta)"];
|
|
111
|
+
}
|
package/package.json
CHANGED
|
@@ -1,80 +1,81 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "brito-cli",
|
|
3
|
-
"version": "0.
|
|
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
|
-
"type": "module",
|
|
6
|
-
"bin": {
|
|
7
|
-
"brito": "dist/cli.js"
|
|
8
|
-
},
|
|
9
|
-
"scripts": {
|
|
10
|
-
"dev": "tsx src/cli.ts",
|
|
11
|
-
"build": "tsc",
|
|
12
|
-
"typecheck": "tsc --noEmit",
|
|
13
|
-
"test": "tsx --test test/*.test.ts",
|
|
14
|
-
"eval:learn": "tsx test/eval-learn.ts",
|
|
15
|
-
"prepare": "tsc",
|
|
16
|
-
"prepublishOnly": "npm run typecheck && npm test",
|
|
17
|
-
"desktop": "npm run build && npm run bundle && npm --prefix desktop install --no-audit --no-fund && npm --prefix desktop start",
|
|
18
|
-
"bundle": "node build-bundle.mjs",
|
|
19
|
-
"desktop:app": "npm run build && npm run bundle && npm --prefix desktop install --no-audit --no-fund && node desktop/package.mjs --install",
|
|
20
|
-
"desktop:dmg": "npm run build && npm run bundle && npm --prefix desktop install --no-audit --no-fund && node desktop/package.mjs --dmg"
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
"@
|
|
25
|
-
"@
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
"
|
|
41
|
-
|
|
42
|
-
"
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
"
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
"
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
"
|
|
55
|
-
".
|
|
56
|
-
"
|
|
57
|
-
"README.
|
|
58
|
-
"
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
"
|
|
63
|
-
"
|
|
64
|
-
"
|
|
65
|
-
"
|
|
66
|
-
"
|
|
67
|
-
"
|
|
68
|
-
"
|
|
69
|
-
"
|
|
70
|
-
"
|
|
71
|
-
"
|
|
72
|
-
"
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "brito-cli",
|
|
3
|
+
"version": "0.10.0",
|
|
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
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"brito": "dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"dev": "tsx src/cli.ts",
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"typecheck": "tsc --noEmit",
|
|
13
|
+
"test": "tsx --test test/*.test.ts",
|
|
14
|
+
"eval:learn": "tsx test/eval-learn.ts",
|
|
15
|
+
"prepare": "tsc",
|
|
16
|
+
"prepublishOnly": "npm run typecheck && npm test",
|
|
17
|
+
"desktop": "npm run build && npm run bundle && npm --prefix desktop install --no-audit --no-fund && npm --prefix desktop start",
|
|
18
|
+
"bundle": "node build-bundle.mjs",
|
|
19
|
+
"desktop:app": "npm run build && npm run bundle && npm --prefix desktop install --no-audit --no-fund && node desktop/package.mjs --install",
|
|
20
|
+
"desktop:dmg": "npm run build && npm run bundle && npm --prefix desktop install --no-audit --no-fund && node desktop/package.mjs --dmg",
|
|
21
|
+
"release": "node scripts/release.mjs"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@anthropic-ai/sdk": "^1.5.0",
|
|
25
|
+
"@ladybugdb/core": "^0.22.3",
|
|
26
|
+
"@modelcontextprotocol/sdk": "^1.32.2",
|
|
27
|
+
"commander": "^14.0.3",
|
|
28
|
+
"string-width": "^8.3.0",
|
|
29
|
+
"tinyglobby": "^0.2.17",
|
|
30
|
+
"tree-sitter-wasms": "^0.1.13",
|
|
31
|
+
"web-tree-sitter": "0.24.7",
|
|
32
|
+
"zod": "^4.6.5"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@types/node": "^26.6.6",
|
|
36
|
+
"esbuild": "^0.28.2",
|
|
37
|
+
"tsx": "latest",
|
|
38
|
+
"typescript": "latest"
|
|
39
|
+
},
|
|
40
|
+
"license": "PolyForm-Noncommercial-1.0.0",
|
|
41
|
+
"repository": {
|
|
42
|
+
"type": "git",
|
|
43
|
+
"url": "git+https://github.com/wilsonbrito/brito.git"
|
|
44
|
+
},
|
|
45
|
+
"homepage": "https://github.com/wilsonbrito/brito#readme",
|
|
46
|
+
"bugs": {
|
|
47
|
+
"url": "https://github.com/wilsonbrito/brito/issues"
|
|
48
|
+
},
|
|
49
|
+
"author": "Wilson Brito",
|
|
50
|
+
"engines": {
|
|
51
|
+
"node": ">=20"
|
|
52
|
+
},
|
|
53
|
+
"files": [
|
|
54
|
+
"dist",
|
|
55
|
+
"!dist/cli.bundle.mjs",
|
|
56
|
+
".brito/agents",
|
|
57
|
+
"README.md",
|
|
58
|
+
"README.pt-BR.md",
|
|
59
|
+
"LICENSE"
|
|
60
|
+
],
|
|
61
|
+
"keywords": [
|
|
62
|
+
"cli",
|
|
63
|
+
"agent",
|
|
64
|
+
"coding-agent",
|
|
65
|
+
"llm",
|
|
66
|
+
"knowledge-graph",
|
|
67
|
+
"code-graph",
|
|
68
|
+
"mcp",
|
|
69
|
+
"claude",
|
|
70
|
+
"tree-sitter",
|
|
71
|
+
"svelte",
|
|
72
|
+
"typescript",
|
|
73
|
+
"python"
|
|
74
|
+
],
|
|
75
|
+
"overrides": {
|
|
76
|
+
"tar": "^7.5.22"
|
|
77
|
+
},
|
|
78
|
+
"optionalDependencies": {
|
|
79
|
+
"node-llama-cpp": "^3.22.3"
|
|
80
|
+
}
|
|
81
|
+
}
|