primocode 9.8.0-beta.2 → 9.8.0-beta.21

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 (74) hide show
  1. package/README.md +33 -6
  2. package/app/ansi.js +55 -0
  3. package/app/app.css +482 -281
  4. package/app/app.js +888 -315
  5. package/app/index.html +176 -101
  6. package/app/primo-marca.js +58 -1
  7. package/bin/primocode.js +697 -118
  8. package/comandos/explicar.md +14 -0
  9. package/comandos/publicar.md +10 -0
  10. package/comandos/revisar.md +13 -0
  11. package/lib/act.js +130 -26
  12. package/lib/api.js +8 -1
  13. package/lib/app.js +317 -74
  14. package/lib/browser.js +79 -3
  15. package/lib/catalogo.js +11 -3
  16. package/lib/checkpoint-git.js +218 -0
  17. package/lib/checkpoint.js +249 -0
  18. package/lib/claude-engine.js +25 -3
  19. package/lib/comandos-extra.js +481 -0
  20. package/lib/comandos.js +104 -0
  21. package/lib/config.js +21 -4
  22. package/lib/estudio/acervo-site.js +157 -0
  23. package/lib/estudio/assets.js +77 -3
  24. package/lib/estudio/cinema.js +354 -0
  25. package/lib/estudio/composicao.js +470 -0
  26. package/lib/estudio/edicao.js +450 -0
  27. package/lib/estudio/impacto.js +263 -0
  28. package/lib/estudio/index.js +769 -5
  29. package/lib/estudio/plano.js +5 -2
  30. package/lib/estudio/remotion.js +228 -23
  31. package/lib/estudio/slides.js +34 -30
  32. package/lib/fala.js +95 -1
  33. package/lib/local/cerebro.json +361 -180
  34. package/lib/local/entrada.js +18 -8
  35. package/lib/local/motor-cli.js +1 -1
  36. package/lib/mencoes.js +98 -0
  37. package/lib/offline.js +168 -0
  38. package/lib/painel.js +291 -0
  39. package/lib/perigo.js +256 -0
  40. package/lib/pre-leitura.js +118 -0
  41. package/lib/repeticao.js +4 -2
  42. package/lib/skills.js +6 -1
  43. package/lib/tempos.js +62 -0
  44. package/lib/ui.js +171 -17
  45. package/package.json +3 -2
  46. package/skills/apresentacao-pptx/SKILL.md +52 -0
  47. package/skills/cartaz-de-arte/LICENSE.txt +202 -0
  48. package/skills/cartaz-de-arte/SKILL.md +47 -0
  49. package/skills/criar-skill/LICENSE.txt +202 -0
  50. package/skills/criar-skill/SKILL.md +49 -0
  51. package/skills/depurar-de-verdade/LICENSE.txt +21 -0
  52. package/skills/depurar-de-verdade/SKILL.md +14 -5
  53. package/skills/design-de-verdade/LICENSE.txt +177 -0
  54. package/skills/design-de-verdade/SKILL.md +42 -0
  55. package/skills/documento-word/SKILL.md +57 -0
  56. package/skills/interface-revisada/SKILL.md +47 -0
  57. package/skills/me-ensina/LICENSE.txt +21 -0
  58. package/skills/me-ensina/SKILL.md +27 -0
  59. package/skills/me-entrevista/LICENSE.txt +21 -0
  60. package/skills/me-entrevista/SKILL.md +27 -0
  61. package/skills/motion-de-primeira/SKILL.md +1 -1
  62. package/skills/pdf-bonito/SKILL.md +48 -0
  63. package/skills/planilha-excel/SKILL.md +47 -0
  64. package/skills/react-sem-dor/SKILL.md +48 -0
  65. package/skills/revisao-antes-de-entregar/LICENSE.txt +21 -0
  66. package/skills/revisao-antes-de-entregar/SKILL.md +6 -1
  67. package/skills/site-com-motion/SKILL.md +39 -0
  68. package/skills/temas-prontos/LICENSE.txt +202 -0
  69. package/skills/temas-prontos/SKILL.md +50 -0
  70. package/skills/testar-o-site/LICENSE.txt +202 -0
  71. package/skills/testar-o-site/SKILL.md +46 -0
  72. package/skills/video-no-remotion/SKILL.md +48 -0
  73. package/studio/narrar.py +111 -10
  74. package/studio/rostos.py +52 -0
package/lib/perigo.js ADDED
@@ -0,0 +1,256 @@
1
+ /**
2
+ * perigo.js — o que não tem volta pede licença antes.
3
+ *
4
+ * Até aqui o PrimoCode rodava qualquer `run_shell` e escrevia qualquer arquivo
5
+ * sem perguntar. Para quase tudo isso é certo: quem não programa não sabe
6
+ * responder "autoriza `npm install`?", e uma pergunta que ninguém sabe
7
+ * responder vira um "sim" automático. O problema é o resto — o `rm -rf ~`, o
8
+ * `git push --force`, o `curl … | sh`. Esses não se desfazem.
9
+ *
10
+ * Por isso a régua é estreita DE PROPÓSITO: perigoso é o que apaga, reescreve
11
+ * história ou sai da pasta do projeto. `rm -rf node_modules` dentro do projeto
12
+ * NÃO é perigoso — se fosse, o Primo perguntaria dez vezes por dia, e na
13
+ * décima primeira a pessoa diria sim sem ler. Falso positivo aqui é tão ruim
14
+ * quanto falso negativo: ele treina a pessoa a não ler a pergunta.
15
+ *
16
+ * Os três modos (/permissoes):
17
+ * auto — o padrão. Roda tudo, mas SEMPRE pergunta antes do perigoso.
18
+ * perguntar — pergunta antes de todo comando e de toda escrita de arquivo.
19
+ * livre — nunca pergunta. Nem o perigoso.
20
+ */
21
+
22
+ 'use strict';
23
+
24
+ const os = require('os');
25
+ const path = require('path');
26
+
27
+ const MODOS = ['auto', 'perguntar', 'livre'];
28
+ const COMANDOS = new Set(['run_shell', 'run_in_new_terminal']);
29
+ const ESCRITAS = new Set(['write_file', 'edit_file', 'multi_edit']);
30
+
31
+ function normalizarModo(m) {
32
+ const s = String(m || '').trim().toLowerCase();
33
+ const apelido = { ask: 'perguntar', suggest: 'perguntar', full: 'livre', 'full-auto': 'livre', yolo: 'livre' };
34
+ const n = apelido[s] || s;
35
+ return MODOS.includes(n) ? n : 'auto';
36
+ }
37
+
38
+ /* Quebra o comando em trechos (separados por ; && || | e quebra de linha) e
39
+ cada trecho em palavras — respeitando aspas. Sem respeitar aspas,
40
+ `git commit -m "tira o rm -rf /"` viraria um trecho `rm -rf /` e o Primo
41
+ pararia para perguntar sobre uma mensagem de commit. */
42
+ function trechos(cmd) {
43
+ const saida = [];
44
+ let palavras = [], atual = '', aspa = null, temPalavra = false;
45
+ const fechaPalavra = () => { if (temPalavra) palavras.push(atual); atual = ''; temPalavra = false; };
46
+ const fechaTrecho = (sep) => { fechaPalavra(); if (palavras.length) saida.push({ palavras, sep }); palavras = []; };
47
+ const s = String(cmd || '');
48
+ for (let i = 0; i < s.length; i++) {
49
+ const ch = s[i];
50
+ if (aspa) {
51
+ if (ch === aspa) aspa = null;
52
+ else if (ch === '\\' && aspa === '"' && i + 1 < s.length) atual += s[++i];
53
+ else atual += ch;
54
+ continue;
55
+ }
56
+ if (ch === '"' || ch === "'") { aspa = ch; temPalavra = true; continue; }
57
+ if (ch === '\\' && i + 1 < s.length) { atual += s[++i]; temPalavra = true; continue; }
58
+ if (ch === ';' || ch === '\n') { fechaTrecho(';'); continue; }
59
+ if (ch === '&' && s[i + 1] === '&') { fechaTrecho(';'); i++; continue; }
60
+ if (ch === '|' && s[i + 1] === '|') { fechaTrecho(';'); i++; continue; }
61
+ if (ch === '|') { fechaTrecho('|'); continue; }
62
+ if (ch === '&') { fechaTrecho(';'); continue; }
63
+ if (/\s/.test(ch)) { fechaPalavra(); continue; }
64
+ atual += ch; temPalavra = true;
65
+ }
66
+ fechaTrecho(';');
67
+ return saida;
68
+ }
69
+
70
+ const TEMPORARIAS = [...new Set(['/tmp', '/private/tmp', os.tmpdir()])];
71
+
72
+ function dentro(pai, filho) {
73
+ const r = path.relative(pai, filho);
74
+ return !!r && !r.startsWith('..') && !path.isAbsolute(r);
75
+ }
76
+
77
+ function expandir(alvo) {
78
+ const home = os.homedir();
79
+ return String(alvo).replace(/^~(?=\/|$)/, home).replace(/^\$\{?HOME\}?(?=\/|$)/, home);
80
+ }
81
+
82
+ /** O alvo cai FORA do projeto (ou é o projeto inteiro, ou um curinga que o esvazia)? */
83
+ function alvoRuim(alvo, cwd, projeto, { recursivo, raizOk = false }) {
84
+ const a = String(alvo);
85
+ if (/^\.?\/?\*$|^\.\*$/.test(a)) return recursivo ? 'apaga tudo o que tem na pasta' : null;
86
+ const e = expandir(a);
87
+ // Variável que não é o HOME: não dá para saber aonde aponta, e "não sei"
88
+ // num rm é motivo de perguntar.
89
+ if (/\$/.test(e)) return 'apaga um caminho que depende de variável';
90
+ const alvoAbs = path.resolve(cwd, e.replace(/\/\*$/, ''));
91
+ const rel = path.relative(projeto, alvoAbs);
92
+ if (e === '/' || alvoAbs === path.parse(alvoAbs).root) return 'apaga a partir da raiz do computador';
93
+ if (alvoAbs === os.homedir()) return 'apaga a sua pasta pessoal inteira';
94
+ if (rel === '') return raizOk ? null : 'apaga a pasta do projeto inteira';
95
+ // A pasta temporária é descartável por definição: limpar lá não é perigo.
96
+ // (Mas nunca o que mora dentro da sua pasta pessoal.)
97
+ if (!dentro(os.homedir(), alvoAbs) && TEMPORARIAS.some((t) => dentro(t, alvoAbs))) return null;
98
+ if (rel.startsWith('..') || path.isAbsolute(rel)) return `apaga algo fora da pasta do projeto (${a})`;
99
+ return null;
100
+ }
101
+
102
+ // Palavras que vêm ANTES do comando de verdade e não mudam o que ele faz.
103
+ const PREFIXOS = new Set(['env', 'nohup', 'time', 'command', 'exec', 'nice', 'xargs']);
104
+
105
+ function analisarTrecho(palavras, estado) {
106
+ let p = palavras.slice();
107
+ // `VAR=x cmd`, `env -i cmd`, `xargs -0 rm`: pula até o comando.
108
+ while (p.length && (PREFIXOS.has(p[0]) || /^[A-Za-z_]\w*=/.test(p[0]) || (estado.pulandoFlags && /^-/.test(p[0])))) {
109
+ estado.pulandoFlags = PREFIXOS.has(p[0]) && p[0] !== 'command';
110
+ p.shift();
111
+ }
112
+ estado.pulandoFlags = false;
113
+ if (!p.length) return null;
114
+ const cmd = path.basename(p[0]);
115
+ const args = p.slice(1);
116
+ const flags = args.filter((x) => /^-/.test(x));
117
+ const alvos = args.filter((x) => !/^-/.test(x));
118
+ const tem = (re) => flags.some((f) => re.test(f));
119
+
120
+ if (cmd === 'sudo' || cmd === 'doas') return 'usa sudo: mexe como administrador no computador inteiro';
121
+ if (cmd === 'cd') { estado.cwd = path.resolve(estado.cwd, expandir(alvos[0] || os.homedir())); return null; }
122
+ if ((cmd === 'sh' || cmd === 'bash' || cmd === 'zsh') && args[0] === '-c' && args[1]) {
123
+ return analisarComando(args[1], estado.cwd, estado.projeto);
124
+ }
125
+ if (cmd === 'rm' || cmd === 'rmdir' || cmd === 'trash') {
126
+ const recursivo = cmd === 'rmdir' || tem(/^-[a-zA-Z]*[rR]|^--recursive$/);
127
+ for (const a of alvos) {
128
+ const r = alvoRuim(a, estado.cwd, estado.projeto, { recursivo });
129
+ if (r) return r;
130
+ }
131
+ return null;
132
+ }
133
+ if (cmd === 'find' && args.includes('-delete')) {
134
+ for (const a of alvos.slice(0, 1)) {
135
+ const r = alvoRuim(a, estado.cwd, estado.projeto, { recursivo: true, raizOk: true });
136
+ if (r) return r.replace(/^apaga/, 'apaga com find');
137
+ }
138
+ return null;
139
+ }
140
+ if (cmd === 'git') {
141
+ const sub = alvos[0];
142
+ if (sub === 'push' && (tem(/^--force$|^-[a-zA-Z]*f/) || alvos.some((a) => /^\+/.test(a)))) {
143
+ return 'git push --force: reescreve o histórico no GitHub, e o que estava lá some';
144
+ }
145
+ if (sub === 'reset' && flags.includes('--hard')) return 'git reset --hard: joga fora o que não foi salvo em commit';
146
+ if (sub === 'clean' && tem(/^-[a-zA-Z]*f/) && !tem(/^-[a-zA-Z]*n|^--dry-run$/)) {
147
+ return 'git clean: apaga arquivos que o git não guardou';
148
+ }
149
+ return null;
150
+ }
151
+ if (cmd === 'dd') return 'dd: escreve direto no disco';
152
+ if (/^mkfs(\.|$)/.test(cmd) || cmd === 'newfs' || cmd === 'fdisk') return 'formata um disco';
153
+ if (cmd === 'diskutil' && /^(erase|zero|secureErase|partitionDisk|reformat)/i.test(alvos[0] || '')) return 'diskutil: apaga um disco';
154
+ if ((cmd === 'chmod' || cmd === 'chown') && tem(/^-[a-zA-Z]*R/)) {
155
+ for (const a of alvos) {
156
+ const e = path.resolve(estado.cwd, expandir(a));
157
+ if (e === '/' || e === os.homedir()) return `${cmd} -R em ${a}: muda a permissão de tudo`;
158
+ }
159
+ return null;
160
+ }
161
+ if (cmd === 'kill' && args.includes('-1')) return 'kill -1: encerra todos os seus programas';
162
+ if (['shutdown', 'reboot', 'halt', 'poweroff'].includes(cmd)) return 'desliga ou reinicia o computador';
163
+ if (['npm', 'pnpm', 'yarn'].includes(cmd) && alvos[0] === 'publish' && !flags.includes('--dry-run')) return `${cmd} publish: publica o pacote para o mundo inteiro`;
164
+ return null;
165
+ }
166
+
167
+ function analisarComando(comando, cwd, projeto) {
168
+ const s = String(comando || '');
169
+ // Baixar e rodar na hora: o que roda é o que o site mandar naquele segundo.
170
+ if (/\b(curl|wget)\b[^;&]*\|\s*(sudo\s+)?(env\s+)?(ba|z|da|k)?sh\b/.test(s)) {
171
+ return 'baixa um script da internet e roda sem ler';
172
+ }
173
+ const estado = { cwd, projeto, pulandoFlags: false };
174
+ for (const t of trechos(s)) {
175
+ const r = analisarTrecho(t.palavras, estado);
176
+ if (r) return r;
177
+ }
178
+ return null;
179
+ }
180
+
181
+ /**
182
+ * Esta ação é perigosa?
183
+ * @returns {{perigoso: boolean, motivo: string}}
184
+ */
185
+ function classificar(nome, args, { projectDir } = {}) {
186
+ const a = args || {};
187
+ const projeto = path.resolve(projectDir || process.cwd());
188
+ let motivo = null;
189
+ if (COMANDOS.has(nome)) {
190
+ const cwd = a.cwd ? path.resolve(projeto, expandir(a.cwd)) : projeto;
191
+ motivo = analisarComando(a.command, cwd, projeto);
192
+ } else if (ESCRITAS.has(nome) && a.path) {
193
+ const alvo = path.resolve(projeto, expandir(a.path));
194
+ const rel = path.relative(projeto, alvo);
195
+ if (rel.startsWith('..') || path.isAbsolute(rel) || /^[a-zA-Z]:[\\/]/.test(String(a.path))) {
196
+ motivo = `mexe num arquivo fora da pasta do projeto (${a.path})`;
197
+ }
198
+ }
199
+ return { perigoso: !!motivo, motivo: motivo || '' };
200
+ }
201
+
202
+ function novo() { return { liberados: new Set(), negados: new Set() }; }
203
+
204
+ /* Uma pergunta por vez. Subagentes rodam em paralelo (Promise.all) e dois
205
+ pedidos ao mesmo tempo no mesmo terminal se atropelariam. */
206
+ let fila = Promise.resolve();
207
+ function emFila(fn) { const p = fila.then(fn, fn); fila = p.catch(() => {}); return p; }
208
+
209
+ /**
210
+ * O portão. Devolve {ok:true} ou {ok:false, error, motivo}.
211
+ *
212
+ * `perguntar(frase, {tipo, motivo, primeira})` → 'agora' | 'sessao' | 'nao',
213
+ * o mesmo contrato do portão do desktop (lib/permissao.js).
214
+ */
215
+ async function portao(estado, { modo, nome, args, projectDir, subagent = false, perguntar = null }) {
216
+ const m = normalizarModo(modo);
217
+ if (m === 'livre') return { ok: true };
218
+ const { perigoso, motivo: motivoPerigo } = classificar(nome, args, { projectDir });
219
+ const tipo = perigoso ? 'perigo' : (m === 'perguntar' && COMANDOS.has(nome)) ? 'comando'
220
+ : (m === 'perguntar' && ESCRITAS.has(nome)) ? 'arquivo' : null;
221
+ if (!tipo) return { ok: true };
222
+
223
+ const motivo = perigoso ? motivoPerigo
224
+ : tipo === 'comando' ? `rodar: ${String((args || {}).command || '').slice(0, 80)}`
225
+ : `escrever ${String((args || {}).path || 'um arquivo')}`;
226
+ const recusa = (texto) => ({ ok: false, motivo, error: texto });
227
+
228
+ // Escrever fora da pasta o tools.js já recusa sozinho: perguntar seria
229
+ // pedir um "sim" que não serve para nada.
230
+ if (perigoso && ESCRITAS.has(nome)) {
231
+ return recusa(`Recusado: ${motivo}. O PrimoCode só escreve dentro da pasta do projeto — use /dir se a pessoa quiser trabalhar em outra pasta.`);
232
+ }
233
+ if (subagent) {
234
+ return recusa(`Recusado: ${motivo}. Subagente não tem como pedir autorização — `
235
+ + 'deixe esse passo para o agente principal e diga no seu resumo o que falta fazer.');
236
+ }
237
+ const chave = `${nome}:${JSON.stringify(args || {})}`;
238
+ const negado = `A pessoa não autorizou: ${motivo}. Siga por outro caminho ou pergunte.`;
239
+ if (estado.negados.has(chave)) return recusa(negado);
240
+ // "A sessão inteira" libera o TIPO de pergunta: no perigoso, aquele motivo
241
+ // (um push --force autorizado não autoriza um rm -rf ~); no modo
242
+ // perguntar, os comandos ou as escritas comuns.
243
+ const liberacao = perigoso ? `perigo:${motivo}` : tipo;
244
+ if (estado.liberados.has(liberacao)) return { ok: true };
245
+ if (typeof perguntar !== 'function') {
246
+ return recusa(`Recusado: ${motivo}. Isso precisa de autorização e não há ninguém para autorizar neste modo.`);
247
+ }
248
+
249
+ const resposta = await emFila(() => perguntar(motivo, { tipo, motivo, primeira: false }));
250
+ if (resposta === 'sessao') { estado.liberados.add(liberacao); return { ok: true }; }
251
+ if (resposta === 'agora') return { ok: true };
252
+ estado.negados.add(chave);
253
+ return recusa(negado);
254
+ }
255
+
256
+ module.exports = { classificar, portao, novo, normalizarModo, trechos, MODOS };
@@ -0,0 +1,118 @@
1
+ /**
2
+ * pre-leitura.js — os arquivos que o agente ia ler, já lidos.
3
+ *
4
+ * "Toda vez a IA lê as pastas novamente, só pra ler vão 30 chamadas de API."
5
+ * (dono, 24/09/2026)
6
+ *
7
+ * O caro não é o disco — ler index.html leva 1ms. O caro é a IDA AO MODELO: o
8
+ * servidor grátis conta pedidos por dia, e cada "deixa eu ler o style.css"
9
+ * numa resposta própria é um pedido. Medido com um modelo de mentira que
10
+ * segue uma política fixa (lê o que ainda não viu, um arquivo por resposta,
11
+ * e confere o index.html antes de editar), num "muda a cor do botão" em
12
+ * projeto de 10 arquivos: 9 pedidos ao modelo antes, 2 depois. Com todos os
13
+ * arquivos grandes demais para entrar aqui, a ordem de juntar leituras e a
14
+ * releitura sem mudança (lib/act.js, RELEITURA) ainda levam de 9 para 4.
15
+ *
16
+ * O mapa do projeto já dava os NOMES. Isto põe o CONTEÚDO dos poucos arquivos
17
+ * que quase todo pedido de página lê — o citado no pedido primeiro, depois
18
+ * index.html, o css e o js principais, package.json, README — dentro de um
19
+ * orçamento. Pequeno pelo servidor grátis (o teto é de ~6 mil tokens por
20
+ * MINUTO, e isto vai em toda rodada); folgado com um provedor próprio.
21
+ *
22
+ * Fica de fora o que não se lê como texto ou não vale o espaço: binário,
23
+ * minificado, lockfile, arquivo grande, e o que já entra por outro caminho
24
+ * (memory.md, PRIMO.md/AGENTS.md/CLAUDE.md). Arquivo que não cabe inteiro no
25
+ * que sobrou do orçamento NÃO entra cortado: meio arquivo marcado como
26
+ * "conteúdo atual" faria o modelo editar às cegas a metade que não viu.
27
+ *
28
+ * Puro, menos a leitura do disco — o teste mede a escolha sem rede.
29
+ */
30
+ 'use strict';
31
+
32
+ const fs = require('fs');
33
+ const path = require('path');
34
+
35
+ const ORCAMENTO_SERVIDOR = 6 * 1024;
36
+ const ORCAMENTO_LOCAL = 40 * 1024;
37
+ const MAIOR_ARQUIVO = 20 * 1024;
38
+
39
+ const TEXTO = /\.(html?|css|scss|m?jsx?|tsx?|vue|svelte|json|md|txt|py|toml|ya?ml)$/i;
40
+ const LOCKFILES = new Set(['package-lock.json', 'yarn.lock', 'pnpm-lock.yaml', 'bun.lockb',
41
+ 'composer.lock', 'Cargo.lock', 'poetry.lock', 'Gemfile.lock']);
42
+ // Já entram no contexto por outro caminho (buildMemoryContext / mencoes.js).
43
+ const JA_ENTRAM = new Set(['memory.md', 'PRIMO.md', 'AGENTS.md', 'CLAUDE.md']);
44
+
45
+ /** O orçamento de agora: folgado só com provedor próprio (/api). */
46
+ function orcamento() {
47
+ try { return require('./local/provedor.js').atual() ? ORCAMENTO_LOCAL : ORCAMENTO_SERVIDOR; }
48
+ catch { return ORCAMENTO_SERVIDOR; }
49
+ }
50
+
51
+ /** Quanto o arquivo importa: menor é mais importante; null = não entra. */
52
+ function prioridade(rel, pedido) {
53
+ const nome = path.basename(rel);
54
+ if (JA_ENTRAM.has(nome) || LOCKFILES.has(nome) || /\.min\./i.test(nome) || !TEXTO.test(nome)) return null;
55
+ const fundo = rel.split(/[\\/]/).length - 1;
56
+ const texto = String(pedido || '').toLowerCase();
57
+ // Citado no pedido ("muda o style.css") é o que ele vai ler com certeza.
58
+ if (texto && (texto.includes(rel.toLowerCase()) || texto.includes(nome.toLowerCase()))) return 0;
59
+ if (fundo > 1) return null;
60
+ if (/^index\.html?$/i.test(nome)) return 1 + fundo;
61
+ if (/\.(css|scss)$/i.test(nome)) return 3 + fundo;
62
+ if (/\.(m?jsx?|tsx?|vue|svelte)$/i.test(nome) && !/\.(test|spec|config)\./i.test(nome)) return 5 + fundo;
63
+ if (nome === 'package.json' && fundo === 0) return 7;
64
+ if (/^readme\.md$/i.test(nome) && fundo === 0) return 8;
65
+ return null;
66
+ }
67
+
68
+ /**
69
+ * Escolhe e lê. Devolve [{ rel, conteudo }] na ordem de importância.
70
+ * @param {string} dir a pasta do projeto
71
+ * @param {string[]} arquivos caminhos relativos (o mesmo que o mapa listou)
72
+ * @param {string} pedido o texto do pedido — o citado nele vem primeiro
73
+ * @param {number} limite bytes no total
74
+ */
75
+ function escolher(dir, arquivos, pedido, limite = orcamento()) {
76
+ const candidatos = (arquivos || [])
77
+ .map((rel) => ({ rel, p: prioridade(rel, pedido) }))
78
+ .filter((x) => x.p !== null)
79
+ .sort((a, b) => a.p - b.p || a.rel.localeCompare(b.rel));
80
+ const lidos = [];
81
+ let soma = 0;
82
+ for (const { rel } of candidatos) {
83
+ const abs = path.join(dir, rel);
84
+ let st;
85
+ try { st = fs.statSync(abs); } catch { continue; }
86
+ if (!st.isFile() || st.size === 0 || st.size > MAIOR_ARQUIVO || soma + st.size > limite) continue;
87
+ let buf;
88
+ try { buf = fs.readFileSync(abs); } catch { continue; }
89
+ if (buf.includes(0)) continue; // binário com nome de texto
90
+ const conteudo = buf.toString('utf-8');
91
+ // Uma linha de milhares de caracteres é minificado sem ".min" no nome.
92
+ if (conteudo.split('\n').some((l) => l.length > 2000)) continue;
93
+ lidos.push({ rel, conteudo });
94
+ soma += st.size;
95
+ }
96
+ return lidos;
97
+ }
98
+
99
+ /** O bloco que vai no contexto, ou '' quando não há nada que valha. */
100
+ function bloco(lidos) {
101
+ if (!lidos || !lidos.length) return '';
102
+ const partes = lidos.map(({ rel, conteudo }) =>
103
+ `[conteúdo atual de ${rel} — não precisa ler de novo]\n${conteudo.replace(/\s+$/, '')}\n[fim de ${rel}]`);
104
+ return 'Estes arquivos estão aqui como estavam no COMEÇO deste pedido — use o que está abaixo em vez de '
105
+ + 'chamar read_file neles. Depois que você editar um deles, o texto abaixo fica velho: aí, sim, leia.\n\n'
106
+ + partes.join('\n\n');
107
+ }
108
+
109
+ /* A ordem de juntar. Vai junto do mapa porque é ali que o modelo decide o que
110
+ ler. Cada resposta dele é uma ida ao servidor; cinco leituras em cinco
111
+ respostas custam cinco pedidos, e as mesmas cinco numa resposta só custam
112
+ um. */
113
+ const JUNTAR_LEITURAS = 'Precisa ler, listar ou buscar mais de uma coisa? Peça TUDO na MESMA resposta, '
114
+ + 'com várias chamadas de ferramenta juntas — nunca uma leitura por resposta. Cada resposta sua é '
115
+ + 'uma ida ao servidor, e o plano conta idas.';
116
+
117
+ module.exports = { escolher, bloco, prioridade, orcamento, JUNTAR_LEITURAS,
118
+ ORCAMENTO_SERVIDOR, ORCAMENTO_LOCAL, MAIOR_ARQUIVO };
package/lib/repeticao.js CHANGED
@@ -173,9 +173,11 @@ function ver(registro, nome, args) {
173
173
  }
174
174
 
175
175
  registro.vistos.set(chave, vezes);
176
- if (vezes === 1) return { repetida: false, vezes, aviso: null };
176
+ // `chave` volta para quem chamou guardar o resultado da leitura pela
177
+ // mesma época em que ela foi vista (lib/act.js, RELEITURA).
178
+ if (vezes === 1) return { repetida: false, vezes, aviso: null, chave };
177
179
 
178
- return { repetida: true, vezes, aviso: aviso(nome, vezes, parecidaCom) };
180
+ return { repetida: true, vezes, aviso: aviso(nome, vezes, parecidaCom), chave };
179
181
  }
180
182
 
181
183
  /** O recado, apertando a cada volta. */
package/lib/skills.js CHANGED
@@ -345,9 +345,14 @@ const semAcento = (v) => String(v || '').toLowerCase()
345
345
  function pontuar(skill, texto) {
346
346
  const t = semAcento(texto);
347
347
  let pontos = 0;
348
+ // "página, pagina" no gatilho é UMA palavra escrita de dois jeitos: sem
349
+ // acento as duas são iguais e contavam dois pontos, e a skill que listava
350
+ // as duas grafias ganhava das outras só por isso.
351
+ const vistas = new Set();
348
352
  for (const palavra of skill.quando) {
349
353
  const p = semAcento(palavra).trim();
350
- if (!p) continue;
354
+ if (!p || vistas.has(p)) continue;
355
+ vistas.add(p);
351
356
  const escapado = p.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
352
357
  // A borda e escrita a mao em vez de `\b`: o gatilho pode comecar ou
353
358
  // terminar em simbolo ("e-mail"), e ali o `\b` nao vale.
package/lib/tempos.js ADDED
@@ -0,0 +1,62 @@
1
+ /**
2
+ * tempos.js — para onde foi o tempo de um pedido.
3
+ *
4
+ * PRIMOCODE_TEMPOS=1 primo
5
+ *
6
+ * No fim de cada pedido sai UMA linha com o que custou cada fase: o plano, a
7
+ * espera do resumo, o nome da pasta, cada rodada do modelo, as ferramentas e
8
+ * a conferência da página. Existe porque "está lento" sem número vira chute:
9
+ * medido em 24/09 com um servidor de mentira que responde na hora, o que o
10
+ * PrimoCode gastava sozinho num pedido de página era quase todo a conferência
11
+ * no navegador escondido (2,1 a 4,6s num Chrome recém-aberto), e não o que
12
+ * parecia: as ferramentas de três rodadas somaram 7 a 23ms, e o planejador
13
+ * nem é chamado em pedido de uma fase só.
14
+ *
15
+ * Desligado, não custa nada: `medir` chama a função e devolve, sem relógio.
16
+ *
17
+ * Um pedido por vez (é assim que o CLI trabalha), então o registro é do
18
+ * módulo. Subagente soma no mesmo pedido, com o nome da fase.
19
+ */
20
+ 'use strict';
21
+
22
+ let atual = null;
23
+
24
+ function ligado() { return process.env.PRIMOCODE_TEMPOS === '1'; }
25
+
26
+ /** Começa o registro de um pedido. Desligado, não faz nada. */
27
+ function comecar() {
28
+ atual = ligado() ? { inicio: Date.now(), fases: new Map() } : null;
29
+ return atual;
30
+ }
31
+
32
+ /** Soma `ms` à fase `nome` (várias rodadas somam na mesma linha, com contagem). */
33
+ function somar(nome, ms) {
34
+ if (!atual) return;
35
+ const f = atual.fases.get(nome) || { ms: 0, vezes: 0 };
36
+ f.ms += ms;
37
+ f.vezes++;
38
+ atual.fases.set(nome, f);
39
+ }
40
+
41
+ /** Mede uma função (síncrona ou async) e soma o tempo à fase. */
42
+ async function medir(nome, fn) {
43
+ if (!atual) return fn();
44
+ const t = Date.now();
45
+ try { return await fn(); } finally { somar(nome, Date.now() - t); }
46
+ }
47
+
48
+ function fmt(ms) { return ms >= 1000 ? `${(ms / 1000).toFixed(1)}s` : `${ms}ms`; }
49
+
50
+ /** A linha do fim. Encerra o registro. null se estava desligado. */
51
+ function relatorio() {
52
+ if (!atual) return null;
53
+ const total = Date.now() - atual.inicio;
54
+ const partes = [...atual.fases].map(([nome, f]) =>
55
+ `${nome} ${fmt(f.ms)}${f.vezes > 1 ? ` (${f.vezes}x)` : ''}`);
56
+ atual = null;
57
+ return `tempos · ${partes.join(' · ')}${partes.length ? ' · ' : ''}total ${fmt(total)}`;
58
+ }
59
+
60
+ function ativo() { return !!atual; }
61
+
62
+ module.exports = { ligado, comecar, somar, medir, relatorio, ativo };