primocode 9.8.0-beta.6 → 9.8.0-beta.8
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 +5 -5
- 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 +77 -26
- package/lib/act.js +71 -7
- package/lib/app.js +297 -89
- package/lib/browser.js +65 -2
- package/lib/fala.js +95 -1
- package/lib/{ide.js → painel.js} +41 -87
- 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/app/ide.css +0 -345
- package/app/ide.html +0 -183
- package/app/ide.js +0 -639
package/lib/fala.js
CHANGED
|
@@ -37,9 +37,95 @@
|
|
|
37
37
|
|
|
38
38
|
'use strict';
|
|
39
39
|
|
|
40
|
+
const { EventEmitter } = require('events');
|
|
40
41
|
const { c, actionHeader, friendlyError } = require('./ui');
|
|
41
42
|
const markdown = require('./markdown.js');
|
|
42
43
|
|
|
44
|
+
/* ── O CANAL ESTRUTURADO ─────────────────────────────────────────────────
|
|
45
|
+
* A janela do app (lib/app.js) mostra a conversa como CHAT: a fala do Primo
|
|
46
|
+
* em texto e cada ação dele numa linha cinza ("Leu index.html", "Rodou npm
|
|
47
|
+
* test ✓"). Adivinhar isso relendo a saída colorida do terminal seria frágil
|
|
48
|
+
* — um rótulo que muda aqui quebraria a janela calado. Então a MESMA função
|
|
49
|
+
* que desenha no terminal também avisa, por este canal, o que desenhou.
|
|
50
|
+
*
|
|
51
|
+
* Sem ouvinte (terminal puro), `avisar` não faz nada: nenhum custo para quem
|
|
52
|
+
* não abriu a janela. */
|
|
53
|
+
const eventos = new EventEmitter();
|
|
54
|
+
eventos.setMaxListeners(20);
|
|
55
|
+
function avisar(tipo, dados) {
|
|
56
|
+
if (!eventos.listenerCount('evento')) return;
|
|
57
|
+
try { eventos.emit('evento', Object.assign({ tipo }, dados || {})); } catch { /* a janela nunca derruba o agente */ }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/* O que a linha de ação da janela diz, por ferramenta. Verbo no passado
|
|
61
|
+
porque a linha fica na conversa depois que a ação acabou — "Lendo" viraria
|
|
62
|
+
mentira um segundo depois. O ícone é uma família, não um desenho por tool:
|
|
63
|
+
a página tem dez ícones, não setenta. */
|
|
64
|
+
const VERBOS = {
|
|
65
|
+
read_file: ['Leu', 'ler'], write_file: ['Escreveu', 'escrever'], edit_file: ['Editou', 'editar'],
|
|
66
|
+
multi_edit: ['Editou', 'editar'], list_dir: ['Listou', 'buscar'], glob: ['Procurou', 'buscar'],
|
|
67
|
+
grep: ['Buscou', 'buscar'], run_shell: ['Rodou', 'rodar'], run_in_new_terminal: ['Abriu um terminal com', 'rodar'],
|
|
68
|
+
open_in_browser: ['Abriu', 'web'], spawn_agent: ['Chamou um agente', 'agente'], write_todos: ['Planejou', 'plano'],
|
|
69
|
+
finish: ['Concluiu', 'plano'], pesquisar_referencia: ['Pesquisou', 'web'], remember_general: ['Guardou na memória', 'plano'],
|
|
70
|
+
planejar_producao: ['Planejou a produção', 'agente'], construir_app: ['Construiu', 'agente'],
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/** Verbo, alvo e ícone de uma ação — só o que cabe numa linha, nunca o
|
|
74
|
+
* conteúdo inteiro de um write_file. */
|
|
75
|
+
function resumoDaAcao(name, args) {
|
|
76
|
+
const a = args || {};
|
|
77
|
+
let [verbo, icone] = VERBOS[name] || [];
|
|
78
|
+
if (!verbo) {
|
|
79
|
+
if (/^browser_/.test(name)) { verbo = 'Navegador · ' + name.slice(8); icone = 'web'; }
|
|
80
|
+
else if (/^desktop_/.test(name)) { verbo = 'Computador · ' + name.slice(8).replace(/_/g, ' '); icone = 'computador'; }
|
|
81
|
+
else if (/^estudio_/.test(name)) { verbo = 'Estúdio · ' + name.slice(8); icone = 'agente'; }
|
|
82
|
+
else { verbo = name; icone = 'outro'; }
|
|
83
|
+
}
|
|
84
|
+
let alvo = a.path || a.command || a.pattern || a.consulta || a.url || a.target || a.app || a.selector
|
|
85
|
+
|| a.titulo || a.description || a.arquivo || (a.todos ? `${a.todos.length} tarefas` : '') || '';
|
|
86
|
+
if (typeof alvo !== 'string') alvo = '';
|
|
87
|
+
alvo = alvo.replace(/\s+/g, ' ').trim();
|
|
88
|
+
if (alvo.length > 120) alvo = alvo.slice(0, 119) + '…';
|
|
89
|
+
return { nome: name, verbo, alvo, icone };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** O resultado de uma ação, resumido para a linha da janela: ok ou não, o
|
|
93
|
+
* "+12 −3" das edições e até 40 linhas de detalhe para quem expandir. */
|
|
94
|
+
function resumoDoResultado(name, r) {
|
|
95
|
+
if (!r || typeof r !== 'object') return { nome: name, ok: true };
|
|
96
|
+
if (r.ok === false) {
|
|
97
|
+
return { nome: name, ok: false, resumo: String(r.error || `exit ${r.code ?? 'erro'}`).replace(/\s+/g, ' ').trim().slice(0, 300) };
|
|
98
|
+
}
|
|
99
|
+
const fora = { nome: name, ok: true };
|
|
100
|
+
if (typeof r._antes === 'string' && typeof r._depois === 'string') {
|
|
101
|
+
try {
|
|
102
|
+
const diff = require('./diff.js');
|
|
103
|
+
const n = r._antes.split('\n').length + r._depois.split('\n').length;
|
|
104
|
+
if (n <= diff.MAX_LINHAS_ARQUIVO * 2) {
|
|
105
|
+
const ops = diff.operacoes(r._antes, r._depois);
|
|
106
|
+
Object.assign(fora, diff.contar(ops));
|
|
107
|
+
fora.detalhe = ops.filter((o) => o.tipo !== ' ').slice(0, 40).map((o) => o.tipo + ' ' + o.texto).join('\n');
|
|
108
|
+
}
|
|
109
|
+
} catch { /* sem diff: a linha sai sem os números */ }
|
|
110
|
+
}
|
|
111
|
+
if (name === 'write_file') fora.criado = !!r.created;
|
|
112
|
+
if (name === 'run_shell' || name === 'run_in_new_terminal') {
|
|
113
|
+
const saida = String(r.stdout || '').trim() + (r.stderr ? '\n' + String(r.stderr).trim() : '');
|
|
114
|
+
fora.detalhe = saida.split('\n').slice(-40).join('\n').trim();
|
|
115
|
+
fora.codigo = r.code ?? 0;
|
|
116
|
+
}
|
|
117
|
+
const plural = (n, um, varios) => `${n} ${n === 1 ? um : varios}`;
|
|
118
|
+
if (name === 'read_file') fora.resumo = plural(r.linhas || 0, 'linha', 'linhas');
|
|
119
|
+
if (name === 'grep') fora.resumo = plural(r.total || 0, 'ocorrência', 'ocorrências');
|
|
120
|
+
if (name === 'glob') fora.resumo = plural(r.total || 0, 'arquivo', 'arquivos');
|
|
121
|
+
if (name === 'list_dir' && r.tree) fora.detalhe = String(r.tree).split('\n').slice(0, 40).join('\n');
|
|
122
|
+
if (name === 'write_todos' && Array.isArray(r.todos)) {
|
|
123
|
+
fora.detalhe = r.todos.map((t) => (t.status === 'completed' ? '✓ ' : '○ ') + t.content).join('\n');
|
|
124
|
+
fora.resumo = `${r.feitos}/${r.total}`;
|
|
125
|
+
}
|
|
126
|
+
return fora;
|
|
127
|
+
}
|
|
128
|
+
|
|
43
129
|
/* Até este tamanho, o texto que o modelo escreve antes de chamar uma tool é
|
|
44
130
|
guardado e sai de uma vez, como pensamento. Passou disso sem tool à vista,
|
|
45
131
|
é resposta — e volta a correr na tela conforme chega. Uma frase de
|
|
@@ -58,6 +144,7 @@ function pensamento(texto, profundidade) {
|
|
|
58
144
|
if (!ls.length || !ls[0]) return '';
|
|
59
145
|
const p = ident(profundidade);
|
|
60
146
|
const saida = ls.map((l, i) => `${p} ${i === 0 ? c.dim('✦') : ' '} ${c.muted(l)}`).join('\n');
|
|
147
|
+
avisar('pensamento', { texto: ls.join('\n'), prof: profundidade || 0 });
|
|
61
148
|
console.log(saida);
|
|
62
149
|
return saida;
|
|
63
150
|
}
|
|
@@ -65,6 +152,7 @@ function pensamento(texto, profundidade) {
|
|
|
65
152
|
/** A ação (tool) que ele decidiu fazer. */
|
|
66
153
|
function acao(name, args, profundidade) {
|
|
67
154
|
const saida = ident(profundidade) + actionHeader(name, args);
|
|
155
|
+
avisar('acao', Object.assign(resumoDaAcao(name, args), { prof: profundidade || 0 }));
|
|
68
156
|
console.log(saida);
|
|
69
157
|
return saida;
|
|
70
158
|
}
|
|
@@ -102,6 +190,7 @@ function resposta(texto) {
|
|
|
102
190
|
const largura = Math.max(40, (process.stdout.columns || 80) - 4);
|
|
103
191
|
const linhasMd = markdown.renderizar(texto, largura);
|
|
104
192
|
const saida = '\n' + linhasMd.map((l, i) => (i === 0 ? `${c.brand('●')} ${l}` : ` ${l}`)).join('\n');
|
|
193
|
+
avisar('resposta', { texto: String(texto) });
|
|
105
194
|
console.log(saida);
|
|
106
195
|
return saida;
|
|
107
196
|
}
|
|
@@ -111,6 +200,7 @@ function resposta(texto) {
|
|
|
111
200
|
function aviso(texto) {
|
|
112
201
|
if (!String(texto || '').trim()) return '';
|
|
113
202
|
const saida = '\n' + formatar(texto, c.warn('●'));
|
|
203
|
+
avisar('aviso', { texto: String(texto) });
|
|
114
204
|
console.log(saida);
|
|
115
205
|
return saida;
|
|
116
206
|
}
|
|
@@ -154,9 +244,11 @@ function fluxo() {
|
|
|
154
244
|
escrever(delta) {
|
|
155
245
|
const t = String(delta || '');
|
|
156
246
|
if (!t) return;
|
|
247
|
+
avisar('pedaco', { texto: t });
|
|
157
248
|
sair(desenho.pedaco(t));
|
|
158
249
|
},
|
|
159
250
|
fechar() {
|
|
251
|
+
avisar('fim', {});
|
|
160
252
|
sair(desenho.fim());
|
|
161
253
|
if (escreveu) process.stdout.write('\n');
|
|
162
254
|
},
|
|
@@ -174,6 +266,7 @@ function paralelo(quantos, profundidade) {
|
|
|
174
266
|
/** Um agente delegado começando: "◆ Agente·frontend a folha de estilo". */
|
|
175
267
|
function agente(rotulo, descricao, profundidade) {
|
|
176
268
|
const saida = `${ident(profundidade)} ${c.brand('◆')} ${c.bold(rotulo || 'Agente')}${descricao ? ' ' + c.muted(descricao) : ''}`;
|
|
269
|
+
avisar('acao', { nome: 'agente', verbo: rotulo || 'Agente', alvo: String(descricao || '').slice(0, 120), icone: 'agente', prof: profundidade || 0 });
|
|
177
270
|
console.log(saida);
|
|
178
271
|
return saida;
|
|
179
272
|
}
|
|
@@ -182,8 +275,9 @@ function agente(rotulo, descricao, profundidade) {
|
|
|
182
275
|
function agenteVoltou(resumo, profundidade) {
|
|
183
276
|
const texto = String(resumo || 'sem resultado').replace(/\s+/g, ' ').trim();
|
|
184
277
|
const saida = resultado(profundidade) + c.muted(texto.length > 140 ? texto.slice(0, 139) + '…' : texto);
|
|
278
|
+
avisar('resultado', { nome: 'agente', ok: true, detalhe: texto.slice(0, 2000) });
|
|
185
279
|
console.log(saida);
|
|
186
280
|
return saida;
|
|
187
281
|
}
|
|
188
282
|
|
|
189
|
-
module.exports = { LIMIAR_PENSAMENTO, pensamento, acao, resultado, recuo, resposta, aviso, problema, frase, fluxo, paralelo, agente, agenteVoltou };
|
|
283
|
+
module.exports = { eventos, avisar, resumoDaAcao, resumoDoResultado, LIMIAR_PENSAMENTO, pensamento, acao, resultado, recuo, resposta, aviso, problema, frase, fluxo, paralelo, agente, agenteVoltou };
|
package/lib/{ide.js → painel.js}
RENAMED
|
@@ -1,27 +1,30 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* painel.js — o painel da direita da janela: a prévia do site e os arquivos.
|
|
3
3
|
*
|
|
4
|
-
* "
|
|
4
|
+
* "Remove a IDE ou deixa idêntico ao Codex."
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
6
|
+
* Antes isto era a PrimoCode IDE: árvore, EDITOR de código, prévia e chat. O
|
|
7
|
+
* editor saiu. Quem usa o PrimoCode não programa — um editor de código na
|
|
8
|
+
* cara dessa pessoa é uma porta para quebrar o que o agente fez, sem saber
|
|
9
|
+
* como voltar. O que ficou é o que ela usa de verdade: ver o site que está
|
|
10
|
+
* nascendo (Prévia) e olhar o que tem na pasta (Arquivos), só leitura.
|
|
11
|
+
*
|
|
12
|
+
* ── POR QUE MORA DENTRO DO SERVIDOR DA JANELA ────────────────────────────
|
|
13
|
+
* A janela (lib/app.js) já resolveu o difícil: 127.0.0.1, chave sorteada e o
|
|
14
|
+
* fluxo de eventos (SSE). Aqui só entram as rotas do painel (árvore, ler,
|
|
15
|
+
* prévia) e o vigia da pasta, que empurra "arquivo mudou" pelo MESMO fluxo —
|
|
16
|
+
* é o que faz a prévia recarregar enquanto o agente escreve.
|
|
14
17
|
*
|
|
15
18
|
* ── SEGURANÇA ────────────────────────────────────────────────────────────
|
|
16
|
-
* Estas rotas leem e ESCREVEM no disco do usuário. Três travas:
|
|
17
19
|
* · a chave do app.js vale aqui também (tudo mora em /api/);
|
|
18
20
|
* · todo caminho é resolvido DENTRO da pasta do projeto — "../", caminho
|
|
19
21
|
* absoluto e link simbólico que aponta para fora são recusados;
|
|
22
|
+
* · nada aqui escreve no disco: sem editor, não há rota de salvar;
|
|
20
23
|
* · a prévia roda num iframe sandbox SEM allow-same-origin, e o servidor
|
|
21
24
|
* manda o mesmo sandbox no cabeçalho. O código da prévia foi escrito pelo
|
|
22
|
-
* agente
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
+
* agente; se ele herdasse a origem da janela, poderia chamar /api/enviar
|
|
26
|
+
* e dar ordens ao agente. Com origem opaca, o fetch dele sai com
|
|
27
|
+
* `Origin: null` e o app.js recusa.
|
|
25
28
|
*/
|
|
26
29
|
|
|
27
30
|
'use strict';
|
|
@@ -36,9 +39,9 @@ const IGNORADOS = new Set(['node_modules', '.git', '.DS_Store', '.next', '.cache
|
|
|
36
39
|
'__pycache__', '.venv', 'venv', '.turbo', '.parcel-cache']);
|
|
37
40
|
const MAX_ITENS = 4000;
|
|
38
41
|
const MAX_PROFUNDIDADE = 12;
|
|
39
|
-
// 1 MB: acima disso
|
|
40
|
-
//
|
|
41
|
-
const
|
|
42
|
+
// 1 MB: acima disso o visualizador engasga, e arquivo desse tamanho num
|
|
43
|
+
// projeto de iniciante é quase sempre gerado (bundle, dado).
|
|
44
|
+
const MAX_LEITURA = 1024 * 1024;
|
|
42
45
|
|
|
43
46
|
// ── caminhos ─────────────────────────────────────────────────────────────
|
|
44
47
|
|
|
@@ -46,12 +49,12 @@ const MAX_EDITOR = 1024 * 1024;
|
|
|
46
49
|
*
|
|
47
50
|
* `path.relative` pega o "../" e o caminho absoluto; o `realpath` pega o link
|
|
48
51
|
* simbólico, que passa pelo primeiro teste com cara de arquivo de dentro e
|
|
49
|
-
* aponta para ~/.ssh.
|
|
52
|
+
* aponta para ~/.ssh. Caminho que ainda não existe é checado
|
|
50
53
|
* pela pasta onde vai nascer. */
|
|
51
54
|
function dentro(raiz, relativo) {
|
|
52
55
|
if (typeof relativo !== 'string' || relativo.includes('\0')) return null;
|
|
53
56
|
// Absoluto é recusado de cara, e não "reinterpretado" como relativo:
|
|
54
|
-
// quem manda /etc/hosts quer o /etc/hosts, e
|
|
57
|
+
// quem manda /etc/hosts quer o /etc/hosts, e ler <projeto>/etc/hosts
|
|
55
58
|
// no lugar seria uma surpresa. `C:\` e `\\servidor` também contam.
|
|
56
59
|
if (path.isAbsolute(relativo) || path.win32.isAbsolute(relativo)) return null;
|
|
57
60
|
const base = path.resolve(raiz);
|
|
@@ -113,11 +116,10 @@ function arvore(raiz) {
|
|
|
113
116
|
return { itens, cortada };
|
|
114
117
|
}
|
|
115
118
|
|
|
116
|
-
// ── ler
|
|
119
|
+
// ── ler ──────────────────────────────────────────────────────────────────
|
|
117
120
|
|
|
118
121
|
/* Binário é o que tem byte zero nos primeiros 8 KB — o mesmo teste do git.
|
|
119
|
-
Mostrar um PNG como texto
|
|
120
|
-
destruiria a imagem sem aviso. */
|
|
122
|
+
Mostrar um PNG como texto é uma tela de lixo que parece arquivo quebrado. */
|
|
121
123
|
function ehBinario(buf) {
|
|
122
124
|
const n = Math.min(buf.length, 8000);
|
|
123
125
|
for (let i = 0; i < n; i++) if (buf[i] === 0) return true;
|
|
@@ -131,35 +133,12 @@ function ler(raiz, rel) {
|
|
|
131
133
|
try { st = fs.statSync(abs); } catch { return { status: 404, corpo: { ok: false, error: 'não existe' } }; }
|
|
132
134
|
if (!st.isFile()) return { status: 400, corpo: { ok: false, error: 'não é arquivo' } };
|
|
133
135
|
const base = { ok: true, caminho: relativoDe(raiz, abs), tamanho: st.size, mtime: st.mtimeMs };
|
|
134
|
-
if (st.size >
|
|
136
|
+
if (st.size > MAX_LEITURA) return { status: 200, corpo: Object.assign(base, { grande: true }) };
|
|
135
137
|
const buf = fs.readFileSync(abs);
|
|
136
138
|
if (ehBinario(buf)) return { status: 200, corpo: Object.assign(base, { binario: true }) };
|
|
137
139
|
return { status: 200, corpo: Object.assign(base, { conteudo: buf.toString('utf8') }) };
|
|
138
140
|
}
|
|
139
141
|
|
|
140
|
-
/* `mtimeBase` é o mtime que a página viu ao abrir o arquivo. Se o disco está
|
|
141
|
-
mais novo, alguém (quase sempre o agente) mexeu no meio — e salvar por cima
|
|
142
|
-
apagaria o trabalho dele sem ninguém perceber. A página decide: recarregar
|
|
143
|
-
ou mandar de novo com `forcar`. */
|
|
144
|
-
function salvar(raiz, dados) {
|
|
145
|
-
const rel = dados && dados.caminho;
|
|
146
|
-
const conteudo = dados && dados.conteudo;
|
|
147
|
-
if (typeof conteudo !== 'string') return { status: 400, corpo: { ok: false, error: 'conteúdo faltando' } };
|
|
148
|
-
if (Buffer.byteLength(conteudo) > MAX_EDITOR) return { status: 413, corpo: { ok: false, error: 'maior que 1 MB' } };
|
|
149
|
-
const abs = dentro(raiz, rel);
|
|
150
|
-
if (!abs || abs === path.resolve(raiz)) return { status: 403, corpo: { ok: false, error: 'fora da pasta do projeto' } };
|
|
151
|
-
if (ignorado(relativoDe(raiz, abs))) return { status: 403, corpo: { ok: false, error: 'pasta protegida' } };
|
|
152
|
-
let st = null;
|
|
153
|
-
try { st = fs.statSync(abs); } catch { /* arquivo novo */ }
|
|
154
|
-
if (st && !st.isFile()) return { status: 400, corpo: { ok: false, error: 'não é arquivo' } };
|
|
155
|
-
if (st && dados.mtimeBase && !dados.forcar && st.mtimeMs > Number(dados.mtimeBase) + 1) {
|
|
156
|
-
return { status: 409, corpo: { ok: false, conflito: true, error: 'mudou no disco depois que você abriu', mtime: st.mtimeMs } };
|
|
157
|
-
}
|
|
158
|
-
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
159
|
-
fs.writeFileSync(abs, conteudo);
|
|
160
|
-
return { status: 200, corpo: { ok: true, caminho: relativoDe(raiz, abs), mtime: fs.statSync(abs).mtimeMs } };
|
|
161
|
-
}
|
|
162
|
-
|
|
163
142
|
// ── prévia ───────────────────────────────────────────────────────────────
|
|
164
143
|
|
|
165
144
|
const TIPOS = {
|
|
@@ -193,7 +172,7 @@ function servirPrevia(raiz, rel, res) {
|
|
|
193
172
|
'Content-Type': TIPOS[path.extname(alvo).toLowerCase()] || 'application/octet-stream',
|
|
194
173
|
'Cache-Control': 'no-store',
|
|
195
174
|
// O mesmo sandbox do iframe, dito pelo servidor: vale até se alguém
|
|
196
|
-
// abrir o endereço da prévia fora da
|
|
175
|
+
// abrir o endereço da prévia fora da janela.
|
|
197
176
|
'Content-Security-Policy': 'sandbox allow-scripts allow-forms allow-modals allow-popups allow-pointer-lock',
|
|
198
177
|
});
|
|
199
178
|
fs.createReadStream(alvo).pipe(res);
|
|
@@ -237,7 +216,7 @@ function vigiar(raiz, aoMudar, espera = 150) {
|
|
|
237
216
|
// ── as rotas ─────────────────────────────────────────────────────────────
|
|
238
217
|
|
|
239
218
|
/**
|
|
240
|
-
* Monta
|
|
219
|
+
* Monta o painel num servidor do app.js.
|
|
241
220
|
*
|
|
242
221
|
* @param {object} op
|
|
243
222
|
* @param {()=>string} op.pasta a pasta do projeto AGORA (o /dir muda ela)
|
|
@@ -247,9 +226,6 @@ function vigiar(raiz, aoMudar, espera = 150) {
|
|
|
247
226
|
function montar(op) {
|
|
248
227
|
let pastaVigiada = null;
|
|
249
228
|
let pararVigia = () => {};
|
|
250
|
-
// O que a própria IDE acabou de salvar, para a página não pintar como
|
|
251
|
-
// "o agente mexeu" a mudança que a pessoa fez.
|
|
252
|
-
const salvosAgora = new Map();
|
|
253
229
|
|
|
254
230
|
function conferirPasta() {
|
|
255
231
|
const atual = op.pasta && op.pasta();
|
|
@@ -257,19 +233,14 @@ function montar(op) {
|
|
|
257
233
|
pararVigia();
|
|
258
234
|
pastaVigiada = atual;
|
|
259
235
|
if (!fs.existsSync(atual)) { pararVigia = () => {}; return atual; }
|
|
260
|
-
pararVigia = vigiar(atual, (mudados) => {
|
|
261
|
-
const agora = Date.now();
|
|
262
|
-
const deVoce = mudados.filter((m) => (salvosAgora.get(m) || 0) > agora - 1500);
|
|
263
|
-
op.emitir('arquivos', { pasta: atual, mudados, deVoce });
|
|
264
|
-
});
|
|
236
|
+
pararVigia = vigiar(atual, (mudados) => op.emitir('arquivos', { pasta: atual, mudados }));
|
|
265
237
|
op.emitir('arquivos', { pasta: atual, mudados: [], trocou: true });
|
|
266
238
|
return atual;
|
|
267
239
|
}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
o fs.watch recursivo custa um aviso do sistema por pasta. */
|
|
240
|
+
/* O /dir e os projetos da barra lateral trocam a pasta sem passar por
|
|
241
|
+
aqui: sem esta ronda o painel vigiaria o projeto anterior. E só depois
|
|
242
|
+
que o painel foi aberto de verdade — no Linux o fs.watch recursivo custa
|
|
243
|
+
um aviso do sistema por pasta, e quem só conversa não precisa disso. */
|
|
273
244
|
let usada = false;
|
|
274
245
|
const ronda = setInterval(() => { if (usada) conferirPasta(); }, 2000);
|
|
275
246
|
if (ronda.unref) ronda.unref();
|
|
@@ -279,48 +250,31 @@ function montar(op) {
|
|
|
279
250
|
res.end(JSON.stringify(corpo));
|
|
280
251
|
}
|
|
281
252
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
let corpo = '';
|
|
285
|
-
let grande = false;
|
|
286
|
-
req.on('data', (c) => { corpo += c; if (corpo.length > MAX_EDITOR * 2) { grande = true; req.destroy(); } });
|
|
287
|
-
req.on('end', () => { try { ok(grande ? null : JSON.parse(corpo)); } catch { ok(null); } });
|
|
288
|
-
});
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
/* Devolve true se a rota era da IDE. A chave já foi conferida pelo
|
|
292
|
-
app.js antes de chegar aqui — só as rotas com /api/ passam por ela, e
|
|
293
|
-
por isso a prévia também mora debaixo de /api/. */
|
|
253
|
+
/* Devolve true se a rota era do painel. A chave já foi conferida pelo
|
|
254
|
+
app.js antes de chegar aqui — por isso a prévia também mora em /api/. */
|
|
294
255
|
async function atender(req, res, url) {
|
|
295
256
|
const rota = url.pathname;
|
|
296
|
-
|
|
257
|
+
const ehArquivos = rota.startsWith('/api/arquivos/');
|
|
258
|
+
const m = rota.match(/^\/api\/previa\/[0-9a-f]+\/?(.*)$/);
|
|
259
|
+
if (!ehArquivos && !m) return false;
|
|
297
260
|
usada = true;
|
|
298
261
|
const raiz = conferirPasta();
|
|
299
262
|
if (!raiz || !fs.existsSync(raiz)) { json(res, 404, { ok: false, error: 'sem pasta de projeto' }); return true; }
|
|
300
263
|
|
|
301
|
-
if (rota === '/api/
|
|
264
|
+
if (rota === '/api/arquivos/arvore') {
|
|
302
265
|
const a = arvore(raiz);
|
|
303
266
|
json(res, 200, { ok: true, pasta: raiz, nome: path.basename(raiz), itens: a.itens,
|
|
304
267
|
cortada: a.cortada, entrada: entradaDaPrevia(raiz) });
|
|
305
268
|
return true;
|
|
306
269
|
}
|
|
307
|
-
if (rota === '/api/
|
|
270
|
+
if (rota === '/api/arquivos/ler') {
|
|
308
271
|
const r = ler(raiz, url.searchParams.get('p') || '');
|
|
309
272
|
json(res, r.status, r.corpo);
|
|
310
273
|
return true;
|
|
311
274
|
}
|
|
312
|
-
|
|
313
|
-
const dados = await lerCorpo(req);
|
|
314
|
-
if (!dados) { json(res, 400, { ok: false, error: 'pedido ilegível ou maior que 1 MB' }); return true; }
|
|
315
|
-
const r = salvar(raiz, dados);
|
|
316
|
-
if (r.corpo.ok) salvosAgora.set(r.corpo.caminho, Date.now());
|
|
317
|
-
json(res, r.status, r.corpo);
|
|
318
|
-
return true;
|
|
319
|
-
}
|
|
320
|
-
/* /api/ide/previa/<chave>/<caminho>: a chave vai no CAMINHO, não na
|
|
275
|
+
/* /api/previa/<chave>/<caminho>: a chave vai no CAMINHO, não na
|
|
321
276
|
query, porque a página da prévia pede style.css e script.js por
|
|
322
277
|
endereço relativo — e o relativo herda o caminho, não a query. */
|
|
323
|
-
const m = rota.match(/^\/api\/ide\/previa\/[0-9a-f]+\/?(.*)$/);
|
|
324
278
|
if (m) { servirPrevia(raiz, m[1], res); return true; }
|
|
325
279
|
|
|
326
280
|
json(res, 404, { ok: false, error: 'rota desconhecida' });
|
|
@@ -334,4 +288,4 @@ function montar(op) {
|
|
|
334
288
|
};
|
|
335
289
|
}
|
|
336
290
|
|
|
337
|
-
module.exports = { montar, dentro, arvore, ler,
|
|
291
|
+
module.exports = { montar, dentro, arvore, ler, vigiar, ehBinario, entradaDaPrevia, MAX_LEITURA };
|
|
@@ -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.8",
|
|
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/
|
|
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/app-janela.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"
|