primocode 9.8.0-beta.6 → 9.8.0-beta.7
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 +2 -2
- package/bin/primocode.js +23 -5
- package/lib/act.js +66 -7
- package/lib/browser.js +65 -2
- package/lib/pre-leitura.js +118 -0
- package/lib/repeticao.js +4 -2
- package/lib/tempos.js +62 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# PrimoCode v9.8.0-beta.
|
|
1
|
+
# PrimoCode v9.8.0-beta.7
|
|
2
2
|
|
|
3
3
|
Agente de engenharia brasileiro para o terminal, no estilo Claude Code.
|
|
4
4
|
Cria arquivos de verdade, roda comandos, **controla o navegador e o desktop** —
|
|
@@ -9,7 +9,7 @@ OpenRouter, e cada apelido (`top`/`main`/`fast`) é uma **corrente**: se um mode
|
|
|
9
9
|
está no limite de pedidos, o próximo atende. Você não vê o erro, e não paga nada.
|
|
10
10
|
|
|
11
11
|
```
|
|
12
|
-
▐▛███▜▌ Primo Code v9.8.0-beta.
|
|
12
|
+
▐▛███▜▌ Primo Code v9.8.0-beta.7
|
|
13
13
|
▝▜█████▛▘ Bem-vindo de volta, Joel
|
|
14
14
|
▘▘ ▝▝ ~/primocode · /dir <pasta> muda
|
|
15
15
|
```
|
package/bin/primocode.js
CHANGED
|
@@ -53,6 +53,7 @@ extra.instalarContador(api, contadorDaSessao, () => {
|
|
|
53
53
|
const p = provedorLocal.atual();
|
|
54
54
|
return p ? p.nome : 'Servidor PrimoCode';
|
|
55
55
|
});
|
|
56
|
+
const tempos = require('../lib/tempos');
|
|
56
57
|
|
|
57
58
|
// Rede de segurança de último recurso: sem isto, uma exceção fora de um
|
|
58
59
|
// try/catch (ex.: dentro de um callback de socket, como o parser de SSE) não
|
|
@@ -1055,6 +1056,16 @@ async function sendPrompt(userText) {
|
|
|
1055
1056
|
return;
|
|
1056
1057
|
}
|
|
1057
1058
|
|
|
1059
|
+
// PRIMOCODE_TEMPOS=1: uma linha no fim com o custo de cada fase.
|
|
1060
|
+
tempos.comecar();
|
|
1061
|
+
try { await enviarPedido(userText); }
|
|
1062
|
+
finally {
|
|
1063
|
+
const linha = tempos.relatorio();
|
|
1064
|
+
if (linha) console.log(c.dim(` ⏱ ${linha}`));
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
async function enviarPedido(userText) {
|
|
1058
1069
|
let prompt = userText;
|
|
1059
1070
|
// "@index.html" no pedido é um /attach só deste pedido: vai junto dos
|
|
1060
1071
|
// anexos, mas não fica em state.attachedFiles para o próximo.
|
|
@@ -1091,15 +1102,22 @@ async function sendPrompt(userText) {
|
|
|
1091
1102
|
|
|
1092
1103
|
// O resumo da rodada anterior é feito em segundo plano logo depois
|
|
1093
1104
|
// dela; aqui só se espera ele terminar, para não sair com memória velha.
|
|
1094
|
-
|
|
1095
|
-
|
|
1105
|
+
//
|
|
1096
1106
|
// O pedido passa por uma cabeça antes de virar trabalho: conserta o
|
|
1097
1107
|
// que está torto, diz qual é o entregável e o que conta como pronto.
|
|
1098
1108
|
// UMA chamada faz as duas coisas: arruma o pedido torto e, se fizer
|
|
1099
1109
|
// sentido, divide em passos. Eram duas idas ao modelo por pedido, e
|
|
1100
1110
|
// num plano limitado por REQUISIÇÕES (o grátis da Groq são 30 por
|
|
1101
1111
|
// minuto) cada chamada extra custa tanto quanto token.
|
|
1102
|
-
|
|
1112
|
+
//
|
|
1113
|
+
// As duas esperas correm JUNTAS. Uma não lê o que a outra escreve: o
|
|
1114
|
+
// plano só olha o texto do pedido, e o resumo só a conversa de antes.
|
|
1115
|
+
// Em fila, o pedido de duas fases pagava o resumo e DEPOIS o plano —
|
|
1116
|
+
// duas idas ao modelo em série antes da primeira rodada.
|
|
1117
|
+
const [, passos] = await Promise.all([
|
|
1118
|
+
tempos.medir('espera do resumo', () => esperarResumo()),
|
|
1119
|
+
tempos.medir('plano', () => dividirTarefa(prompt)),
|
|
1120
|
+
]);
|
|
1103
1121
|
const instrucaoKit = await prepararKit(plan, userText);
|
|
1104
1122
|
|
|
1105
1123
|
let r = null;
|
|
@@ -1214,8 +1232,8 @@ async function sendPrompt(userText) {
|
|
|
1214
1232
|
state.gravador.turno(userText, r.assistantMessage.content,
|
|
1215
1233
|
{ modo: 'primocode', projeto: state.currentProject || null });
|
|
1216
1234
|
} else {
|
|
1217
|
-
await esperarResumo();
|
|
1218
|
-
await runChat(prompt, userText);
|
|
1235
|
+
await tempos.medir('espera do resumo', () => esperarResumo());
|
|
1236
|
+
await tempos.medir('chat', () => runChat(prompt, userText));
|
|
1219
1237
|
}
|
|
1220
1238
|
|
|
1221
1239
|
state.attachedFiles = [];
|
package/lib/act.js
CHANGED
|
@@ -24,6 +24,8 @@ const permissao = require('./permissao');
|
|
|
24
24
|
const perigo = require('./perigo');
|
|
25
25
|
const skills = require('./skills');
|
|
26
26
|
const checkpoint = require('./checkpoint');
|
|
27
|
+
const tempos = require('./tempos');
|
|
28
|
+
const preLeitura = require('./pre-leitura');
|
|
27
29
|
|
|
28
30
|
// 40, não 15: com o freio de repetição (repeticao.js) e o de rodada parada,
|
|
29
31
|
// volta a mais não é volta em círculo — é trabalho. Quinze acabavam no meio
|
|
@@ -35,6 +37,8 @@ const MAX_EMPURROES = 3;
|
|
|
35
37
|
// Voltas seguidas sem executar nada antes de desistir.
|
|
36
38
|
const MAX_RODADAS_PARADAS = 3;
|
|
37
39
|
const MAX_PROFUNDIDADE = 2; // principal + 1 nível de subagente
|
|
40
|
+
// As leituras que, repetidas sem mudança no meio, devolvem o guardado.
|
|
41
|
+
const RELEITURA = new Set(['read_file', 'list_dir']);
|
|
38
42
|
|
|
39
43
|
// ── Interrupção (Esc Esc) ─────────────────────────────────────────────────
|
|
40
44
|
// O usuário aperta Esc duas vezes e o agente PARA — entre uma rodada e outra
|
|
@@ -382,6 +386,7 @@ async function pausar(ms, subagent) {
|
|
|
382
386
|
const sp = subagent ? null : new Spinner(PENSANDO).start();
|
|
383
387
|
await new Promise((r) => setTimeout(r, ms));
|
|
384
388
|
if (sp) sp.stop();
|
|
389
|
+
tempos.somar('espera de cota/rede', ms);
|
|
385
390
|
}
|
|
386
391
|
|
|
387
392
|
/**
|
|
@@ -451,10 +456,10 @@ function motivoDaParada({ voltas, voltasEmFalso, falhasPendentes, travou }) {
|
|
|
451
456
|
const PULAR_NO_MAPA = new Set(['node_modules', '.git', 'dist', 'build', '.next', '.nuxt', 'out',
|
|
452
457
|
'.cache', 'coverage', '__pycache__', '.venv', 'venv', '.turbo', '.vercel', '.idea', '.vscode']);
|
|
453
458
|
|
|
454
|
-
function
|
|
459
|
+
function arquivosDoProjeto(dir, limite = 80) {
|
|
455
460
|
const path = require('path');
|
|
456
461
|
const fs = require('fs');
|
|
457
|
-
if (!dir || path.resolve(dir) === path.resolve(cfg.ROOT_DIR)) return
|
|
462
|
+
if (!dir || path.resolve(dir) === path.resolve(cfg.ROOT_DIR)) return [];
|
|
458
463
|
const achados = [];
|
|
459
464
|
const andar = (atual, fundo) => {
|
|
460
465
|
if (fundo > 4 || achados.length >= limite) return;
|
|
@@ -470,12 +475,18 @@ function mapaDoProjeto(dir, limite = 80) {
|
|
|
470
475
|
}
|
|
471
476
|
};
|
|
472
477
|
andar(dir, 0);
|
|
478
|
+
return achados;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function mapaDoProjeto(dir, limite = 80, achados = arquivosDoProjeto(dir, limite)) {
|
|
473
482
|
if (!achados.length) return '';
|
|
474
483
|
const mais = achados.length >= limite ? `\n… (mais arquivos — use glob para achar)` : '';
|
|
475
484
|
return `Arquivos que JÁ existem neste projeto (edite-os em vez de recriar):\n${achados.join('\n')}${mais}`;
|
|
476
485
|
}
|
|
477
486
|
|
|
478
|
-
|
|
487
|
+
/* `pedido` é opcional: com ele, o arquivo citado no pedido é o primeiro a
|
|
488
|
+
entrar já lido (ver lib/pre-leitura.js). */
|
|
489
|
+
function buildMemoryContext(projectDir, projectName, pedido = '') {
|
|
479
490
|
const parts = [];
|
|
480
491
|
// As instruções vêm primeiro: são regra de quem manda no projeto, e a
|
|
481
492
|
// memória é o que o agente anotou. Entram aqui (e não no bin) para valer
|
|
@@ -486,8 +497,11 @@ function buildMemoryContext(projectDir, projectName) {
|
|
|
486
497
|
if (geral.trim()) parts.push(`[memory-geral.md]\n${geral}`);
|
|
487
498
|
const projeto = cfg.readProjectMemory(projectDir);
|
|
488
499
|
if (projeto.trim()) parts.push(`[${projectName || 'projeto'}/memory.md]\n${projeto}`);
|
|
489
|
-
const
|
|
490
|
-
|
|
500
|
+
const arquivos = arquivosDoProjeto(projectDir);
|
|
501
|
+
const mapa = mapaDoProjeto(projectDir, 80, arquivos);
|
|
502
|
+
if (mapa) parts.push(`[${projectName || 'projeto'}/arquivos]\n${mapa}\n\n${preLeitura.JUNTAR_LEITURAS}`);
|
|
503
|
+
const lidos = preLeitura.bloco(preLeitura.escolher(projectDir, arquivos, pedido));
|
|
504
|
+
if (lidos) parts.push(lidos);
|
|
491
505
|
return parts.join('\n\n');
|
|
492
506
|
}
|
|
493
507
|
|
|
@@ -520,6 +534,11 @@ async function runAct(userPrompt, opts) {
|
|
|
520
534
|
/* Quem abre a página para conferir antes do "pronto". Injetável pelo
|
|
521
535
|
mesmo motivo do `motor`: o teste mede a regra sem abrir um Chrome. */
|
|
522
536
|
medirPagina = (dir) => require('./pipeline.js').medirNoNavegador(dir),
|
|
537
|
+
/* Quem LIGA o navegador escondido antes da hora. Injetável pelo mesmo
|
|
538
|
+
motivo do `medirPagina` — e só vale junto com o padrão dele: um
|
|
539
|
+
teste que troca o medidor não pode subir um Chrome de verdade. */
|
|
540
|
+
aquecerPagina = opts && opts.medirPagina ? null
|
|
541
|
+
: () => require('./browser.js').aquecerRevisor(),
|
|
523
542
|
} = opts || {};
|
|
524
543
|
|
|
525
544
|
// A pasta de projeto só nasce quando o agente for mesmo MEXER EM ARQUIVO.
|
|
@@ -550,6 +569,7 @@ async function runAct(userPrompt, opts) {
|
|
|
550
569
|
let mexeuNaPagina = false; // mexeu desde a última conferência?
|
|
551
570
|
let conferencias = 0; // até duas: entregar, e conferir o conserto
|
|
552
571
|
let avisoDaPagina = ''; // o que ainda estava quebrado na última
|
|
572
|
+
let msConferindo = 0; // para o PRIMOCODE_TEMPOS separar as fases
|
|
553
573
|
/* Abre a página e devolve o relato quando é para VOLTAR (achou problema e
|
|
554
574
|
ainda cabe uma volta); null quando é para entregar. O que ficar
|
|
555
575
|
quebrado na última conferência fica em `avisoDaPagina`, e a entrega sai
|
|
@@ -560,10 +580,13 @@ async function runAct(userPrompt, opts) {
|
|
|
560
580
|
conferencias++;
|
|
561
581
|
mexeuNaPagina = false;
|
|
562
582
|
let medida = null;
|
|
583
|
+
const t = Date.now();
|
|
563
584
|
try {
|
|
564
585
|
fala.pensamento('abrindo a página para conferir antes de entregar', profundidade);
|
|
565
586
|
medida = await medirPagina(pastaAtual);
|
|
566
587
|
} catch { medida = null; }
|
|
588
|
+
msConferindo += Date.now() - t;
|
|
589
|
+
tempos.somar('conferir página', Date.now() - t);
|
|
567
590
|
if (!medida || !medida.testou) return null;
|
|
568
591
|
avisoDaPagina = medida.limpo ? ''
|
|
569
592
|
: String(medida.relato || '').split('\n').filter((l) => /·|BRANCO/.test(l)).slice(0, 3).join(' ');
|
|
@@ -574,6 +597,8 @@ async function runAct(userPrompt, opts) {
|
|
|
574
597
|
? `${texto}\n\nAtenção: abri a página e ela ainda tem problema — ${avisoDaPagina.trim()}` : texto);
|
|
575
598
|
// Quem já foi chamado, e quantas vezes. Ver lib/repeticao.js.
|
|
576
599
|
const jaChamado = repeticao.novo();
|
|
600
|
+
// O que read_file/list_dir devolveram, pela chave da época (repeticao.js).
|
|
601
|
+
const leiturasGuardadas = new Map();
|
|
577
602
|
/* O livro deste pedido: arquivos, comandos, links, testes e falhas. Sai
|
|
578
603
|
no fim como balanço (ver lib/registro.js). O subagente tem o seu, que
|
|
579
604
|
não se mistura com o do principal — mas o que ele fez também precisa
|
|
@@ -623,7 +648,7 @@ async function runAct(userPrompt, opts) {
|
|
|
623
648
|
|
|
624
649
|
const messages = [];
|
|
625
650
|
if (!subagent) {
|
|
626
|
-
const memory = buildMemoryContext(projectDir, projectName);
|
|
651
|
+
const memory = buildMemoryContext(projectDir, projectName, userPrompt);
|
|
627
652
|
if (memory) messages.push({ role: 'system', content: `Memória persistente:\n\n${memory}` });
|
|
628
653
|
for (const m of (history || []).slice(-20)) {
|
|
629
654
|
if (m && m.role && m.content) messages.push({ role: m.role, content: m.content });
|
|
@@ -735,6 +760,7 @@ async function runAct(userPrompt, opts) {
|
|
|
735
760
|
let fluxo = null;
|
|
736
761
|
|
|
737
762
|
emVoo = new AbortController();
|
|
763
|
+
const tRodada = Date.now();
|
|
738
764
|
try {
|
|
739
765
|
await motor.requestAct(server, { messages: compactar(messages), model, effort, token, projectDir: pastaAtual, projectName, subagent, sinal: emVoo.signal }, (evt) => {
|
|
740
766
|
if (evt.erro) { streamError = evt.erro; return; }
|
|
@@ -781,6 +807,7 @@ async function runAct(userPrompt, opts) {
|
|
|
781
807
|
}
|
|
782
808
|
sp.stop();
|
|
783
809
|
emVoo = null;
|
|
810
|
+
tempos.somar(subagent ? 'rodada do subagente' : 'rodada do modelo', Date.now() - tRodada);
|
|
784
811
|
if (PARAR) { if (fluxo) fluxo.fechar(); break; }
|
|
785
812
|
|
|
786
813
|
// Chamada de função malformada: o modelo errou a SINTAXE da chamada,
|
|
@@ -957,6 +984,8 @@ async function runAct(userPrompt, opts) {
|
|
|
957
984
|
|
|
958
985
|
let finished = null;
|
|
959
986
|
let algoRodou = false;
|
|
987
|
+
const tTools = Date.now();
|
|
988
|
+
const conferindoAntes = msConferindo;
|
|
960
989
|
|
|
961
990
|
// ORQUESTRAÇÃO: subagentes da MESMA rodada rodam juntos. O modelo que
|
|
962
991
|
// pede "3 agentes, um por arquivo" esperava paralelismo e recebia uma
|
|
@@ -1064,7 +1093,18 @@ async function runAct(userPrompt, opts) {
|
|
|
1064
1093
|
const eco = repeticao.ver(jaChamado, tc.name, tc.arguments);
|
|
1065
1094
|
if (eco.repetida) {
|
|
1066
1095
|
voltasEmFalso++;
|
|
1067
|
-
|
|
1096
|
+
/* RELER SEM MUDANÇA devolve o que já foi lido, uma vez.
|
|
1097
|
+
O aviso seco ("o resultado está na conversa acima") mentia
|
|
1098
|
+
depois do `compactar`: a leitura velha já tinha virado 300
|
|
1099
|
+
caracteres, e o modelo gastava OUTRA ida ao servidor para
|
|
1100
|
+
achar o arquivo por outro caminho (um `cat`, um grep). Na
|
|
1101
|
+
segunda vez vai o conteúdo guardado — ler de novo sem nada
|
|
1102
|
+
mudar dá o mesmo texto, então não há o que executar. Da
|
|
1103
|
+
terceira em diante é laço, e o freio segue como era. */
|
|
1104
|
+
const guardado = eco.vezes === 2 && leiturasGuardadas.get(eco.chave);
|
|
1105
|
+
messages.push({ role: 'tool', tool_call_id: tc.id, content: guardado
|
|
1106
|
+
? `(sem mudança desde a última leitura — é o mesmo conteúdo; não precisa ler de novo)\n${guardado}`
|
|
1107
|
+
: eco.aviso });
|
|
1068
1108
|
continue;
|
|
1069
1109
|
}
|
|
1070
1110
|
|
|
@@ -1125,6 +1165,9 @@ async function runAct(userPrompt, opts) {
|
|
|
1125
1165
|
const result = await tools.execute(tc.name, tc.arguments, { projectDir: pastaAtual });
|
|
1126
1166
|
logResult(tc.name, result, profundidade);
|
|
1127
1167
|
repeticao.executou(jaChamado, tc.name, tc.arguments);
|
|
1168
|
+
if (RELEITURA.has(tc.name) && result && result.ok !== false) {
|
|
1169
|
+
leiturasGuardadas.set(eco.chave, summarizeForModel(result));
|
|
1170
|
+
}
|
|
1128
1171
|
if (tc.name === 'write_todos' && result && Array.isArray(result.todos)) todosDaTarefa = result.todos;
|
|
1129
1172
|
// Além de narrar, ANOTA: no fim do pedido sai o balanço do que
|
|
1130
1173
|
// nasceu, mudou, rodou e falhou. Narrar sozinho não basta — numa
|
|
@@ -1133,6 +1176,19 @@ async function runAct(userPrompt, opts) {
|
|
|
1133
1176
|
if (result && result.ok !== false && catalogo.criaProjeto(tc.name, tc.arguments)
|
|
1134
1177
|
&& /\.(html?|css|m?jsx?|tsx?|vue|svelte)$/i.test(String((tc.arguments && tc.arguments.path) || ''))) {
|
|
1135
1178
|
mexeuNaPagina = true;
|
|
1179
|
+
/* O NAVEGADOR ESCONDIDO SOBE AGORA, e não no `finish`.
|
|
1180
|
+
A conferência só roda quando o agente diz "pronto", e até
|
|
1181
|
+
lá ainda há pelo menos uma rodada do modelo. Subir o Chrome
|
|
1182
|
+
ali, em paralelo com ela, tira do "pronto" o tempo de ligar
|
|
1183
|
+
o navegador e de fazer a PRIMEIRA navegação dele, que é a
|
|
1184
|
+
parte cara (ver `aquecerRevisor` em lib/browser.js). Medido
|
|
1185
|
+
em 24/09: a conferência num Chrome recém-aberto levava de
|
|
1186
|
+
2,1 a 4,6s; aquecido antes, 1,24s — que é o assentar de
|
|
1187
|
+
1,2s, e nada mais. Sem `await` e sem erro: se não subir, a
|
|
1188
|
+
conferência tenta de novo e diz o que houve. */
|
|
1189
|
+
if (aquecerPagina && !subagent && conferencias < 2) {
|
|
1190
|
+
try { Promise.resolve(aquecerPagina()).catch(() => {}); } catch { /* segue */ }
|
|
1191
|
+
}
|
|
1136
1192
|
}
|
|
1137
1193
|
|
|
1138
1194
|
// Uma escrita bem-sucedida limpa a dívida — mas só a do MESMO
|
|
@@ -1174,6 +1230,9 @@ async function runAct(userPrompt, opts) {
|
|
|
1174
1230
|
messages.push({ role: 'tool', tool_call_id: tc.id, content: summarizeForModel(result) });
|
|
1175
1231
|
}
|
|
1176
1232
|
|
|
1233
|
+
// A conferência da página tem linha própria: aqui fica só o resto.
|
|
1234
|
+
tempos.somar('ferramentas', Date.now() - tTools - (msConferindo - conferindoAntes));
|
|
1235
|
+
|
|
1177
1236
|
// Uma volta inteira sem executar nada é o modelo preso. Insistir custa
|
|
1178
1237
|
// um pedido ao servidor por volta, e as 15 voltas saíam caras para
|
|
1179
1238
|
// terminar no mesmo lugar — melhor parar e devolver a vez ao usuário.
|
package/lib/browser.js
CHANGED
|
@@ -821,6 +821,7 @@ function close() {
|
|
|
821
821
|
let PORTA_REVISOR = Number(process.env.PRIMOCODE_CDP_PORT_REVISOR || 0);
|
|
822
822
|
let perfilRevisor = null;
|
|
823
823
|
let revisorChild = null;
|
|
824
|
+
let saidaRegistrada = false;
|
|
824
825
|
|
|
825
826
|
function lerPortaDoPerfil(perfil) {
|
|
826
827
|
try {
|
|
@@ -830,7 +831,20 @@ function lerPortaDoPerfil(perfil) {
|
|
|
830
831
|
} catch { return 0; }
|
|
831
832
|
}
|
|
832
833
|
|
|
833
|
-
|
|
834
|
+
/* Quem está subindo o revisor AGORA. O agente liga o navegador escondido
|
|
835
|
+
adiantado, quando escreve a primeira página (lib/act.js), e a conferência
|
|
836
|
+
do "pronto" chama isto de novo — às vezes com o primeiro ainda subindo.
|
|
837
|
+
Sem esta promessa em comum, a segunda chamada via PORTA_REVISOR zerada e
|
|
838
|
+
lançava um SEGUNDO Chrome, que ficava órfão. */
|
|
839
|
+
let subindoRevisor = null;
|
|
840
|
+
|
|
841
|
+
function subirRevisor() {
|
|
842
|
+
if (subindoRevisor) return subindoRevisor;
|
|
843
|
+
subindoRevisor = _subirRevisor().finally(() => { subindoRevisor = null; });
|
|
844
|
+
return subindoRevisor;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
async function _subirRevisor() {
|
|
834
848
|
if (PORTA_REVISOR) {
|
|
835
849
|
try {
|
|
836
850
|
const v = await httpGetJson(`http://127.0.0.1:${PORTA_REVISOR}/json/version`, 800);
|
|
@@ -880,6 +894,17 @@ async function subirRevisor() {
|
|
|
880
894
|
revisorChild = spawn(chrome, args, { detached: true, stdio: 'ignore' });
|
|
881
895
|
revisorChild.on('error', () => {});
|
|
882
896
|
revisorChild.unref();
|
|
897
|
+
/* O REVISOR MORRE COM O PROCESSO. Ele é `detached` + `unref` para não
|
|
898
|
+
segurar o CLI de pé — e por isso mesmo sobrevivia a ele: só o pipeline
|
|
899
|
+
(construir_app) chamava fecharRevisor, e toda sessão que conferiu uma
|
|
900
|
+
página deixava um Chrome invisível para trás. Medido em 24/09 nesta
|
|
901
|
+
máquina: 7 revisores órfãos somando ~2 GB de memória, um deles com 3
|
|
902
|
+
minutos de CPU. É o tipo de lentidão que a pessoa sente no computador
|
|
903
|
+
inteiro e não liga ao PrimoCode. */
|
|
904
|
+
if (!saidaRegistrada) {
|
|
905
|
+
saidaRegistrada = true;
|
|
906
|
+
process.on('exit', () => { try { fecharRevisor(); } catch { /* saindo */ } });
|
|
907
|
+
}
|
|
883
908
|
|
|
884
909
|
const prazo = Date.now() + 15000;
|
|
885
910
|
while (Date.now() < prazo) {
|
|
@@ -1110,6 +1135,44 @@ async function abrirEscondido(url) {
|
|
|
1110
1135
|
/* O revisor é ferramenta de uma tarefa, não um processo que fica. Deixá-lo de
|
|
1111
1136
|
pé come memória até a pessoa desligar o computador, e ele não é o navegador
|
|
1112
1137
|
dela — ninguém sentiria falta nem saberia que existe para fechar. */
|
|
1138
|
+
/* LIGAR O REVISOR ANTES DA HORA — e não só ligar: navegar uma vez.
|
|
1139
|
+
*
|
|
1140
|
+
* Medido em 24/09, cada fase da conferência num Chrome recém-aberto:
|
|
1141
|
+
* subir 313ms · abrir aba 24ms · canal 29ms · PRIMEIRA navegação 3.019ms ·
|
|
1142
|
+
* assentar 1.200ms. Na segunda página, no mesmo Chrome, a navegação levou
|
|
1143
|
+
* 4ms. O caro não é o processo, é a primeira ida à rede (o Chrome sobe o
|
|
1144
|
+
* serviço de rede e o primeiro renderizador nela) — navegar para about:blank
|
|
1145
|
+
* ou data: não aquece isso (a conferência seguinte continuou em ~2,1s);
|
|
1146
|
+
* navegar para um http local aquece (caiu para 1.240ms, que é o assentar).
|
|
1147
|
+
*
|
|
1148
|
+
* Por isso isto abre uma aba num servidor de uma linha, em 127.0.0.1, e fecha.
|
|
1149
|
+
* Uma vez por navegador: depois da primeira, não há o que aquecer. Nunca
|
|
1150
|
+
* rejeita — aquecer é atalho, e quem confere de verdade tenta de novo e diz
|
|
1151
|
+
* o que houve. */
|
|
1152
|
+
let aquecido = null; // a porta do revisor que já foi aquecido
|
|
1153
|
+
let aquecendo = null;
|
|
1154
|
+
function aquecerRevisor() {
|
|
1155
|
+
if (aquecendo) return aquecendo;
|
|
1156
|
+
aquecendo = (async () => {
|
|
1157
|
+
await subirRevisor();
|
|
1158
|
+
if (aquecido && aquecido === PORTA_REVISOR) return true;
|
|
1159
|
+
const srv = http.createServer((req, res) => {
|
|
1160
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
1161
|
+
res.end('<!doctype html><title>.</title>');
|
|
1162
|
+
});
|
|
1163
|
+
try {
|
|
1164
|
+
await new Promise((ok, falhou) => { srv.once('error', falhou); srv.listen(0, '127.0.0.1', ok); });
|
|
1165
|
+
const aba = await abaEscondida(`http://127.0.0.1:${srv.address().port}/`, { assentar: 0, prazoLoad: 10000 });
|
|
1166
|
+
await aba.fechar();
|
|
1167
|
+
aquecido = PORTA_REVISOR;
|
|
1168
|
+
return true;
|
|
1169
|
+
} finally {
|
|
1170
|
+
try { srv.close(); } catch { /* já fechou */ }
|
|
1171
|
+
}
|
|
1172
|
+
})().catch(() => false).finally(() => { aquecendo = null; });
|
|
1173
|
+
return aquecendo;
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1113
1176
|
function fecharRevisor() {
|
|
1114
1177
|
if (revisorChild && revisorChild.pid) {
|
|
1115
1178
|
try { process.kill(-revisorChild.pid); } catch {
|
|
@@ -1132,5 +1195,5 @@ module.exports = {
|
|
|
1132
1195
|
scriptDaSeta, verSeta, findChrome,
|
|
1133
1196
|
open, click, clickText, clickSelector, type, typeInSelector,
|
|
1134
1197
|
key, screenshot, evaluate, waitFor, wait, scroll, close,
|
|
1135
|
-
inspecionarEscondido, rodarEscondido, abrirEscondido, fecharRevisor,
|
|
1198
|
+
inspecionarEscondido, rodarEscondido, abrirEscondido, fecharRevisor, subirRevisor, aquecerRevisor,
|
|
1136
1199
|
};
|
|
@@ -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/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 };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "primocode",
|
|
3
|
-
"version": "9.8.0-beta.
|
|
3
|
+
"version": "9.8.0-beta.7",
|
|
4
4
|
"description": "PrimoCode — agente de engenharia com IA e cursor próprio. Requer conta Conecta Primo AI (Premium ou Super). Cria arquivos, roda comandos, controla navegador e desktop: abre apps, clica em botões e ícones pelo nome, digita e usa atalhos.",
|
|
5
5
|
"main": "bin/primocode.js",
|
|
6
6
|
"bin": {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
10
|
"start": "node bin/primocode.js",
|
|
11
|
-
"test": "node test/tools.test.js && node test/ui.test.js && node test/entrada.test.js && node test/transparencia.test.js && node test/permissao.test.js && node test/compatibilidade.test.js && node test/skills.test.js && node test/skills-de-fora.test.js && node test/skills-novas.test.js && node test/comandos.test.js && node test/estudio.test.js && node test/voz.test.js && node test/voz-conversa.test.js && node test/modo.test.js && node test/pasta.test.js && node test/projeto.test.js && node test/nome-projeto.test.js && node test/kits.test.js && node test/api-propria.test.js && node test/claude-engine.test.js && node test/codex.test.js && node test/conferir.test.js && node test/catalogo.test.js && node test/desktop.test.js && node test/seta.test.js && node test/effort.test.js && node test/regressao.test.js && node test/memoria-conversa.test.js && node test/repeticao.test.js && node test/autonomia.test.js && node test/desfazer.test.js && node test/desfazer-git.test.js && node test/referencia.test.js && node test/parar.test.js && node test/continuar.test.js && node test/pipeline.test.js && node test/app.test.js && node test/ide.test.js && node test/aplicativo.test.js && node test/conta.test.mjs && node test/nuvem.test.js && node test/prazo.test.js && node test/primeira-vez.test.js && node test/janela.test.js && node test/fala.test.js && node test/especialistas.test.js && node test/empurrao.test.js && node test/terminal-novo.test.js && node test/markdown.test.js && node test/coerencia.test.js && node test/contexto.test.js && node test/perigo.test.js && node test/mascotes.test.js && node test/comandos-extra.test.js"
|
|
11
|
+
"test": "node test/tools.test.js && node test/ui.test.js && node test/entrada.test.js && node test/transparencia.test.js && node test/permissao.test.js && node test/compatibilidade.test.js && node test/skills.test.js && node test/skills-de-fora.test.js && node test/skills-novas.test.js && node test/comandos.test.js && node test/estudio.test.js && node test/voz.test.js && node test/voz-conversa.test.js && node test/modo.test.js && node test/pasta.test.js && node test/projeto.test.js && node test/nome-projeto.test.js && node test/kits.test.js && node test/api-propria.test.js && node test/claude-engine.test.js && node test/codex.test.js && node test/conferir.test.js && node test/catalogo.test.js && node test/desktop.test.js && node test/seta.test.js && node test/effort.test.js && node test/regressao.test.js && node test/memoria-conversa.test.js && node test/repeticao.test.js && node test/autonomia.test.js && node test/desfazer.test.js && node test/desfazer-git.test.js && node test/referencia.test.js && node test/parar.test.js && node test/continuar.test.js && node test/pipeline.test.js && node test/app.test.js && node test/ide.test.js && node test/aplicativo.test.js && node test/conta.test.mjs && node test/nuvem.test.js && node test/prazo.test.js && node test/primeira-vez.test.js && node test/janela.test.js && node test/fala.test.js && node test/especialistas.test.js && node test/empurrao.test.js && node test/terminal-novo.test.js && node test/markdown.test.js && node test/coerencia.test.js && node test/contexto.test.js && node test/perigo.test.js && node test/rapidez.test.js && node test/mascotes.test.js && node test/comandos-extra.test.js"
|
|
12
12
|
},
|
|
13
13
|
"engines": {
|
|
14
14
|
"node": ">=18.17.0"
|