primocode 10.19.0 → 11.0.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.
Files changed (116) hide show
  1. package/LICENSES/opencode-MIT.txt +24 -0
  2. package/LICENSES/pptxgenjs-MIT.txt +31 -0
  3. package/README.md +16 -3
  4. package/agentes/acessibilidade.md +10 -0
  5. package/agentes/ambiente.md +10 -0
  6. package/agentes/api.md +10 -0
  7. package/agentes/backend.md +10 -0
  8. package/agentes/copy.md +10 -0
  9. package/agentes/css-layout.md +10 -0
  10. package/agentes/dados.md +10 -0
  11. package/agentes/depuracao.md +10 -0
  12. package/agentes/design.md +10 -0
  13. package/agentes/docs.md +10 -0
  14. package/agentes/frontend.md +10 -0
  15. package/agentes/geral/capacidades/arquivos.json +132 -0
  16. package/agentes/geral/capacidades/codigo.json +132 -0
  17. package/agentes/geral/capacidades/dados.json +126 -0
  18. package/agentes/geral/capacidades/depuracao.json +126 -0
  19. package/agentes/geral/capacidades/escrita.json +126 -0
  20. package/agentes/geral/capacidades/git.json +126 -0
  21. package/agentes/geral/capacidades/pesquisa.json +126 -0
  22. package/agentes/geral/capacidades/seguranca.json +132 -0
  23. package/agentes/geral/capacidades/shell.json +132 -0
  24. package/agentes/geral/capacidades/web.json +126 -0
  25. package/agentes/git-pr.md +10 -0
  26. package/agentes/indice.md +86 -0
  27. package/agentes/nextjs.md +10 -0
  28. package/agentes/node.md +10 -0
  29. package/agentes/performance.md +10 -0
  30. package/agentes/pesquisa.md +10 -0
  31. package/agentes/python.md +10 -0
  32. package/agentes/react.md +10 -0
  33. package/agentes/refatoracao.md +10 -0
  34. package/agentes/revisor.md +10 -0
  35. package/agentes/seguranca.md +10 -0
  36. package/agentes/sql.md +10 -0
  37. package/agentes/testes.md +10 -0
  38. package/agentes/typescript.md +10 -0
  39. package/agentes/ux.md +10 -0
  40. package/bin/primocode.js +65 -8
  41. package/editor/helpers/camera_viva.py +28 -2
  42. package/lib/act.js +79 -10
  43. package/lib/agentes/index.js +146 -0
  44. package/lib/agentes/nucleo.js +18 -0
  45. package/lib/catalogo.js +12 -2
  46. package/lib/cli/review.js +64 -0
  47. package/lib/cli/subcomandos.js +34 -0
  48. package/lib/cli/tentativas.js +107 -0
  49. package/lib/cli/util.js +51 -0
  50. package/lib/comandos-extra.js +4 -2
  51. package/lib/editor-pro-maquina.js +165 -0
  52. package/lib/editor-pro.js +10 -7
  53. package/lib/entregaveis/cli.js +26 -0
  54. package/lib/entregaveis/especialistas.js +50 -0
  55. package/lib/entregaveis/nucleo.js +111 -0
  56. package/lib/entregaveis/slides.js +221 -0
  57. package/lib/estudio/materiais.js +172 -0
  58. package/lib/fundo.js +34 -1
  59. package/lib/kits.js +41 -2
  60. package/lib/nomes.js +6 -0
  61. package/lib/placar.js +173 -0
  62. package/lib/plugin/autoteste.js +99 -0
  63. package/lib/plugin/edicao-visual.js +133 -0
  64. package/lib/plugin/pesquisar.js +92 -0
  65. package/lib/plugin/preview-cli.js +38 -0
  66. package/lib/plugin/preview.js +89 -0
  67. package/lib/plugin/tunel.js +27 -0
  68. package/lib/review.js +134 -0
  69. package/lib/session/compactacao.js +124 -0
  70. package/lib/statusline.js +52 -0
  71. package/lib/tool/diagnostico.js +100 -0
  72. package/lib/tool/perguntar.js +60 -0
  73. package/lib/tool/reparar-entrada.js +66 -0
  74. package/lib/tool/web.js +102 -0
  75. package/lib/tools.js +20 -1
  76. package/lib/ui.js +1 -1
  77. package/materiais/saas-linear/base.ts +84 -0
  78. package/materiais/saas-linear/camera/Camera.tsx +38 -0
  79. package/materiais/saas-linear/fontes/OFL-Inter.txt +93 -0
  80. package/materiais/saas-linear/fontes/OFL-Newsreader.txt +93 -0
  81. package/materiais/saas-linear/fontes/inter-400.woff2 +0 -0
  82. package/materiais/saas-linear/fontes/inter-600.woff2 +0 -0
  83. package/materiais/saas-linear/fontes/newsreader-400.woff2 +0 -0
  84. package/materiais/saas-linear/formas/Agenda.tsx +46 -0
  85. package/materiais/saas-linear/formas/CaixaPrompt.tsx +41 -0
  86. package/materiais/saas-linear/formas/CartoesIntegracao.tsx +57 -0
  87. package/materiais/saas-linear/formas/Cursor.tsx +40 -0
  88. package/materiais/saas-linear/formas/Menu.tsx +46 -0
  89. package/materiais/saas-linear/formas/Monitor.tsx +28 -0
  90. package/materiais/saas-linear/paleta.json +18 -0
  91. package/materiais/saas-linear/pecas.json +590 -0
  92. package/materiais/saas-linear/regras.md +40 -0
  93. package/materiais/saas-linear/sons/Som.tsx +39 -0
  94. package/materiais/saas-linear/sons/assinatura.mp3 +0 -0
  95. package/materiais/saas-linear/sons/clique.mp3 +0 -0
  96. package/materiais/saas-linear/sons/impacto.mp3 +0 -0
  97. package/materiais/saas-linear/sons/pop-cartao.mp3 +0 -0
  98. package/materiais/saas-linear/sons/pop-palavra.mp3 +0 -0
  99. package/materiais/saas-linear/sons/subida.mp3 +0 -0
  100. package/materiais/saas-linear/sons/swoosh.mp3 +0 -0
  101. package/materiais/saas-linear/sons/trilha.mp3 +0 -0
  102. package/materiais/saas-linear/sons/whoosh-entra.mp3 +0 -0
  103. package/materiais/saas-linear/sons/whoosh-sai.mp3 +0 -0
  104. package/materiais/saas-linear/texturas/Fundo.tsx +18 -0
  105. package/materiais/saas-linear/texturas/Particulas.tsx +22 -0
  106. package/materiais/saas-linear/tipografia/FraseSerifa.tsx +25 -0
  107. package/materiais/saas-linear/tipografia/Marca.tsx +20 -0
  108. package/materiais/saas-linear/tipografia/MarcaFinal.tsx +22 -0
  109. package/materiais/saas-linear/tipografia/Saudacao.tsx +27 -0
  110. package/materiais/saas-linear/transicoes/Cena.tsx +38 -0
  111. package/materiais/saas-linear/transicoes/CorteImpacto.tsx +16 -0
  112. package/package.json +3 -1
  113. package/skills/deck-conferido/SKILL.md +29 -0
  114. package/vendor/pptxgenjs/carregar.js +12 -0
  115. package/vendor/pptxgenjs/jszip.min.js +13 -0
  116. package/vendor/pptxgenjs/pptxgen.cjs.js +7370 -0
@@ -0,0 +1,38 @@
1
+ /**
2
+ * preview-cli.js — `primocode preview [pasta] [--visual] [--tunel]`
3
+ * Sobe o app, imprime a URL local. --visual liga a edição por clique; --tunel pede link público (só com cloudflared).
4
+ */
5
+ 'use strict';
6
+
7
+ const path = require('path');
8
+ const P = require('./preview.js');
9
+
10
+ async function main(args, deps = {}) {
11
+ const dir = path.resolve(args.find((a) => !a.startsWith('-')) || process.cwd());
12
+ const visual = args.includes('--visual'), tunel = args.includes('--tunel');
13
+ const { sh } = require('../cli/util.js');
14
+ const bin = path.join(__dirname, '..', '..', 'bin', 'primocode.js');
15
+ const V = visual ? require('./edicao-visual.js') : null;
16
+ const aoEditar = deps.aoEditar || (async (pedido) => { console.log(' ✎ edição recebida, mandando pro agente…'); sh(process.execPath, [bin, '-p', pedido], { cwd: dir }); console.log(' pronto, recarregue a página.'); });
17
+ const app = await (deps.subir || P.subir)(dir, visual ? { estatico: { transformar: V.injetar, extra: V.tratar(dir, aoEditar) } } : {});
18
+ if (!app.ok) { console.log(' ' + app.erro); return 1; }
19
+ let url = app.url, parar = app.parar;
20
+ if (visual && app.tipo === 'script') {
21
+ const porta = await P.portaLivre();
22
+ const px = await V.proxiar(app.url, porta, { dir, aoEditar });
23
+ url = `http://localhost:${porta}`; const antes = parar; parar = () => { px.close(); antes(); };
24
+ }
25
+ console.log(` Preview: ${url}${visual ? ' (botão "Editar" no canto da página)' : ''}`);
26
+ let t = null;
27
+ if (tunel) {
28
+ t = await require('./tunel.js').abrir(url, deps.tunel || {});
29
+ console.log(t.ok ? ` Link público: ${t.url} (quem tem o link vê o app enquanto isto estiver rodando)` : ` Sem link público: ${t.motivo}`);
30
+ }
31
+ if (deps.sair) { parar(); if (t && t.ok) t.parar(); return 0; }
32
+ console.log(' Ctrl+C encerra.');
33
+ await new Promise((r) => process.on('SIGINT', r));
34
+ parar(); if (t && t.ok) t.parar();
35
+ return 0;
36
+ }
37
+
38
+ module.exports = { main };
@@ -0,0 +1,89 @@
1
+ /**
2
+ * preview.js — sobe o app web da pasta e devolve uma URL local.
3
+ * - package.json com script "dev" (ou "start"): roda o script com PORT livre e espera responder
4
+ * - senão, index.html: um servidor estático pequeno (node, sem dependência)
5
+ * Usado pelo autoteste (lib/plugin/autoteste.js), pelo `primocode preview` e pela edição visual.
6
+ * Não instala nada: sem node_modules num projeto que precisa, avisa em vez de rodar `npm install` sozinho.
7
+ */
8
+ 'use strict';
9
+
10
+ const fs = require('fs');
11
+ const http = require('http');
12
+ const net = require('net');
13
+ const path = require('path');
14
+ const { spawn } = require('child_process');
15
+
16
+ const TIPOS = { '.html': 'text/html; charset=utf-8', '.js': 'text/javascript', '.mjs': 'text/javascript', '.css': 'text/css', '.json': 'application/json', '.svg': 'image/svg+xml', '.png': 'image/png', '.jpg': 'image/jpeg', '.ico': 'image/x-icon', '.txt': 'text/plain' };
17
+
18
+ function portaLivre() {
19
+ return new Promise((res, rej) => { const s = net.createServer(); s.listen(0, '127.0.0.1', () => { const p = s.address().port; s.close(() => res(p)); }); s.on('error', rej); });
20
+ }
21
+
22
+ /** Como subir esta pasta: { tipo: 'script'|'estatico', script? } ou { tipo: null, motivo }. */
23
+ function detectar(dir) {
24
+ try {
25
+ const p = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8'));
26
+ const s = p.scripts || {};
27
+ const nome = s.dev ? 'dev' : s.start ? 'start' : null;
28
+ if (nome) {
29
+ if (!fs.existsSync(path.join(dir, 'node_modules')) && (p.dependencies || p.devDependencies)) return { tipo: null, motivo: 'faltam as dependências: rode `npm install` (não instalo sozinho)' };
30
+ return { tipo: 'script', script: nome };
31
+ }
32
+ } catch { /* sem package.json */ }
33
+ if (fs.existsSync(path.join(dir, 'index.html'))) return { tipo: 'estatico' };
34
+ return { tipo: null, motivo: 'não achei script dev/start nem index.html' };
35
+ }
36
+
37
+ /** Servidor estático. `transformar(html, req)` deixa injetar algo nas páginas; `extra(req,res)` devolve true se tratou. */
38
+ function servirEstatico(dir, porta, { transformar, extra } = {}) {
39
+ const raiz = path.resolve(dir);
40
+ const srv = http.createServer((req, res) => {
41
+ if (extra && extra(req, res)) return;
42
+ let rel = decodeURIComponent((req.url || '/').split('?')[0]);
43
+ if (rel.endsWith('/')) rel += 'index.html';
44
+ const p = path.resolve(raiz, '.' + rel);
45
+ if (p !== raiz && !p.startsWith(raiz + path.sep)) { res.writeHead(403); return res.end('fora da pasta'); }
46
+ fs.readFile(p, (err, buf) => {
47
+ if (err) { res.writeHead(404); return res.end('não achei'); }
48
+ const tipo = TIPOS[path.extname(p).toLowerCase()] || 'application/octet-stream';
49
+ if (tipo.startsWith('text/html') && transformar) buf = Buffer.from(transformar(buf.toString('utf8'), req));
50
+ res.writeHead(200, { 'content-type': tipo }); res.end(buf);
51
+ });
52
+ });
53
+ return new Promise((res) => srv.listen(porta, '127.0.0.1', () => res(srv)));
54
+ }
55
+
56
+ function responde(url, ms = 800) {
57
+ return new Promise((res) => { const r = http.get(url, (x) => { x.resume(); res(true); }); r.on('error', () => res(false)); r.setTimeout(ms, () => { r.destroy(); res(false); }); });
58
+ }
59
+
60
+ /**
61
+ * Sobe e espera responder. Devolve { ok, url, parar(), log() } ou { ok: false, erro }.
62
+ * `opcoes.estatico` = { transformar, extra } passa pro servidor estático.
63
+ */
64
+ async function subir(dir, { espera = 30000, estatico } = {}) {
65
+ const d = detectar(dir);
66
+ if (!d.tipo) return { ok: false, erro: d.motivo };
67
+ if (d.tipo === 'estatico') {
68
+ const porta = await portaLivre();
69
+ const srv = await servirEstatico(dir, porta, estatico);
70
+ return { ok: true, url: `http://localhost:${porta}`, tipo: 'estatico', parar: () => srv.close(), log: () => '' };
71
+ }
72
+ const porta = await portaLivre();
73
+ let saida = '';
74
+ const filho = spawn('npm', ['run', d.script], { cwd: dir, env: { ...process.env, PORT: String(porta), BROWSER: 'none', FORCE_COLOR: '0' }, stdio: ['ignore', 'pipe', 'pipe'], detached: true });
75
+ filho.stdout.on('data', (b) => { saida += b; }); filho.stderr.on('data', (b) => { saida += b; });
76
+ let morreu = false; filho.on('exit', () => { morreu = true; });
77
+ const parar = () => { try { process.kill(-filho.pid); } catch { try { filho.kill(); } catch { /* */ } } };
78
+ const fim = Date.now() + espera;
79
+ while (Date.now() < fim && !morreu) {
80
+ const m = saida.match(/https?:\/\/(?:localhost|127\.0\.0\.1|\[::1\]|0\.0\.0\.0):(\d+)/);
81
+ const url = `http://localhost:${m ? m[1] : porta}`;
82
+ if (await responde(url)) return { ok: true, url, tipo: 'script', parar, log: () => saida };
83
+ await new Promise((r) => setTimeout(r, 300));
84
+ }
85
+ parar();
86
+ return { ok: false, erro: morreu ? 'o servidor do app fechou sozinho' : `o app não respondeu em ${Math.round(espera / 1000)}s`, log: saida.slice(-600) };
87
+ }
88
+
89
+ module.exports = { detectar, subir, servirEstatico, portaLivre, responde };
@@ -0,0 +1,27 @@
1
+ /**
2
+ * tunel.js — link público opcional pro preview. SÓ se o `cloudflared` já estiver instalado e a pessoa pediu (--tunel).
3
+ * Nunca instala nada: sem ele, diz como instalar e segue só com o link local.
4
+ */
5
+ 'use strict';
6
+
7
+ const { spawn } = require('child_process');
8
+ const { temComando } = require('../cli/util.js');
9
+
10
+ const RE_URL = /https:\/\/[a-z0-9-]+\.trycloudflare\.com/;
11
+
12
+ /** Devolve { ok, url, parar() } ou { ok: false, motivo }. `temFerramenta` e `iniciar` são injetáveis. */
13
+ async function abrir(urlLocal, { temFerramenta = () => temComando('cloudflared'), iniciar = (u) => spawn('cloudflared', ['tunnel', '--url', u], { stdio: ['ignore', 'pipe', 'pipe'] }), espera = 20000 } = {}) {
14
+ if (!temFerramenta()) return { ok: false, motivo: 'o cloudflared não está instalado, então fico só com o link local (não instalo sozinho; no Mac: brew install cloudflared)' };
15
+ const filho = iniciar(urlLocal);
16
+ return new Promise((res) => {
17
+ let buf = '';
18
+ const parar = () => { try { filho.kill(); } catch { /* */ } };
19
+ const t = setTimeout(() => { parar(); res({ ok: false, motivo: 'o túnel não devolveu um link em ' + Math.round(espera / 1000) + 's' }); }, espera);
20
+ const le = (b) => { buf += b; const m = buf.match(RE_URL); if (m) { clearTimeout(t); res({ ok: true, url: m[0], parar }); } };
21
+ filho.stdout && filho.stdout.on('data', le); filho.stderr && filho.stderr.on('data', le);
22
+ filho.on('error', (e) => { clearTimeout(t); res({ ok: false, motivo: e.message }); });
23
+ filho.on('exit', () => { clearTimeout(t); res({ ok: false, motivo: 'o cloudflared fechou antes de dar o link' }); });
24
+ });
25
+ }
26
+
27
+ module.exports = { abrir, RE_URL };
package/lib/review.js ADDED
@@ -0,0 +1,134 @@
1
+ /**
2
+ * review.js — /review em duas passadas (ideia: revisão do Claude Code; código nosso).
3
+ *
4
+ * passada 1 lê o diff (com o número de linha de cada linha nova) e lista achados {arquivo, linha, gravidade, problema, porque}
5
+ * passada 2 UM chamado por achado: recebe o achado + o trecho real do arquivo e diz se é verdade. Não confirmou → cai.
6
+ * saída só os confirmados, do mais grave ao menos, cada um com arquivo:linha.
7
+ *
8
+ * Quem fala com o modelo é `chamar(prompt) → Promise<texto>` (injetado: o teste roda sem rede).
9
+ */
10
+ 'use strict';
11
+
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+ const { execFileSync } = require('child_process');
15
+
16
+ const TETO_DIFF = 24000;
17
+ const MAX_ACHADOS = 10;
18
+ const GRAVIDADE = { alta: 0, media: 1, média: 1, baixa: 2 };
19
+
20
+ function git(dir, args) {
21
+ try { return execFileSync('git', args, { cwd: dir, encoding: 'utf-8', maxBuffer: 8 * 1024 * 1024, stdio: ['ignore', 'pipe', 'ignore'] }); } catch { return ''; }
22
+ }
23
+
24
+ /** O que mudou: tudo que ainda não foi commitado (staged + não staged) contra o HEAD. */
25
+ function coletarDiff(dir) {
26
+ let d = git(dir, ['diff', 'HEAD', '--no-color', '-U3']);
27
+ if (!d.trim()) d = git(dir, ['diff', '--no-color', '-U3']);
28
+ return d;
29
+ }
30
+
31
+ /** Põe `L<n>|` na frente de cada linha nova/contexto, pra o modelo citar a linha certa. Linhas removidas ficam com `-`. */
32
+ function anotarDiff(diff) {
33
+ const out = [];
34
+ let n = 0;
35
+ for (const linha of String(diff).split('\n')) {
36
+ const h = /^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@/.exec(linha);
37
+ if (h) { n = Number(h[1]); out.push(linha); continue; }
38
+ if (/^(diff --git|index |--- |\+\+\+ |new file|deleted file)/.test(linha)) { out.push(linha); continue; }
39
+ if (linha.startsWith('-')) { out.push(` |${linha}`); continue; }
40
+ if (linha.startsWith('+') || linha.startsWith(' ')) { out.push(`L${n}|${linha}`); n++; continue; }
41
+ out.push(linha);
42
+ }
43
+ return out.join('\n');
44
+ }
45
+
46
+ function extrairJson(texto) {
47
+ const t = String(texto || '');
48
+ const cerca = /```(?:json)?\s*([\s\S]*?)```/i.exec(t);
49
+ const candidatos = [cerca && cerca[1], t].filter(Boolean);
50
+ for (const c of candidatos) {
51
+ const ini = Math.min(...['[', '{'].map((k) => { const i = c.indexOf(k); return i < 0 ? Infinity : i; }));
52
+ if (!Number.isFinite(ini)) continue;
53
+ const fim = Math.max(c.lastIndexOf(']'), c.lastIndexOf('}'));
54
+ if (fim <= ini) continue;
55
+ try { return JSON.parse(c.slice(ini, fim + 1)); } catch { /* tenta o próximo */ }
56
+ }
57
+ return null;
58
+ }
59
+
60
+ function prompt1(diffAnotado, foco) {
61
+ return 'Você é um revisor de código rigoroso. Abaixo está um diff; cada linha nova começa com L<número>| (a linha no arquivo novo).\n'
62
+ + 'Ache SÓ problemas reais que o diff introduz: bug, erro de lógica, caso de borda que quebra, falha de segurança, perda de dado, '
63
+ + 'chamada com argumento errado. NÃO aponte estilo, nome, falta de comentário nem gosto.\n'
64
+ + (foco ? `Foco pedido: ${foco}\n` : '')
65
+ + `Devolva SÓ um JSON, uma lista (no máximo ${MAX_ACHADOS}, vazia [] se não há nada): `
66
+ + '[{"arquivo":"caminho","linha":12,"gravidade":"alta|media|baixa","problema":"uma frase","porque":"o que acontece com quem usa"}]\n\n'
67
+ + `DIFF:\n${diffAnotado}`;
68
+ }
69
+
70
+ function prompt2(a, trecho) {
71
+ return 'Você é o verificador. Outro revisor apontou o problema abaixo. Confira contra o código REAL e seja cético: '
72
+ + 'confirme só se der pra explicar o caminho exato em que o erro acontece. Se for suposição, se o código já trata o caso, ou se a linha não é isso, rejeite.\n'
73
+ + `ACHADO: ${a.arquivo}:${a.linha} [${a.gravidade}] ${a.problema} — ${a.porque}\n\n`
74
+ + `CÓDIGO (${a.arquivo}, linhas em volta de ${a.linha}):\n${trecho || '(arquivo não encontrado)'}\n\n`
75
+ + 'Devolva SÓ um JSON: {"confirmado":true|false,"motivo":"uma frase com a prova ou a razão de rejeitar"}';
76
+ }
77
+
78
+ function trechoDoArquivo(dir, arquivo, linha, raio = 12) {
79
+ try {
80
+ const p = path.resolve(dir, arquivo);
81
+ if (!p.startsWith(path.resolve(dir) + path.sep)) return '';
82
+ const ls = fs.readFileSync(p, 'utf-8').split('\n');
83
+ const de = Math.max(0, (Number(linha) || 1) - 1 - raio);
84
+ return ls.slice(de, de + 2 * raio + 1).map((l, i) => `${de + i + 1}| ${l}`).join('\n');
85
+ } catch { return ''; }
86
+ }
87
+
88
+ function normalizar(lista) {
89
+ if (!Array.isArray(lista)) return [];
90
+ return lista.filter((a) => a && a.arquivo && a.problema).slice(0, MAX_ACHADOS).map((a) => ({
91
+ arquivo: String(a.arquivo).replace(/^[ab]\//, ''), linha: Number(a.linha) || 1,
92
+ gravidade: GRAVIDADE[String(a.gravidade || '').toLowerCase()] !== undefined ? String(a.gravidade).toLowerCase().replace('é', 'e') : 'media',
93
+ problema: String(a.problema).slice(0, 300), porque: String(a.porque || '').slice(0, 300),
94
+ }));
95
+ }
96
+
97
+ async function emLotes(itens, n, fn) {
98
+ const out = new Array(itens.length);
99
+ for (let i = 0; i < itens.length; i += n) {
100
+ await Promise.all(itens.slice(i, i + n).map(async (it, j) => { out[i + j] = await fn(it); }));
101
+ }
102
+ return out;
103
+ }
104
+
105
+ /** @returns {Promise<{ok:boolean, vazio?:boolean, achados:Array, descartados:number, total:number, erro?:string}>} */
106
+ async function revisar({ dir, foco = '', chamar, diff = null, concorrencia = 3 }) {
107
+ const bruto = diff != null ? diff : coletarDiff(dir);
108
+ if (!bruto.trim()) return { ok: true, vazio: true, achados: [], descartados: 0, total: 0 };
109
+ const recorte = bruto.length > TETO_DIFF ? bruto.slice(0, TETO_DIFF) : bruto;
110
+ const resp1 = await chamar(prompt1(anotarDiff(recorte), foco));
111
+ const lista = extrairJson(resp1);
112
+ if (!Array.isArray(lista)) return { ok: false, erro: 'a primeira passada não devolveu uma lista legível', achados: [], descartados: 0, total: 0 };
113
+ const candidatos = normalizar(lista);
114
+ const veredictos = await emLotes(candidatos, concorrencia, async (a) => {
115
+ try {
116
+ const v = extrairJson(await chamar(prompt2(a, trechoDoArquivo(dir, a.arquivo, a.linha))));
117
+ return v && v.confirmado === true ? { ...a, verificacao: String(v.motivo || '').slice(0, 300) } : null;
118
+ } catch { return null; } // verificador que falha não confirma: na dúvida, cai
119
+ });
120
+ const achados = veredictos.filter(Boolean)
121
+ .sort((x, y) => GRAVIDADE[x.gravidade] - GRAVIDADE[y.gravidade] || x.arquivo.localeCompare(y.arquivo) || x.linha - y.linha);
122
+ return { ok: true, achados, descartados: candidatos.length - achados.length, total: candidatos.length, cortado: bruto.length > TETO_DIFF };
123
+ }
124
+
125
+ function formatar(r) {
126
+ if (!r.ok) return `Não consegui revisar: ${r.erro}.`;
127
+ if (r.vazio) return 'Nada mudou desde o último commit: não há o que revisar.';
128
+ if (!r.achados.length) return `Nenhum problema confirmado (${r.total} suspeita${r.total === 1 ? '' : 's'} da 1ª passada, todas derrubadas na verificação).`;
129
+ const l = r.achados.map((a, i) => `${i + 1}. [${a.gravidade}] ${a.arquivo}:${a.linha} ${a.problema}\n ${a.porque}${a.verificacao ? `\n conferido: ${a.verificacao}` : ''}`);
130
+ l.push(`\n${r.achados.length} confirmado${r.achados.length === 1 ? '' : 's'} de ${r.total}; ${r.descartados} derrubado${r.descartados === 1 ? '' : 's'} na verificação.${r.cortado ? ' (diff grande: só o começo foi lido)' : ''}`);
131
+ return l.join('\n');
132
+ }
133
+
134
+ module.exports = { revisar, formatar, coletarDiff, anotarDiff, extrairJson, prompt1, prompt2, trechoDoArquivo };
@@ -0,0 +1,124 @@
1
+ /**
2
+ * compactacao.js — resumo automático no MEIO da tarefa (ideia: session/compaction.ts do OpenCode, MIT).
3
+ *
4
+ * Quando a conversa do agente passa de ~80% da janela do modelo, as mensagens antigas viram UM resumo
5
+ * feito pelo próprio modelo (objetivo, decisões, arquivos mexidos, to-dos em aberto) e saem da conversa.
6
+ * As últimas mensagens ficam cruas, e as de sistema não se perdem.
7
+ *
8
+ * Mesmo caminho do /compact: `resumirComFatos` é o `resumir` que os dois usam (resumo do modelo +
9
+ * bloco de fatos tirado das próprias mensagens: arquivos escritos e to-dos abertos — isso não se confia ao modelo).
10
+ *
11
+ * O que já existia e continua: lib/memoria-conversa.js (resumo entre pedidos) e compactar() do act.js
12
+ * (encolhe resultado de ferramenta antigo, sem modelo). Isto é a camada de cima: quando só encolher não basta.
13
+ */
14
+ 'use strict';
15
+
16
+ const LIMIAR = 0.8;
17
+ const MANTER = 6; // mensagens finais que ficam cruas
18
+ const JANELA_PADRAO = 32000; // não sabemos o modelo: ser conservador é o lado seguro
19
+
20
+ // [padrão no id do modelo, janela em tokens]. A primeira que casar vale.
21
+ const JANELAS = [
22
+ [/gemini/i, 1000000], [/claude|anthropic/i, 200000], [/gpt-4\.1|gpt-5|o[134]-/i, 200000], [/gpt-4o|gpt-4/i, 128000],
23
+ [/llama-?3\.[1-3]|llama-?4/i, 128000], [/qwen|deepseek|kimi|mistral|gpt-oss/i, 128000],
24
+ ];
25
+
26
+ function janelaDe(modelo) {
27
+ const env = Number(process.env.PRIMOCODE_JANELA);
28
+ if (env > 0) return env;
29
+ const id = String(modelo || '');
30
+ for (const [re, n] of JANELAS) if (re.test(id)) return n;
31
+ return JANELA_PADRAO;
32
+ }
33
+
34
+ function tokensDe(messages) {
35
+ let chars = 0;
36
+ for (const m of messages || []) { try { chars += (typeof m.content === 'string' ? m.content : JSON.stringify(m.content || '')).length + (m.tool_calls ? JSON.stringify(m.tool_calls).length : 0); } catch { /* */ } }
37
+ return Math.ceil(chars / 4);
38
+ }
39
+
40
+ function precisaCompactar(messages, { janela = JANELA_PADRAO, limiar = LIMIAR } = {}) {
41
+ return tokensDe(messages) >= janela * limiar;
42
+ }
43
+
44
+ /** Onde cortar: tudo antes de `corte` vira resumo. Nunca no meio de um assistant+tool (corta antes do assistant). */
45
+ function acharCorte(messages, manter = MANTER) {
46
+ let corte = messages.length - manter;
47
+ while (corte > 0 && messages[corte] && messages[corte].role === 'tool') corte--;
48
+ return corte;
49
+ }
50
+
51
+ const ESCRITA = new Set(['write_file', 'edit_file', 'multi_edit']);
52
+
53
+ function arquivosMexidos(mensagens) {
54
+ const set = new Set();
55
+ for (const m of mensagens || []) {
56
+ for (const tc of (m && m.tool_calls) || []) {
57
+ const nome = (tc.function && tc.function.name) || tc.name;
58
+ if (!ESCRITA.has(nome)) continue;
59
+ let a = (tc.function && tc.function.arguments) || tc.arguments;
60
+ if (typeof a === 'string') { try { a = JSON.parse(a); } catch { a = null; } }
61
+ if (a && a.path) set.add(String(a.path));
62
+ }
63
+ }
64
+ return [...set];
65
+ }
66
+
67
+ function todosAbertos(mensagens, todosAtuais) {
68
+ let lista = Array.isArray(todosAtuais) && todosAtuais.length ? todosAtuais : null;
69
+ if (!lista) {
70
+ for (const m of mensagens || []) for (const tc of (m && m.tool_calls) || []) {
71
+ const nome = (tc.function && tc.function.name) || tc.name;
72
+ if (nome !== 'write_todos') continue;
73
+ let a = (tc.function && tc.function.arguments) || tc.arguments;
74
+ if (typeof a === 'string') { try { a = JSON.parse(a); } catch { a = null; } }
75
+ if (a && Array.isArray(a.todos)) lista = a.todos;
76
+ }
77
+ }
78
+ return (lista || []).filter((t) => t && t.status !== 'completed' && t.content).map((t) => String(t.content).slice(0, 120));
79
+ }
80
+
81
+ /** O bloco que não depende do modelo lembrar. */
82
+ function blocoDeFatos(mensagens, todosAtuais) {
83
+ const arq = arquivosMexidos(mensagens);
84
+ const abertos = todosAbertos(mensagens, todosAtuais);
85
+ const linhas = [];
86
+ if (arq.length) linhas.push(`Arquivos que você já mexeu (estão no disco; releia antes de editar): ${arq.slice(0, 30).join(', ')}`);
87
+ if (abertos.length) linhas.push(`To-dos em aberto: ${abertos.slice(0, 12).join(' | ')}`);
88
+ return linhas.join('\n');
89
+ }
90
+
91
+ /**
92
+ * O `resumir` que /compact e a compactação automática compartilham.
93
+ * `chamar({resumo, mensagens}) → Promise<string>` é quem fala com o servidor (api.requestResumo).
94
+ */
95
+ async function resumirComFatos({ resumo, mensagens }, chamar, todosAtuais) {
96
+ const texto = String(await chamar({ resumo, mensagens }) || '').trim();
97
+ if (!texto) return '';
98
+ const fatos = blocoDeFatos(mensagens, todosAtuais);
99
+ return fatos && !texto.includes(fatos) ? `${texto}\n\n${fatos}` : texto;
100
+ }
101
+
102
+ /**
103
+ * Substitui, EM LUGAR, as mensagens antigas por um resumo. Devolve { ok, antes, depois, resumidas } (tokens).
104
+ * Se o resumo falhar ou não houver o que cortar, não mexe em nada (ok:false).
105
+ */
106
+ async function compactarMensagens(messages, { resumir, todos, manter = MANTER } = {}) {
107
+ const antes = tokensDe(messages);
108
+ let inicio = 0;
109
+ while (inicio < messages.length && messages[inicio].role === 'system') inicio++;
110
+ const corte = acharCorte(messages, manter);
111
+ if (corte - inicio < 2) return { ok: false, antes, depois: antes, resumidas: 0, motivo: 'pouca coisa pra resumir' };
112
+ const velhas = messages.slice(inicio, corte);
113
+ const sistema = velhas.filter((m) => m.role === 'system');
114
+ const conversa = velhas.filter((m) => m.role !== 'system');
115
+ let texto = '';
116
+ try { texto = await resumir({ resumo: '', mensagens: conversa }, todos); } catch { texto = ''; }
117
+ if (!String(texto || '').trim()) return { ok: false, antes, depois: antes, resumidas: 0, motivo: 'o resumo falhou' };
118
+ const nota = { role: 'user', content: `[Resumo automático: a conversa antiga foi trocada por este resumo para caber na janela. Siga a tarefa de onde parou.]\n${texto}` };
119
+ messages.splice(inicio, corte - inicio, ...sistema, nota);
120
+ const depois = tokensDe(messages);
121
+ return { ok: depois < antes, antes, depois, resumidas: conversa.length };
122
+ }
123
+
124
+ module.exports = { janelaDe, tokensDe, precisaCompactar, acharCorte, arquivosMexidos, todosAbertos, blocoDeFatos, resumirComFatos, compactarMensagens, LIMIAR, MANTER };
@@ -0,0 +1,52 @@
1
+ /**
2
+ * statusline.js — as peças da linha de status do terminal: modelo, contexto usado, tokens de hoje, ramo.
3
+ * Puro (as cores entram por parâmetro) e barato: o que vem do disco (uso de hoje) fica em cache por 3 s,
4
+ * porque a barra se redesenha a cada tecla.
5
+ *
6
+ * registrarModelo(id) o act.js chama quando o servidor diz quem respondeu
7
+ * modelo() → nome curto do último modelo que respondeu, ou ''
8
+ * tokensDeHoje() → número (do arquivo do /usage: lib/limite.js)
9
+ * partes({...}) → lista de textos já coloridos, na ordem da linha
10
+ */
11
+ 'use strict';
12
+
13
+ let ultimo = '';
14
+ let cache = { quando: 0, tokens: 0 };
15
+
16
+ function registrarModelo(id) { ultimo = String(id || ''); }
17
+
18
+ /** "anthropic/claude-sonnet-4:free" → "claude-sonnet-4"; "gemini:gemini-2.5-flash" → "gemini-2.5-flash". */
19
+ function nomeCurto(id) {
20
+ return String(id || '').replace(/^[a-z0-9-]+:/i, '').replace(/^.*\//, '').replace(/:free$/i, '').slice(0, 28);
21
+ }
22
+ function modelo() { return nomeCurto(ultimo); }
23
+
24
+ function tokensDeHoje(agora = Date.now(), ler = () => require('./limite.js').usoDeHoje().tokens) {
25
+ if (agora - cache.quando > 3000) {
26
+ try { cache = { quando: agora, tokens: Number(ler()) || 0 }; } catch { cache = { quando: agora, tokens: cache.tokens }; }
27
+ }
28
+ return cache.tokens;
29
+ }
30
+
31
+ function compacto(n) {
32
+ n = Number(n) || 0;
33
+ if (n >= 1e6) return `${(n / 1e6).toFixed(1).replace(/\.0$/, '')}M`;
34
+ if (n >= 1e3) return `${(n / 1e3).toFixed(n >= 1e4 ? 0 : 1).replace(/\.0$/, '')}k`;
35
+ return String(n);
36
+ }
37
+
38
+ /**
39
+ * @param {{modelo?:string, ctxPct?:number, tokensHoje?:number, c:object}} o `c` = as cores do ui
40
+ * Sem dado, a peça some: linha de status cheia de "0" vira ruído.
41
+ */
42
+ function partes({ modelo: m, ctxPct = 0, tokensHoje = 0, c }) {
43
+ const out = [];
44
+ if (m) out.push(c.muted(m));
45
+ if (ctxPct >= 5) out.push((ctxPct >= 100 ? c.err : ctxPct >= 70 ? c.warn : c.dim)(`${ctxPct}% ctx`));
46
+ if (tokensHoje > 0) out.push(c.dim(`${compacto(tokensHoje)} tok hoje`));
47
+ return out;
48
+ }
49
+
50
+ function _reset() { ultimo = ''; cache = { quando: 0, tokens: 0 }; }
51
+
52
+ module.exports = { registrarModelo, modelo, nomeCurto, tokensDeHoje, partes, compacto, _reset };
@@ -0,0 +1,100 @@
1
+ /**
2
+ * diagnostico.js — depois que edit_file/write_file/multi_edit mexe num arquivo, roda o verificador
3
+ * mais barato da linguagem e devolve os erros (com teto) pra irem no resultado da ferramenta.
4
+ * O modelo vê o erro na MESMA volta, em vez de descobrir só no fim. (Ideia: diagnósticos do OpenCode.)
5
+ *
6
+ * .js .mjs .cjs → node --check (+ eslint se o projeto configurou e instalou)
7
+ * .py → compile() do python3 .ts .tsx → tsc local, se há tsconfig.json
8
+ * .json → JSON.parse .sh → bash -n
9
+ * Sem verificador, ferramenta ausente ou timeout → null (nunca atrapalha a edição).
10
+ * PRIMOCODE_SEM_DIAGNOSTICO=1 desliga.
11
+ */
12
+ 'use strict';
13
+
14
+ const fs = require('fs');
15
+ const path = require('path');
16
+ const { spawnSync } = require('child_process');
17
+
18
+ const TETO_CHARS = 1500;
19
+ const TETO_LINHAS = 12;
20
+ const PRAZO_MS = 8000;
21
+
22
+ function rodar(cmd, args, cwd, prazo = PRAZO_MS) {
23
+ const r = spawnSync(cmd, args, { cwd, encoding: 'utf-8', timeout: prazo, maxBuffer: 2 * 1024 * 1024 });
24
+ if (r.error) return null; // ferramenta ausente ou estourou o prazo: sem diagnóstico
25
+ return { code: r.status, out: `${r.stdout || ''}\n${r.stderr || ''}`.trim() };
26
+ }
27
+
28
+ function achar(dir, nome, ate) {
29
+ for (let d = dir; ; d = path.dirname(d)) {
30
+ if (fs.existsSync(path.join(d, nome))) return d;
31
+ if (d === ate || d === path.dirname(d)) return null;
32
+ }
33
+ }
34
+
35
+ function cortar(texto, arquivo) {
36
+ const linhas = String(texto).split('\n').map((l) => l.trimEnd()).filter(Boolean);
37
+ let s = linhas.slice(0, TETO_LINHAS).join('\n');
38
+ if (s.length > TETO_CHARS) s = s.slice(0, TETO_CHARS) + '…';
39
+ if (linhas.length > TETO_LINHAS) s += `\n… e mais ${linhas.length - TETO_LINHAS} linhas`;
40
+ return s.split(arquivo).join(path.basename(arquivo));
41
+ }
42
+
43
+ function eslintConfigurado(raiz) {
44
+ if (!raiz) return false;
45
+ const tem = fs.readdirSync(raiz).some((f) => /^(\.eslintrc(\..+)?|eslint\.config\.(js|mjs|cjs|ts))$/.test(f));
46
+ return tem && fs.existsSync(path.join(raiz, 'node_modules', '.bin', 'eslint'));
47
+ }
48
+
49
+ /** @returns {{verificador:string, erros:string}|null} null = sem verificador ou sem erro */
50
+ function diagnosticar(arquivo, projectDir) {
51
+ try {
52
+ if (process.env.PRIMOCODE_SEM_DIAGNOSTICO === '1') return null;
53
+ if (!fs.existsSync(arquivo) || fs.statSync(arquivo).size > 2 * 1024 * 1024) return null;
54
+ const ext = path.extname(arquivo).toLowerCase();
55
+ const dir = path.dirname(arquivo);
56
+ let r = null; let nome = '';
57
+ if (['.js', '.mjs', '.cjs'].includes(ext)) {
58
+ nome = 'node --check'; r = rodar(process.execPath, ['--check', arquivo], dir);
59
+ if (r && r.code === 0) {
60
+ const raiz = achar(dir, 'package.json', projectDir);
61
+ if (eslintConfigurado(raiz)) {
62
+ nome = 'eslint';
63
+ r = rodar(path.join(raiz, 'node_modules', '.bin', 'eslint'), ['--no-warn-ignored', '-f', 'unix', arquivo], raiz, 15000);
64
+ }
65
+ }
66
+ } else if (ext === '.py') {
67
+ nome = 'python compile';
68
+ r = rodar('python3', ['-c', 'import sys;compile(open(sys.argv[1],encoding="utf-8").read(),sys.argv[1],"exec")', arquivo], dir);
69
+ } else if (ext === '.ts' || ext === '.tsx') {
70
+ const raiz = achar(dir, 'tsconfig.json', projectDir);
71
+ const tsc = raiz && path.join(raiz, 'node_modules', '.bin', 'tsc');
72
+ if (!tsc || !fs.existsSync(tsc)) return null;
73
+ nome = 'tsc';
74
+ r = rodar(tsc, ['--noEmit', '-p', raiz, '--pretty', 'false'], raiz, 30000);
75
+ if (r && r.code !== 0) { // só os erros deste arquivo
76
+ const rel = path.relative(raiz, arquivo).replace(/\\/g, '/');
77
+ r.out = r.out.split('\n').filter((l) => l.startsWith(rel)).join('\n');
78
+ if (!r.out) return null;
79
+ }
80
+ } else if (ext === '.json') {
81
+ nome = 'JSON.parse';
82
+ try { JSON.parse(fs.readFileSync(arquivo, 'utf-8')); return null; } catch (e) { return { verificador: nome, erros: String(e.message).slice(0, 300) }; }
83
+ } else if (ext === '.sh') {
84
+ nome = 'bash -n'; r = rodar('bash', ['-n', arquivo], dir);
85
+ }
86
+ if (!r || r.code === 0 || !r.out) return null;
87
+ return { verificador: nome, erros: cortar(r.out, arquivo) };
88
+ } catch { return null; }
89
+ }
90
+
91
+ /** Anexa `diagnosticos` ao resultado de uma ferramenta de escrita. Não mexe no que não for ok. */
92
+ function anexar(resultado, projectDir) {
93
+ if (!resultado || resultado.ok !== true || !resultado.path) return resultado;
94
+ const d = diagnosticar(path.resolve(projectDir, resultado.path), projectDir);
95
+ if (!d) return resultado;
96
+ return { ...resultado, diagnosticos: `${d.verificador} achou erro neste arquivo depois da edição:\n${d.erros}`,
97
+ comoCorrigir: 'Corrija o erro acima com edit_file antes de seguir. Não diga que terminou com o arquivo quebrado.' };
98
+ }
99
+
100
+ module.exports = { diagnosticar, anexar };
@@ -0,0 +1,60 @@
1
+ /**
2
+ * perguntar.js — a ferramenta `perguntar`: pergunta + 2 a 4 opções (+ texto livre).
3
+ * Só pra quando o agente está BLOQUEADO (duas saídas razoáveis, não dá pra escolher sozinho).
4
+ *
5
+ * Quem desenha na tela é o terminal (ctx.escolher, vem do bin/primocode.js). Sem terminal
6
+ * (primocode -p, pipe, subagente) devolve a PRIMEIRA opção e registra no log — nunca trava esperando.
7
+ */
8
+ 'use strict';
9
+
10
+ const fs = require('fs');
11
+ const os = require('os');
12
+ const path = require('path');
13
+
14
+ const LOG = () => path.join(process.env.PRIMOCODE_HOME || path.join(os.homedir(), '.primocode'), 'perguntas-automaticas.log');
15
+
16
+ function limparOpcoes(opcoes) {
17
+ let o = opcoes;
18
+ if (typeof o === 'string') { try { o = JSON.parse(o); } catch { o = o.split(/\n|;|\|/); } }
19
+ if (!Array.isArray(o)) return [];
20
+ return o.map((x) => String(x && (x.label || x.texto || x.opcao) || x || '').trim()).filter(Boolean).slice(0, 4);
21
+ }
22
+
23
+ async function perguntar(args, ctx = {}) {
24
+ const pergunta = String((args && (args.pergunta || args.question)) || '').trim();
25
+ const opcoes = limparOpcoes(args && (args.opcoes || args.options));
26
+ if (!pergunta) return { ok: false, error: 'faltou "pergunta".' };
27
+ if (opcoes.length < 2) return { ok: false, error: 'passe de 2 a 4 "opcoes" (lista de textos curtos).', comoResolver: 'perguntar pergunta:"...", opcoes:["A","B"]. Se não está bloqueado, escolha você e siga.' };
28
+ const livre = args.texto_livre !== false && args.texto_livre !== 'false';
29
+
30
+ if (typeof ctx.escolher !== 'function') {
31
+ const escolha = opcoes[0];
32
+ try {
33
+ fs.mkdirSync(path.dirname(LOG()), { recursive: true });
34
+ fs.appendFileSync(LOG(), `${new Date().toISOString()}\t${pergunta}\t[${opcoes.join(' | ')}]\t=> ${escolha}\n`);
35
+ } catch { /* log é só rastro */ }
36
+ return { ok: true, escolha, indice: 0, automatico: true,
37
+ aviso: 'Sem terminal: ninguém pode responder, então valeu a primeira opção. Siga com ela e diga isso no fim.' };
38
+ }
39
+ const r = await ctx.escolher(pergunta, opcoes, { livre });
40
+ if (r == null) return { ok: true, escolha: opcoes[0], indice: 0, automatico: true, aviso: 'A pessoa não respondeu; valeu a primeira opção.' };
41
+ if (typeof r === 'number') return { ok: true, escolha: opcoes[r] || opcoes[0], indice: opcoes[r] ? r : 0 };
42
+ return { ok: true, escolha: String(r), indice: -1, texto_livre: true };
43
+ }
44
+
45
+ /** O desenho no terminal: lista numerada + "ou escreva". `ask(prompt) → Promise<string>`. Enter = opção 1. */
46
+ async function escolherNoTerminal(ask, c, pergunta, opcoes, { livre = true } = {}) {
47
+ console.log('');
48
+ console.log(' ' + c.brand('?') + ' ' + c.bold(c.white(pergunta)));
49
+ opcoes.forEach((o, i) => console.log(' ' + c.brand(String(i + 1)) + c.muted(' › ') + c.white(o)));
50
+ console.log(' ' + c.dim(livre ? `Enter = 1 · ou escreva a sua resposta` : 'Enter = 1'));
51
+ const dito = String(await ask(' ' + c.brand('❯ ') + c.muted(`1-${opcoes.length} `))).trim();
52
+ if (!dito) return 0;
53
+ const n = Number(dito);
54
+ if (Number.isInteger(n) && n >= 1 && n <= opcoes.length) return n - 1;
55
+ const igual = opcoes.findIndex((o) => o.toLowerCase() === dito.toLowerCase());
56
+ if (igual >= 0) return igual;
57
+ return livre ? dito : 0;
58
+ }
59
+
60
+ module.exports = { perguntar, escolherNoTerminal, limparOpcoes };