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.
- package/README.md +33 -6
- package/app/ansi.js +55 -0
- package/app/app.css +482 -281
- package/app/app.js +888 -315
- package/app/index.html +176 -101
- package/app/primo-marca.js +58 -1
- package/bin/primocode.js +697 -118
- package/comandos/explicar.md +14 -0
- package/comandos/publicar.md +10 -0
- package/comandos/revisar.md +13 -0
- package/lib/act.js +130 -26
- package/lib/api.js +8 -1
- package/lib/app.js +317 -74
- package/lib/browser.js +79 -3
- package/lib/catalogo.js +11 -3
- package/lib/checkpoint-git.js +218 -0
- package/lib/checkpoint.js +249 -0
- package/lib/claude-engine.js +25 -3
- package/lib/comandos-extra.js +481 -0
- package/lib/comandos.js +104 -0
- package/lib/config.js +21 -4
- package/lib/estudio/acervo-site.js +157 -0
- package/lib/estudio/assets.js +77 -3
- package/lib/estudio/cinema.js +354 -0
- package/lib/estudio/composicao.js +470 -0
- package/lib/estudio/edicao.js +450 -0
- package/lib/estudio/impacto.js +263 -0
- package/lib/estudio/index.js +769 -5
- package/lib/estudio/plano.js +5 -2
- package/lib/estudio/remotion.js +228 -23
- package/lib/estudio/slides.js +34 -30
- package/lib/fala.js +95 -1
- package/lib/local/cerebro.json +361 -180
- package/lib/local/entrada.js +18 -8
- package/lib/local/motor-cli.js +1 -1
- package/lib/mencoes.js +98 -0
- package/lib/offline.js +168 -0
- package/lib/painel.js +291 -0
- package/lib/perigo.js +256 -0
- package/lib/pre-leitura.js +118 -0
- package/lib/repeticao.js +4 -2
- package/lib/skills.js +6 -1
- package/lib/tempos.js +62 -0
- package/lib/ui.js +171 -17
- package/package.json +3 -2
- package/skills/apresentacao-pptx/SKILL.md +52 -0
- package/skills/cartaz-de-arte/LICENSE.txt +202 -0
- package/skills/cartaz-de-arte/SKILL.md +47 -0
- package/skills/criar-skill/LICENSE.txt +202 -0
- package/skills/criar-skill/SKILL.md +49 -0
- package/skills/depurar-de-verdade/LICENSE.txt +21 -0
- package/skills/depurar-de-verdade/SKILL.md +14 -5
- package/skills/design-de-verdade/LICENSE.txt +177 -0
- package/skills/design-de-verdade/SKILL.md +42 -0
- package/skills/documento-word/SKILL.md +57 -0
- package/skills/interface-revisada/SKILL.md +47 -0
- package/skills/me-ensina/LICENSE.txt +21 -0
- package/skills/me-ensina/SKILL.md +27 -0
- package/skills/me-entrevista/LICENSE.txt +21 -0
- package/skills/me-entrevista/SKILL.md +27 -0
- package/skills/motion-de-primeira/SKILL.md +1 -1
- package/skills/pdf-bonito/SKILL.md +48 -0
- package/skills/planilha-excel/SKILL.md +47 -0
- package/skills/react-sem-dor/SKILL.md +48 -0
- package/skills/revisao-antes-de-entregar/LICENSE.txt +21 -0
- package/skills/revisao-antes-de-entregar/SKILL.md +6 -1
- package/skills/site-com-motion/SKILL.md +39 -0
- package/skills/temas-prontos/LICENSE.txt +202 -0
- package/skills/temas-prontos/SKILL.md +50 -0
- package/skills/testar-o-site/LICENSE.txt +202 -0
- package/skills/testar-o-site/SKILL.md +46 -0
- package/skills/video-no-remotion/SKILL.md +48 -0
- package/studio/narrar.py +111 -10
- 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
|
-
|
|
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 };
|