brito-cli 0.6.1 → 0.6.4
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 +15 -1
- package/README.pt-BR.md +15 -1
- package/dist/agent/claude-cli.js +3 -3
- package/dist/agent/context.js +2 -2
- package/dist/agent/loop.js +2 -2
- package/dist/permissions.js +2 -2
- package/dist/repl.js +18 -14
- package/dist/ui/dock.js +2 -2
- package/dist/ui/table.js +102 -0
- package/dist/ui/ui.js +89 -18
- package/dist/util/images.js +10 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -27,6 +27,10 @@ Brito is an agent for software development. Use it in the terminal (`brito-cli`
|
|
|
27
27
|
|
|
28
28
|
## How it works
|
|
29
29
|
|
|
30
|
+

|
|
31
|
+
|
|
32
|
+
*The graph is built once and updated incrementally; a question like "what breaks if I change this?" becomes one or two graph calls.*
|
|
33
|
+
|
|
30
34
|
```mermaid
|
|
31
35
|
flowchart LR
|
|
32
36
|
code["Your project<br/>.ts · .js · .py · .java · .kt · .go · .rb · .php · .cs · .rs · .svelte · .vue · .astro"] -->|"tree-sitter<br/>incremental"| cg[("Code graph<br/>symbols · calls · imports")]
|
|
@@ -47,8 +51,18 @@ flowchart LR
|
|
|
47
51
|
- **Skills**: step-by-step procedures for *your* project (commands, order, traps, how to verify). The agent loads one with `skill_load` before a task it covers.
|
|
48
52
|
- **Journal**: Brito remembers across sessions which tasks repeated, which is the best hint that a procedure is general and worth saving.
|
|
49
53
|
|
|
54
|
+
### Architecture at a glance
|
|
55
|
+
|
|
56
|
+

|
|
57
|
+
|
|
58
|
+
*Everything the model sees or can do is one of eight components, grouped by role. The mono-spaced text is the real file or command; the pill says when it fires.*
|
|
59
|
+
|
|
50
60
|
## Skills that earn their place
|
|
51
61
|
|
|
62
|
+

|
|
63
|
+
|
|
64
|
+
*The two paths a skill can take. Safe ones prove themselves in trial; risky ones wait for your approval.*
|
|
65
|
+
|
|
52
66
|

|
|
53
67
|
|
|
54
68
|
0. **Autonomous path (default).** The reflection saves a new skill as *trial*, marked "(em teste)" in the index. Each turn that loads it counts as a win (no error-then-fix, no user correction) or a loss. 3 wins (and at most 1 loss per 3 wins) promote it; 2 losses without more wins, or 21 days unused, archive it to `.brito/skills/.trash`. Brito prints one line per promotion or archive. Limits: at most 3 new automatic skills a day, project scope only, and a weekly automatic consolidation that only touches automatic skills.
|
|
@@ -133,7 +147,7 @@ OK: 20 arquivos, 66 símbolos, 54 chamadas, 17 imports | +0 ~0 -0 =20 (28ms)
|
|
|
133
147
|
|---|---|
|
|
134
148
|
| `/help` | list commands |
|
|
135
149
|
| `/model`, `/effort`, `/reasoning` | choose model, reasoning effort and thinking (session, global or once) |
|
|
136
|
-
| `/modo` or `Shift+Tab` | switch permission mode: manual → auto-edits → plan |
|
|
150
|
+
| `/modo` or `Shift+Tab` | switch permission mode: manual → auto-edits → plan → bypass ("everything allowed") |
|
|
137
151
|
| `/skills` | list skills · `pending` · `approve <name>` · `reject <name>` · `consolidate` · `rm <name>` · `promote <name>` |
|
|
138
152
|
| `/learn` | extract skills from the last turn now |
|
|
139
153
|
| `/agents`, `/tools`, `/index`, `/context` | inspect subagents, tools, the index and the context window |
|
package/README.pt-BR.md
CHANGED
|
@@ -27,6 +27,10 @@ O Brito é um agente para desenvolvimento de software. Use no terminal (`brito-c
|
|
|
27
27
|
|
|
28
28
|
## Como funciona
|
|
29
29
|
|
|
30
|
+

|
|
31
|
+
|
|
32
|
+
*O grafo é construído uma vez e atualizado de forma incremental; uma pergunta como "o que quebra se eu mudar isto?" vira uma ou duas chamadas ao grafo.*
|
|
33
|
+
|
|
30
34
|
```mermaid
|
|
31
35
|
flowchart LR
|
|
32
36
|
code["Seu projeto<br/>.ts · .js · .py · .java · .kt · .go · .rb · .php · .cs · .rs · .svelte · .vue · .astro"] -->|"tree-sitter<br/>incremental"| cg[("Grafo de código<br/>símbolos · chamadas · imports")]
|
|
@@ -43,8 +47,18 @@ flowchart LR
|
|
|
43
47
|
- **Skills**: procedimentos passo a passo para o *seu* projeto (comandos, ordem, armadilhas, como verificar). O agente carrega uma com `skill_load` antes de uma tarefa que ela cobre.
|
|
44
48
|
- **Journal**: o Brito lembra entre sessões quais tarefas se repetiram, o melhor indício de que um procedimento é geral e vale guardar.
|
|
45
49
|
|
|
50
|
+
### Arquitetura em uma imagem
|
|
51
|
+
|
|
52
|
+

|
|
53
|
+
|
|
54
|
+
*Tudo o que o modelo vê ou pode fazer é um dos oito componentes, agrupados por papel. O texto em monoespaçado é o arquivo ou comando real; a etiqueta diz quando ele entra em ação.*
|
|
55
|
+
|
|
46
56
|
## Skills que merecem o lugar
|
|
47
57
|
|
|
58
|
+

|
|
59
|
+
|
|
60
|
+
*Os dois caminhos de uma skill. As seguras se provam em teste; as arriscadas esperam a sua aprovação.*
|
|
61
|
+
|
|
48
62
|

|
|
49
63
|
|
|
50
64
|
0. **Caminho autônomo (padrão).** A reflexão grava a skill nova como *em teste*, marcada "(em teste)" no índice. Cada turno que a carrega conta como vitória (sem erro→conserto nem correção do usuário) ou falha. 3 vitórias (e no máximo 1 falha a cada 3 vitórias) a promovem; 2 falhas sem mais vitórias, ou 21 dias sem uso, a arquivam em `.brito/skills/.trash`. O Brito imprime uma linha a cada promoção ou arquivamento. Limites: no máximo 3 skills automáticas novas por dia, só no escopo do projeto, e uma consolidação semanal automática que só mexe em skills automáticas.
|
|
@@ -129,7 +143,7 @@ OK: 20 arquivos, 66 símbolos, 54 chamadas, 17 imports | +0 ~0 -0 =20 (28ms)
|
|
|
129
143
|
|---|---|
|
|
130
144
|
| `/help` | lista os comandos |
|
|
131
145
|
| `/model`, `/effort`, `/reasoning` | escolhem modelo, esforço de raciocínio e thinking (sessão, global ou uma vez) |
|
|
132
|
-
| `/modo` ou `Shift+Tab` | troca o modo de permissão: manual → auto-edições → plano |
|
|
146
|
+
| `/modo` ou `Shift+Tab` | troca o modo de permissão: manual → auto-edições → plano → tudo liberado |
|
|
133
147
|
| `/skills` | lista skills · `pending` · `approve <nome>` · `reject <nome>` · `consolidate` · `rm <nome>` · `promote <nome>` |
|
|
134
148
|
| `/learn` | extrai skills do último turno agora |
|
|
135
149
|
| `/agents`, `/tools`, `/index`, `/context` | inspecionam subagentes, tools, o índice e a janela de contexto |
|
package/dist/agent/claude-cli.js
CHANGED
|
@@ -144,11 +144,11 @@ export class ClaudeSession {
|
|
|
144
144
|
ui.turnStart();
|
|
145
145
|
const { ok: imgs, tooBig } = splitOversize(findImages(text, this.o.root));
|
|
146
146
|
for (const i of tooBig)
|
|
147
|
-
ui.warn(`${i.name} passa
|
|
147
|
+
ui.warn(`${i.name} passa do limite da API e não foi anexado`);
|
|
148
148
|
if (imgs.length)
|
|
149
|
-
ui.info(
|
|
149
|
+
ui.info(`📎 ${imgs.map((i) => `${i.name} (${fmtBytes(i.bytes)})`).join(", ")} anexado${imgs.length > 1 ? "s" : ""}`);
|
|
150
150
|
// O claude vê a imagem pela tool Read: o prompt diz quais arquivos são imagens anexadas pelo usuário.
|
|
151
|
-
const prompt = imgs.length ? `${text}\n\n[O usuário anexou ${imgs.length > 1 ? "
|
|
151
|
+
const prompt = imgs.length ? `${text}\n\n[O usuário anexou ${imgs.length > 1 ? "estes arquivos" : "este arquivo"}; abra com a tool Read para vê-${imgs.length > 1 ? "las" : "la"}:\n${imgs.map((i) => `- ${i.path}`).join("\n")}]` : text;
|
|
152
152
|
const cmd = claudeCommand(this.args(prompt, [...new Set(imgs.map((i) => path.dirname(i.path)))]));
|
|
153
153
|
const child = (this.child = spawn(cmd.command, cmd.args, {
|
|
154
154
|
cwd: this.o.root, stdio: ["ignore", "pipe", "pipe"],
|
package/dist/agent/context.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Imagem em base64 tem megabytes, mas custa ~1,6 mil tokens: troca o conteúdo por um marcador antes de medir ou resumir. */
|
|
2
|
-
const stubImages = (_k, v) => (v && typeof v === "object" && v.type === "image" ? { type:
|
|
2
|
+
const stubImages = (_k, v) => (v && typeof v === "object" && (v.type === "image" || v.type === "document") && v.source ? { type: v.type, note: "x".repeat(6400) } : v);
|
|
3
3
|
export const estimateTokens = (msgs) => Math.ceil(JSON.stringify(msgs, stubImages).length / 4);
|
|
4
4
|
const MAX_OLD_RESULT = 1500;
|
|
5
5
|
/** Fase 1 (barata): encurta tool_results antigos, preservando as últimas `keep` mensagens. */
|
|
@@ -35,7 +35,7 @@ export async function summarizeMiddle(client, model, msgs, keepTail = 6) {
|
|
|
35
35
|
model,
|
|
36
36
|
max_tokens: 2000,
|
|
37
37
|
system: "Resuma a conversa de um agente de programação. Preserve: objetivo, decisões, arquivos/símbolos tocados, erros encontrados, estado atual e próximos passos. Seja denso.",
|
|
38
|
-
messages: [{ role: "user", content: JSON.stringify(middle, (_k, v) => (v && typeof v === "object" && v.type === "image" ? { type:
|
|
38
|
+
messages: [{ role: "user", content: JSON.stringify(middle, (_k, v) => (v && typeof v === "object" && (v.type === "image" || v.type === "document") && v.source ? { type: v.type, note: "[anexo omitido]" } : v)).slice(0, 150_000) }],
|
|
39
39
|
});
|
|
40
40
|
const summary = res.content.map((b) => (b.type === "text" ? b.text : "")).join("");
|
|
41
41
|
return [...head, { role: "user", content: `[Resumo da conversa anterior]\n${summary}` }, { role: "assistant", content: "Entendido, continuando a partir do resumo." }, ...tail];
|
package/dist/agent/loop.js
CHANGED
|
@@ -63,9 +63,9 @@ export class Agent {
|
|
|
63
63
|
// (usado na reflexão) guarda só o texto, para não carregar base64.
|
|
64
64
|
const { ok, tooBig } = splitOversize(findImages(userText, this.ctx.root));
|
|
65
65
|
for (const i of tooBig)
|
|
66
|
-
this.log(`⚠ ${i.name} passa
|
|
66
|
+
this.log(`⚠ ${i.name} passa do limite da API e não foi anexado`);
|
|
67
67
|
if (ok.length) {
|
|
68
|
-
this.log(
|
|
68
|
+
this.log(`📎 ${ok.map((i) => `${i.name} (${fmtBytes(i.bytes)})`).join(", ")} anexado${ok.length > 1 ? "s" : ""}`);
|
|
69
69
|
this.messages.push({ role: "user", content: [...ok.map(imageBlock), { type: "text", text: userText }] });
|
|
70
70
|
turn.messages.push({ role: "user", content: userText });
|
|
71
71
|
}
|
package/dist/permissions.js
CHANGED
|
@@ -6,8 +6,8 @@ export const perms = { mode: "manual" };
|
|
|
6
6
|
*/
|
|
7
7
|
export const MODE_ENV = "BRITO_PERMS_MODE";
|
|
8
8
|
export const modeFromEnv = (v) => ["manual", "auto", "plan", "bypass"].find((m) => m === v);
|
|
9
|
-
const CYCLE = ["manual", "auto", "plan"];
|
|
10
|
-
/** Próximo modo do ciclo; `bypass` (
|
|
9
|
+
export const CYCLE = ["manual", "auto", "plan", "bypass"];
|
|
10
|
+
/** Próximo modo do ciclo; depois de `bypass` (tudo liberado) volta para manual. */
|
|
11
11
|
export function cycleMode() {
|
|
12
12
|
perms.mode = CYCLE[(CYCLE.indexOf(perms.mode) + 1) % CYCLE.length];
|
|
13
13
|
return perms.mode;
|
package/dist/repl.js
CHANGED
|
@@ -39,11 +39,14 @@ export async function runRepl(b, o) {
|
|
|
39
39
|
const modeColor = { manual: c.muted, auto: c.ok, plan: c.warn, bypass: c.err };
|
|
40
40
|
const modeChip = () => { const m = MODE_LABEL[perms.mode]; return modeColor[perms.mode](`${m.icon} ${m.name}`); };
|
|
41
41
|
const status = () => ui.statusLine({
|
|
42
|
+
project: path.basename(o.root),
|
|
42
43
|
mode: modeChip(),
|
|
43
44
|
model: b.model(), ctxTokens: b.contextTokens(), ctxWindow: b.ctxWindow,
|
|
44
45
|
input: b.usage().input, output: b.usage().output, cacheRead: b.usage().cacheRead, startedAt: o.startedAt, cost: b.cost?.(),
|
|
45
46
|
extra: `${skillCount} ${skillCount === 1 ? "skill" : "skills"}`,
|
|
46
47
|
});
|
|
48
|
+
if (interactive && process.stdout.isTTY)
|
|
49
|
+
process.stdout.write(`\x1b]0;brito · ${path.basename(o.root)}\x07`);
|
|
47
50
|
const dock = interactive ? new Dock({ status, commands: menu, historyFile: path.join(globalHome(), "history") }) : null;
|
|
48
51
|
if (dock) {
|
|
49
52
|
await dock.start();
|
|
@@ -90,7 +93,7 @@ export async function runRepl(b, o) {
|
|
|
90
93
|
"/help": () => {
|
|
91
94
|
for (const m of menu)
|
|
92
95
|
ui.print(` ${c.soft(m.name.padEnd(12))} ${c.muted(m.desc)}`);
|
|
93
|
-
ui.print(` ${c.soft("Shift+Tab".padEnd(12))} ${c.muted("alterna modo: manual → auto-edições → plano")}`);
|
|
96
|
+
ui.print(` ${c.soft("Shift+Tab".padEnd(12))} ${c.muted("alterna modo: manual → auto-edições → plano → tudo liberado")}`);
|
|
94
97
|
ui.print(` ${c.soft("!comando".padEnd(12))} ${c.muted("roda no shell, sem chamar o modelo")}`);
|
|
95
98
|
ui.print(c.dim(" Enter envia · Alt+Enter nova linha · ↑↓ histórico · Tab completa · Ctrl+C interrompe"));
|
|
96
99
|
},
|
|
@@ -106,13 +109,13 @@ export async function runRepl(b, o) {
|
|
|
106
109
|
"/indicadores": () => { for (const l of ui.statusLegend())
|
|
107
110
|
ui.print(l); },
|
|
108
111
|
"/modo": ([m]) => {
|
|
109
|
-
const pick = { manual: "manual", auto: "auto", plano: "plan", plan: "plan" };
|
|
112
|
+
const pick = { manual: "manual", auto: "auto", plano: "plan", plan: "plan", tudo: "bypass", bypass: "bypass" };
|
|
110
113
|
if (m && pick[m]) {
|
|
111
114
|
perms.mode = pick[m];
|
|
112
115
|
dock?.repaint();
|
|
113
116
|
}
|
|
114
117
|
else if (m) {
|
|
115
|
-
ui.warn("use: /modo manual | auto | plano");
|
|
118
|
+
ui.warn("use: /modo manual | auto | plano | tudo");
|
|
116
119
|
return;
|
|
117
120
|
}
|
|
118
121
|
const i = MODE_LABEL[perms.mode];
|
|
@@ -150,13 +153,13 @@ export async function runRepl(b, o) {
|
|
|
150
153
|
const echo = (text) => {
|
|
151
154
|
const lines = text.split("\n");
|
|
152
155
|
const shown = lines.length > 4 ? [...lines.slice(0, 3), c.dim(`… (+${lines.length - 3} linhas)`)] : lines;
|
|
153
|
-
// balão do usuário: mesmo formato da caixa do Brito, em vermelho e com o nome de usuário da máquina
|
|
156
|
+
// balão do usuário (sem borda lateral, para copiar limpo): mesmo formato da caixa do Brito, em vermelho e com o nome de usuário da máquina
|
|
154
157
|
const label = ` ${userName()} `;
|
|
155
|
-
const room = Math.max(10, cols() -
|
|
158
|
+
const room = Math.max(10, cols() - 1);
|
|
156
159
|
const body = shown.flatMap((l) => (l.length <= room ? [l] : l.match(new RegExp(`.{1,${room}}`, "gs")) ?? [l]));
|
|
157
|
-
ui.print("\n" + c.err("
|
|
158
|
-
body.map((l) => c.
|
|
159
|
-
c.err("
|
|
160
|
+
ui.print("\n" + c.err("──") + c.bold(c.err(label)) + c.err("─".repeat(Math.max(2, cols() - label.length - 2))) + "\n" +
|
|
161
|
+
body.map((l) => c.bold(c.err(l))).join("\n") + "\n" +
|
|
162
|
+
c.err("─".repeat(cols())));
|
|
160
163
|
};
|
|
161
164
|
const shutdown = async () => {
|
|
162
165
|
if (closing)
|
|
@@ -185,15 +188,16 @@ export async function runRepl(b, o) {
|
|
|
185
188
|
await shutdown();
|
|
186
189
|
return;
|
|
187
190
|
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
if (!run) {
|
|
191
|
-
ui.warn(`comando desconhecido: ${cmd}. Digite /help.`);
|
|
192
|
-
return;
|
|
193
|
-
}
|
|
191
|
+
const run = line.startsWith("/") ? common[key] ?? b.commands[key]?.run : undefined;
|
|
192
|
+
if (run) {
|
|
194
193
|
await run(args);
|
|
195
194
|
return;
|
|
196
195
|
}
|
|
196
|
+
// "/Users/x/arquivo.pdf" é caminho, não comando: segue como mensagem ao agente
|
|
197
|
+
if (line.startsWith("/") && !cmd.slice(1).includes("/")) {
|
|
198
|
+
ui.warn(`comando desconhecido: ${cmd}. Digite /help.`);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
197
201
|
const ac = (running = new AbortController());
|
|
198
202
|
dock?.setBusy(true);
|
|
199
203
|
try {
|
package/dist/ui/dock.js
CHANGED
|
@@ -8,7 +8,7 @@ const graphemes = (s) => Array.from(seg.segment(s), (x) => x.segment);
|
|
|
8
8
|
const ANSI = /\x1b\[[0-9;]*[A-Za-z]/y;
|
|
9
9
|
const MAX_INPUT_ROWS = 8;
|
|
10
10
|
const MAX_MENU = 7;
|
|
11
|
-
const PASTE_RE = /\[(?:colado|imagem) #(\d+): [^\]]*\]/g;
|
|
11
|
+
const PASTE_RE = /\[(?:colado|imagem|anexo) #(\d+): [^\]]*\]/g;
|
|
12
12
|
export class Dock {
|
|
13
13
|
opts;
|
|
14
14
|
onSubmit = () => { };
|
|
@@ -415,7 +415,7 @@ export class Dock {
|
|
|
415
415
|
if (imgs.length) {
|
|
416
416
|
const n = ++this.pasteN;
|
|
417
417
|
this.pastes.set(n, imgs.map((i) => (/\s/.test(i.path) ? `"${i.path}"` : i.path)).join(" "));
|
|
418
|
-
this.insert(`[
|
|
418
|
+
this.insert(`[anexo #${n}: ${imgs.length === 1 ? imgs[0].name : `${imgs.length} arquivos`}] `);
|
|
419
419
|
}
|
|
420
420
|
else if (lines >= 2 || text.length > 300) {
|
|
421
421
|
const n = ++this.pasteN;
|
package/dist/ui/table.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import stringWidth from "string-width";
|
|
2
|
+
import { c } from "./theme.js";
|
|
3
|
+
/** Separa as células de uma linha `| a | b |` (aceita `\|` dentro da célula). */
|
|
4
|
+
function cells(line) {
|
|
5
|
+
let s = line.trim();
|
|
6
|
+
if (s.startsWith("|"))
|
|
7
|
+
s = s.slice(1);
|
|
8
|
+
if (s.endsWith("|") && !s.endsWith("\\|"))
|
|
9
|
+
s = s.slice(0, -1);
|
|
10
|
+
return s.split(/(?<!\\)\|/).map((x) => x.replace(/\\\|/g, "|").trim());
|
|
11
|
+
}
|
|
12
|
+
const isSep = (row) => row.length > 0 && row.every((x) => /^:?-{1,}:?$/.test(x.replace(/\s/g, "")));
|
|
13
|
+
/** Tira a marcação inline (**negrito**, *itálico*, `código`) — a tabela é desenhada sem ela. */
|
|
14
|
+
const plain = (s) => s.replace(/\*\*(.+?)\*\*/g, "$1").replace(/__(.+?)__/g, "$1").replace(/(^|[^\w*])\*(?!\s)(.+?)\*(?!\w)/g, "$1$2").replace(/`([^`]*)`/g, "$1").replace(/<br\s*\/?>/gi, " ");
|
|
15
|
+
/** Quebra o texto em linhas de no máximo `w` colunas (palavra longa demais é cortada). */
|
|
16
|
+
function wrap(text, w) {
|
|
17
|
+
const out = [];
|
|
18
|
+
let cur = "";
|
|
19
|
+
const push = (word) => {
|
|
20
|
+
while (stringWidth(word) > w) {
|
|
21
|
+
if (cur) {
|
|
22
|
+
out.push(cur);
|
|
23
|
+
cur = "";
|
|
24
|
+
}
|
|
25
|
+
let piece = "";
|
|
26
|
+
for (const ch of word) {
|
|
27
|
+
if (stringWidth(piece + ch) > w)
|
|
28
|
+
break;
|
|
29
|
+
piece += ch;
|
|
30
|
+
}
|
|
31
|
+
out.push(piece);
|
|
32
|
+
word = word.slice(piece.length);
|
|
33
|
+
}
|
|
34
|
+
if (!cur)
|
|
35
|
+
cur = word;
|
|
36
|
+
else if (stringWidth(cur) + 1 + stringWidth(word) <= w)
|
|
37
|
+
cur += " " + word;
|
|
38
|
+
else {
|
|
39
|
+
out.push(cur);
|
|
40
|
+
cur = word;
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
for (const word of text.split(/\s+/).filter(Boolean))
|
|
44
|
+
push(word);
|
|
45
|
+
if (cur || !out.length)
|
|
46
|
+
out.push(cur);
|
|
47
|
+
return out;
|
|
48
|
+
}
|
|
49
|
+
const pad = (s, w, align) => {
|
|
50
|
+
const gap = Math.max(0, w - stringWidth(s));
|
|
51
|
+
if (align === "r")
|
|
52
|
+
return " ".repeat(gap) + s;
|
|
53
|
+
if (align === "c")
|
|
54
|
+
return " ".repeat(Math.floor(gap / 2)) + s + " ".repeat(Math.ceil(gap / 2));
|
|
55
|
+
return s + " ".repeat(gap);
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* Desenha uma tabela markdown como grade com bordas (┌─┬─┐ / │ / ├─┼─┤ / └─┴─┘), como o Claude Code.
|
|
59
|
+
* Cabe em `width` colunas: as colunas mais largas encolhem e o texto quebra dentro delas.
|
|
60
|
+
* Devolve null se as linhas não formam uma tabela (falta a linha `|---|---|`).
|
|
61
|
+
*/
|
|
62
|
+
export function renderTable(lines, width) {
|
|
63
|
+
if (lines.length < 2)
|
|
64
|
+
return null;
|
|
65
|
+
const head = cells(lines[0]);
|
|
66
|
+
const sepRow = cells(lines[1]);
|
|
67
|
+
if (!isSep(sepRow))
|
|
68
|
+
return null;
|
|
69
|
+
const n = head.length;
|
|
70
|
+
const align = Array.from({ length: n }, (_, i) => {
|
|
71
|
+
const s = (sepRow[i] ?? "").replace(/\s/g, "");
|
|
72
|
+
return s.startsWith(":") && s.endsWith(":") ? "c" : s.endsWith(":") ? "r" : "l";
|
|
73
|
+
});
|
|
74
|
+
const norm = (r) => Array.from({ length: n }, (_, i) => plain(r[i] ?? ""));
|
|
75
|
+
const rows = [norm(head), ...lines.slice(2).map(cells).map(norm)];
|
|
76
|
+
const w = Array.from({ length: n }, (_, i) => Math.max(1, ...rows.map((r) => stringWidth(r[i]))));
|
|
77
|
+
const room = Math.max(n, width - (3 * n + 1));
|
|
78
|
+
// encolhe sempre a coluna mais larga até caber (mínimo de 8 colunas, ou menos se não houver espaço)
|
|
79
|
+
const min = Math.max(1, Math.min(8, Math.floor(room / n)));
|
|
80
|
+
while (w.reduce((a, b) => a + b, 0) > room) {
|
|
81
|
+
const i = w.indexOf(Math.max(...w));
|
|
82
|
+
if (w[i] <= min)
|
|
83
|
+
break;
|
|
84
|
+
w[i]--;
|
|
85
|
+
}
|
|
86
|
+
const border = (l, m, r) => c.accent(l + w.map((x) => "─".repeat(x + 2)).join(m) + r);
|
|
87
|
+
const bar = c.accent("│");
|
|
88
|
+
const out = [border("┌", "┬", "┐")];
|
|
89
|
+
rows.forEach((r, ri) => {
|
|
90
|
+
const wrapped = r.map((cell, i) => wrap(cell, w[i]));
|
|
91
|
+
const h = Math.max(...wrapped.map((x) => x.length));
|
|
92
|
+
for (let k = 0; k < h; k++) {
|
|
93
|
+
const body = wrapped.map((x, i) => {
|
|
94
|
+
const cell = pad(x[k] ?? "", w[i], ri === 0 ? "c" : align[i]);
|
|
95
|
+
return " " + (ri === 0 ? c.bold(cell) : cell) + " ";
|
|
96
|
+
});
|
|
97
|
+
out.push(bar + body.join(bar) + bar);
|
|
98
|
+
}
|
|
99
|
+
out.push(ri === rows.length - 1 ? border("└", "┴", "┘") : border("├", "┼", "┤"));
|
|
100
|
+
});
|
|
101
|
+
return out;
|
|
102
|
+
}
|
package/dist/ui/ui.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { BOLD, CODE, ITALIC, OFF, c, cols, palette, useColor } from "./theme.js";
|
|
2
2
|
import { renderBanner } from "./banner.js";
|
|
3
|
+
import { renderTable } from "./table.js";
|
|
3
4
|
import stringWidth from "string-width";
|
|
4
5
|
export const NAME = "Brito";
|
|
5
6
|
const isTTY = !!process.stdout.isTTY;
|
|
@@ -159,6 +160,70 @@ class UI {
|
|
|
159
160
|
skip = false; // descarta o resto da linha do ``` (nome da linguagem)
|
|
160
161
|
bol = true; // só espaços até agora nesta linha
|
|
161
162
|
prev = " "; // último caractere resolvido
|
|
163
|
+
// tabelas markdown: linhas que começam com `|` são guardadas inteiras e desenhadas alinhadas no fim da tabela
|
|
164
|
+
rawBol = true; // início de linha no texto bruto do modelo
|
|
165
|
+
lead = ""; // espaços no início da linha, antes de decidir se é tabela
|
|
166
|
+
tblLine = null; // linha de tabela em andamento
|
|
167
|
+
tbl = []; // linhas completas da tabela atual
|
|
168
|
+
/** Primeira etapa do streaming: separa as linhas de tabela; o resto segue para o markdown. */
|
|
169
|
+
raw(ch) {
|
|
170
|
+
if (this.tblLine !== null) {
|
|
171
|
+
if (ch === "\n") {
|
|
172
|
+
this.tbl.push(this.tblLine);
|
|
173
|
+
this.tblLine = null;
|
|
174
|
+
this.rawBol = true;
|
|
175
|
+
}
|
|
176
|
+
else
|
|
177
|
+
this.tblLine += ch;
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
if (this.rawBol && !this.st.fence) {
|
|
181
|
+
if (ch === " " || ch === "\t") {
|
|
182
|
+
this.lead += ch;
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
if (ch === "|") {
|
|
186
|
+
this.tblLine = "|";
|
|
187
|
+
this.lead = "";
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
this.flushTable();
|
|
192
|
+
if (this.lead) {
|
|
193
|
+
for (const x of this.lead)
|
|
194
|
+
this.feed(x);
|
|
195
|
+
this.lead = "";
|
|
196
|
+
}
|
|
197
|
+
this.rawBol = ch === "\n";
|
|
198
|
+
this.feed(ch);
|
|
199
|
+
}
|
|
200
|
+
/** Fecha a tabela pendente: desenha alinhada ou, se não for tabela de verdade, devolve as linhas como texto. */
|
|
201
|
+
flushTable() {
|
|
202
|
+
if (this.tblLine !== null) {
|
|
203
|
+
this.tbl.push(this.tblLine);
|
|
204
|
+
this.tblLine = null;
|
|
205
|
+
}
|
|
206
|
+
if (!this.tbl.length)
|
|
207
|
+
return;
|
|
208
|
+
const lines = this.tbl;
|
|
209
|
+
this.tbl = [];
|
|
210
|
+
const drawn = renderTable(lines, cols() - 1);
|
|
211
|
+
if (!drawn) {
|
|
212
|
+
for (const l of lines)
|
|
213
|
+
for (const x of l + "\n")
|
|
214
|
+
this.feed(x);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
this.emitWord();
|
|
218
|
+
if (!this.lineStart)
|
|
219
|
+
this.out("\n");
|
|
220
|
+
for (const l of drawn)
|
|
221
|
+
this.out(l + "\n");
|
|
222
|
+
this.lineStart = true;
|
|
223
|
+
this.lineW = 0;
|
|
224
|
+
this.prev = "\n";
|
|
225
|
+
this.bol = true;
|
|
226
|
+
}
|
|
162
227
|
seq() {
|
|
163
228
|
const { b, i, k, fence } = this.st;
|
|
164
229
|
return k || fence ? CODE : (i ? ITALIC : "") + (b ? BOLD : "");
|
|
@@ -178,6 +243,10 @@ class UI {
|
|
|
178
243
|
this.skip = false;
|
|
179
244
|
this.bol = true;
|
|
180
245
|
this.prev = " ";
|
|
246
|
+
this.rawBol = true;
|
|
247
|
+
this.lead = "";
|
|
248
|
+
this.tblLine = null;
|
|
249
|
+
this.tbl = [];
|
|
181
250
|
}
|
|
182
251
|
/** Descarrega a palavra em andamento, quebrando a linha antes dela se não couber. */
|
|
183
252
|
emitWord() {
|
|
@@ -190,11 +259,8 @@ class UI {
|
|
|
190
259
|
this.lineStart = true;
|
|
191
260
|
this.lineW = 0;
|
|
192
261
|
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
this.lineStart = false;
|
|
196
|
-
}
|
|
197
|
-
// o estilo é reaplicado por palavra: a borda e os espaços não herdam a cor
|
|
262
|
+
this.lineStart = false; // sem borda lateral: copiar o texto do terminal sai limpo
|
|
263
|
+
// o estilo é reaplicado por palavra: os espaços não herdam a cor
|
|
198
264
|
this.out(this.wordStart + this.word + (this.active() ? OFF : ""));
|
|
199
265
|
this.lineW += w;
|
|
200
266
|
this.word = "";
|
|
@@ -269,16 +335,13 @@ class UI {
|
|
|
269
335
|
this.st.i = false;
|
|
270
336
|
} // linha em branco encerra ênfase aberta
|
|
271
337
|
this.wordStart = this.seq();
|
|
272
|
-
this.out(
|
|
338
|
+
this.out("\n");
|
|
273
339
|
this.lineStart = true;
|
|
274
340
|
this.lineW = 0;
|
|
275
341
|
}
|
|
276
342
|
else if (ch === " ") {
|
|
277
343
|
this.emitWord();
|
|
278
|
-
|
|
279
|
-
this.out(c.accent("│ "));
|
|
280
|
-
this.lineStart = false;
|
|
281
|
-
}
|
|
344
|
+
this.lineStart = false;
|
|
282
345
|
if (this.lineW + 1 > cols() - 2) {
|
|
283
346
|
this.out("\n");
|
|
284
347
|
this.lineStart = true;
|
|
@@ -299,7 +362,7 @@ class UI {
|
|
|
299
362
|
if (!this.textOpen) {
|
|
300
363
|
this.clear();
|
|
301
364
|
const label = ` ${NAME} `;
|
|
302
|
-
this.out(c.accent("
|
|
365
|
+
this.out(c.accent("──") + c.bold(c.light(label)) + c.accent("─".repeat(Math.max(2, cols() - label.length - 2))) + "\n");
|
|
303
366
|
this.textOpen = true;
|
|
304
367
|
this.lineStart = true;
|
|
305
368
|
this.lineW = 0;
|
|
@@ -307,18 +370,24 @@ class UI {
|
|
|
307
370
|
this.resetStyle();
|
|
308
371
|
}
|
|
309
372
|
for (const ch of chunk)
|
|
310
|
-
this.
|
|
373
|
+
this.raw(ch);
|
|
311
374
|
}
|
|
312
375
|
textEnd() {
|
|
313
376
|
if (!this.textOpen)
|
|
314
377
|
return;
|
|
378
|
+
this.flushTable();
|
|
379
|
+
if (this.lead) {
|
|
380
|
+
for (const x of this.lead)
|
|
381
|
+
this.feed(x);
|
|
382
|
+
this.lead = "";
|
|
383
|
+
}
|
|
315
384
|
if (this.run.n)
|
|
316
385
|
this.resolve("\n"); // marcador no fim da resposta: fecha se havia abertura, senão fica literal
|
|
317
386
|
this.emitWord();
|
|
318
387
|
this.resetStyle();
|
|
319
388
|
if (!this.lineStart)
|
|
320
389
|
this.out("\n");
|
|
321
|
-
this.out(c.accent("
|
|
390
|
+
this.out(c.accent("─".repeat(cols())) + "\n");
|
|
322
391
|
this.textOpen = false;
|
|
323
392
|
this.draw();
|
|
324
393
|
}
|
|
@@ -355,17 +424,17 @@ class UI {
|
|
|
355
424
|
return;
|
|
356
425
|
this.textEnd();
|
|
357
426
|
const w = cols();
|
|
358
|
-
const wrapped = body.split("\n").flatMap((l) => wrapLine(l.trimEnd(), w -
|
|
427
|
+
const wrapped = body.split("\n").flatMap((l) => wrapLine(l.trimEnd(), w - 1));
|
|
359
428
|
const MAX = 10;
|
|
360
429
|
const cut = !this.reasoning.full && wrapped.length > MAX;
|
|
361
430
|
const lines = cut ? wrapped.slice(0, MAX) : wrapped;
|
|
362
431
|
const label = " raciocínio ";
|
|
363
|
-
this.print(c.dim(
|
|
432
|
+
this.print(c.dim(`──${label}${"─".repeat(Math.max(0, w - 2 - label.length))}`));
|
|
364
433
|
for (const l of lines)
|
|
365
|
-
this.print(c.dim(
|
|
434
|
+
this.print(c.dim(l));
|
|
366
435
|
if (cut)
|
|
367
|
-
this.print(c.dim(
|
|
368
|
-
this.print(c.dim(
|
|
436
|
+
this.print(c.dim(`… +${wrapped.length - MAX} linhas (/reasoning full mostra tudo)`));
|
|
437
|
+
this.print(c.dim("─".repeat(w)));
|
|
369
438
|
}
|
|
370
439
|
info(msg) { this.textEnd(); this.print(c.muted(msg)); }
|
|
371
440
|
warn(msg) { this.textEnd(); this.print(c.rgb(palette.warn)(msg)); }
|
|
@@ -389,6 +458,7 @@ class UI {
|
|
|
389
458
|
const hitColour = hit === undefined ? c.muted : hit >= 70 ? c.ok : hit >= 40 ? c.warn : c.err;
|
|
390
459
|
// [prioridade (maior = descarta primeiro), texto]
|
|
391
460
|
const segs = [
|
|
461
|
+
...(s.project ? [[0, c.light("📁 ") + c.bold(c.soft(s.project))]] : []),
|
|
392
462
|
...(s.mode ? [[0, s.mode]] : []),
|
|
393
463
|
[0, c.light("⬢ ") + c.bold(c.soft(s.model))],
|
|
394
464
|
[0, label("contexto") + c.muted(`${fmtTokens(s.ctxTokens)}/${fmtTokens(s.ctxWindow)}`) + ` [${bar}] ${colour(`${pct}%`)}`],
|
|
@@ -417,6 +487,7 @@ class UI {
|
|
|
417
487
|
["tokens", "total da sessão: ↑ enviados ao modelo (inclui cache) · ↓ gerados por ele."],
|
|
418
488
|
["custo", "gasto estimado da sessão em dólares."],
|
|
419
489
|
["tempo", "há quanto tempo a sessão está aberta · (ativo) soma do tempo gasto em todos os turnos."],
|
|
490
|
+
["📁", "projeto (pasta) em que o Brito está trabalhando — também aparece no título da janela do terminal."],
|
|
420
491
|
["skill", "skill carregada (skill_load) no turno em andamento."],
|
|
421
492
|
].map(([k, v]) => ` ${c.soft(k.padEnd(9))} ${c.muted(v)}`);
|
|
422
493
|
}
|
package/dist/util/images.js
CHANGED
|
@@ -2,10 +2,13 @@ import fs from "node:fs";
|
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
-
const TYPES = { png: "image/png", jpg: "image/jpeg", jpeg: "image/jpeg", gif: "image/gif", webp: "image/webp" };
|
|
6
|
-
const IMG_EXT = /\.(png|jpe?g|gif|webp)$/i;
|
|
7
|
-
/** Limite da API por imagem (5 MB) e teto de anexos por mensagem. */
|
|
5
|
+
const TYPES = { png: "image/png", jpg: "image/jpeg", jpeg: "image/jpeg", gif: "image/gif", webp: "image/webp", pdf: "application/pdf" };
|
|
6
|
+
const IMG_EXT = /\.(png|jpe?g|gif|webp|pdf)$/i;
|
|
7
|
+
/** Limite da API por imagem (5 MB), por PDF (32 MB) e teto de anexos por mensagem. */
|
|
8
8
|
export const MAX_IMAGE_BYTES = 5 * 1024 * 1024;
|
|
9
|
+
export const MAX_PDF_BYTES = 32 * 1024 * 1024;
|
|
10
|
+
export const limitOf = (i) => (i.mediaType === "application/pdf" ? MAX_PDF_BYTES : MAX_IMAGE_BYTES);
|
|
11
|
+
export const isPdf = (i) => i.mediaType === "application/pdf";
|
|
9
12
|
export const MAX_IMAGES = 5;
|
|
10
13
|
/** Divide o texto em tokens, respeitando aspas e (fora do Windows) `\ ` como espaço do nome do arquivo. */
|
|
11
14
|
function tokens(text) {
|
|
@@ -67,9 +70,11 @@ export function droppedImages(text, root = process.cwd()) {
|
|
|
67
70
|
export const fmtBytes = (n) => (n >= 1_048_576 ? `${(n / 1_048_576).toFixed(1)} MB` : `${Math.max(1, Math.round(n / 1024))} kB`);
|
|
68
71
|
/** Separa o que dá para enviar do que passa do limite da API. */
|
|
69
72
|
export function splitOversize(imgs) {
|
|
70
|
-
return { ok: imgs.filter((i) => i.bytes <=
|
|
73
|
+
return { ok: imgs.filter((i) => i.bytes <= limitOf(i)), tooBig: imgs.filter((i) => i.bytes > limitOf(i)) };
|
|
71
74
|
}
|
|
72
|
-
/** Bloco `image` da API da Anthropic, com o arquivo em base64. */
|
|
75
|
+
/** Bloco `image` (ou `document`, para PDF) da API da Anthropic, com o arquivo em base64. */
|
|
73
76
|
export function imageBlock(img) {
|
|
77
|
+
if (isPdf(img))
|
|
78
|
+
return { type: "document", source: { type: "base64", media_type: "application/pdf", data: fs.readFileSync(img.path).toString("base64") } };
|
|
74
79
|
return { type: "image", source: { type: "base64", media_type: img.mediaType, data: fs.readFileSync(img.path).toString("base64") } };
|
|
75
80
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "brito-cli",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.4",
|
|
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": {
|