primocode 7.15.0 → 7.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/primocode.js +136 -18
- package/lib/act.js +10 -2
- package/lib/catalogo.js +1 -0
- package/lib/config.js +5 -2
- package/lib/effort.js +51 -0
- package/lib/referencia.js +288 -0
- package/lib/streaming.js +127 -29
- package/lib/tools.js +10 -0
- package/lib/ui.js +6 -6
- package/package.json +2 -2
package/bin/primocode.js
CHANGED
|
@@ -23,6 +23,7 @@ const { runAct, interromper } = require('../lib/act');
|
|
|
23
23
|
const pasta = require('../lib/pasta');
|
|
24
24
|
const catalogo = require('../lib/catalogo');
|
|
25
25
|
const streaming = require('../lib/streaming');
|
|
26
|
+
const effort = require('../lib/effort');
|
|
26
27
|
|
|
27
28
|
// A pasta de onde o comando foi dado. Não é mais onde ele trabalha (o padrão é
|
|
28
29
|
// ~/primocode), mas continua servindo de ponto de partida para achar pasta que
|
|
@@ -48,7 +49,7 @@ ${c.bold('Comandos')}
|
|
|
48
49
|
${c.brand('/dir')} <caminho> trabalha numa pasta que já existe (onde você quiser)
|
|
49
50
|
${c.brand('/project')} lista projetos ou cria em ~/primocode: /project <nome>
|
|
50
51
|
${c.brand('/mode')} /mode primocode|chat|auto
|
|
51
|
-
${c.brand('/
|
|
52
|
+
${c.brand('/effort')} esforço do agente — do rápido ao mais esperto
|
|
52
53
|
${c.brand('/attach')} <arquivo> anexa arquivo ao próximo pedido
|
|
53
54
|
${c.brand('/files')} lista anexos ${c.brand('/detach')} remove
|
|
54
55
|
${c.brand('/tudo')} lista tudo que ele sabe fazer
|
|
@@ -87,7 +88,7 @@ const SLASH_COMANDOS = [
|
|
|
87
88
|
['/project', 'lista/cria projetos em ~/primocode'],
|
|
88
89
|
['/dir', 'trabalhar numa pasta sua'],
|
|
89
90
|
['/mode', 'primocode | chat | auto'],
|
|
90
|
-
['/
|
|
91
|
+
['/effort', 'esforço: low → ultracode (←/→ para ajustar)'],
|
|
91
92
|
['/attach', 'anexa arquivo ao pedido'],
|
|
92
93
|
['/detach', 'remove os anexos'],
|
|
93
94
|
['/files', 'lista os anexos'],
|
|
@@ -117,6 +118,105 @@ function completarSlash(line) {
|
|
|
117
118
|
return [acertos, line];
|
|
118
119
|
}
|
|
119
120
|
|
|
121
|
+
// ── /effort: o slider de esforço (estilo Claude Code) ──────────────────
|
|
122
|
+
// Régua com os níveis, o marcador ▲ embaixo do escolhido, "Faster" à
|
|
123
|
+
// esquerda e "Smarter" à direita. ←/→ move, Enter confirma, Esc cancela.
|
|
124
|
+
|
|
125
|
+
function desenharEffort(idx) {
|
|
126
|
+
const cols = Math.max(52, Math.min(process.stdout.columns || 100, 104));
|
|
127
|
+
const regua = '▔'.repeat(cols);
|
|
128
|
+
const linhas = [];
|
|
129
|
+
|
|
130
|
+
// Cada nível ocupa uma coluna própria; o ultracode fica destacado no fim.
|
|
131
|
+
const rotulos = effort.NOMES;
|
|
132
|
+
const larguras = rotulos.map((n) => Math.max(n.length + 3, 9));
|
|
133
|
+
const posicoes = [];
|
|
134
|
+
let x = 23; // recuo da régua, igual ao do mockup
|
|
135
|
+
for (let i = 0; i < rotulos.length; i++) {
|
|
136
|
+
posicoes.push(x + Math.floor(larguras[i] / 2));
|
|
137
|
+
x += larguras[i];
|
|
138
|
+
}
|
|
139
|
+
const fimRegua = x;
|
|
140
|
+
|
|
141
|
+
linhas.push(c.dim(regua));
|
|
142
|
+
linhas.push(' ' + c.bold(c.white('Effort')));
|
|
143
|
+
linhas.push('');
|
|
144
|
+
|
|
145
|
+
// Faster ....▲.... Smarter — a trilha, com o marcador na posição atual.
|
|
146
|
+
const marca = posicoes[idx];
|
|
147
|
+
let trilha = '';
|
|
148
|
+
for (let k = 23; k < fimRegua; k++) {
|
|
149
|
+
if (k === marca) trilha += c.brand('▲');
|
|
150
|
+
else if (k === fimRegua - 12) trilha += c.dim('┆'); // ultracode é um degrau à parte
|
|
151
|
+
else trilha += c.dim(k < marca ? '─' : '─');
|
|
152
|
+
}
|
|
153
|
+
linhas.push(' ' + c.muted('Faster') + ' '.repeat(Math.max(1, fimRegua - 23 - 6 - 7)) + c.muted('Smarter'));
|
|
154
|
+
linhas.push(' ' + trilha);
|
|
155
|
+
|
|
156
|
+
// Os nomes, o escolhido aceso.
|
|
157
|
+
let linhaNomes = ' '.repeat(23);
|
|
158
|
+
for (let i = 0; i < rotulos.length; i++) {
|
|
159
|
+
const nome = rotulos[i];
|
|
160
|
+
const espaco = larguras[i];
|
|
161
|
+
const pad = Math.floor((espaco - nome.length) / 2);
|
|
162
|
+
const texto = i === idx ? c.bold(c.brand(nome)) : c.muted(nome);
|
|
163
|
+
linhaNomes += ' '.repeat(pad) + texto + ' '.repeat(espaco - nome.length - pad);
|
|
164
|
+
}
|
|
165
|
+
linhas.push(linhaNomes);
|
|
166
|
+
|
|
167
|
+
const nivel = effort.nivelDe(rotulos[idx]);
|
|
168
|
+
linhas.push(' ' + c.dim(nivel.resumo));
|
|
169
|
+
linhas.push('');
|
|
170
|
+
linhas.push(' ' + c.muted('←/→ ajusta · Enter confirma · Esc cancela'));
|
|
171
|
+
return linhas.join('\n');
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Desenha, escuta as setas, e devolve o nome escolhido (ou null se cancelou).
|
|
175
|
+
function sliderEffort(atual) {
|
|
176
|
+
return new Promise((resolve) => {
|
|
177
|
+
if (!process.stdin.isTTY) { // sem terminal (pipe, CI) não há slider
|
|
178
|
+
console.log(c.muted(` Esforço atual: ${effort.normalizar(atual)}. Use /effort <${effort.NOMES.join('|')}>.`));
|
|
179
|
+
return resolve(null);
|
|
180
|
+
}
|
|
181
|
+
let idx = effort.indiceDe(atual);
|
|
182
|
+
const ALTURA = 9; // linhas que o desenho ocupa — para redesenhar por cima
|
|
183
|
+
|
|
184
|
+
const pintar = (primeira) => {
|
|
185
|
+
if (!primeira) process.stdout.write(`\x1b[${ALTURA}A\r`);
|
|
186
|
+
process.stdout.write(desenharEffort(idx).split('\n').map((l) => '\x1b[2K' + l).join('\n') + '\n');
|
|
187
|
+
};
|
|
188
|
+
pintar(true);
|
|
189
|
+
|
|
190
|
+
const aoTeclar = (ch, key) => {
|
|
191
|
+
if (!key) return;
|
|
192
|
+
if (key.name === 'right' || key.name === 'l') { idx = Math.min(effort.NOMES.length - 1, idx + 1); pintar(false); return; }
|
|
193
|
+
if (key.name === 'left' || key.name === 'h') { idx = Math.max(0, idx - 1); pintar(false); return; }
|
|
194
|
+
if (key.name === 'return' || key.name === 'enter') { encerrar(effort.NOMES[idx]); return; }
|
|
195
|
+
if (key.name === 'escape' || (key.ctrl && key.name === 'c')) { encerrar(null); return; }
|
|
196
|
+
};
|
|
197
|
+
const encerrar = (valor) => {
|
|
198
|
+
process.stdin.removeListener('keypress', aoTeclar);
|
|
199
|
+
sliderAtivo = false;
|
|
200
|
+
resolve(valor);
|
|
201
|
+
};
|
|
202
|
+
sliderAtivo = true;
|
|
203
|
+
process.stdin.on('keypress', aoTeclar);
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function aplicarEffort(nome) {
|
|
208
|
+
const nivel = effort.nivelDe(nome);
|
|
209
|
+
state.config.effort = nivel.nome;
|
|
210
|
+
state.config.model = nivel.model;
|
|
211
|
+
cfg.saveConfig({ effort: nivel.nome, model: nivel.model });
|
|
212
|
+
console.log(c.ok(`Esforço: ${nivel.nome}`) + c.muted(` ${nivel.resumo}`));
|
|
213
|
+
if (nivel.pesquisa) console.log(c.dim(' Pesquisa de referência ligada: ele consulta docs e exemplos antes de escrever.'));
|
|
214
|
+
refreshPrompt();
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Enquanto o slider está no ar, o fantasma do autocompletar não desenha.
|
|
218
|
+
let sliderAtivo = false;
|
|
219
|
+
|
|
120
220
|
function mostrarMenuSlash() {
|
|
121
221
|
console.log('');
|
|
122
222
|
for (const [nome, desc] of SLASH_COMANDOS) {
|
|
@@ -309,6 +409,7 @@ async function sendPrompt(userText) {
|
|
|
309
409
|
const r = await runAct(prompt, {
|
|
310
410
|
server: state.config.server,
|
|
311
411
|
model: state.config.model,
|
|
412
|
+
effort: state.config.effort,
|
|
312
413
|
token: state.config.token,
|
|
313
414
|
history: state.history,
|
|
314
415
|
projectDir: plan.dir,
|
|
@@ -464,24 +565,38 @@ async function handleSlash(text) {
|
|
|
464
565
|
break;
|
|
465
566
|
}
|
|
466
567
|
|
|
568
|
+
// O /model virou /effort: você escolhe o ESFORÇO num slider (←/→,
|
|
569
|
+
// Enter confirma, Esc cancela) e o modelo sai daí. Com argumento
|
|
570
|
+
// ("/effort high") aplica direto, para quem já sabe o que quer.
|
|
571
|
+
case 'effort': case 'esforco': case 'esforço': {
|
|
572
|
+
const pedido = rest.toLowerCase().trim();
|
|
573
|
+
if (pedido) {
|
|
574
|
+
if (!effort.NOMES.includes(pedido)) {
|
|
575
|
+
console.log(c.err(`Esforço inválido. Use: ${effort.NOMES.join(' | ')}`));
|
|
576
|
+
break;
|
|
577
|
+
}
|
|
578
|
+
aplicarEffort(pedido);
|
|
579
|
+
break;
|
|
580
|
+
}
|
|
581
|
+
const escolhido = await sliderEffort(state.config.effort);
|
|
582
|
+
if (escolhido) aplicarEffort(escolhido);
|
|
583
|
+
else console.log(c.muted(' Nada mudou.'));
|
|
584
|
+
break;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// Continua existindo para quem fixa um ID do OpenRouter na unha; o
|
|
588
|
+
// caminho normal é o /effort.
|
|
467
589
|
case 'model': {
|
|
468
590
|
const m = rest.toLowerCase();
|
|
469
|
-
|
|
470
|
-
const nomes = {
|
|
471
|
-
top: 'o mais capaz — 550B, 1 milhão de tokens de contexto',
|
|
472
|
-
main: 'o padrão — agente de código, o melhor em tarefa de terminal',
|
|
473
|
-
fast: 'o mais rápido — resposta curta, alta vazão',
|
|
474
|
-
};
|
|
475
|
-
if (nomes[m]) {
|
|
591
|
+
if (m.includes('/')) {
|
|
476
592
|
state.config.model = m;
|
|
477
593
|
cfg.saveConfig({ model: m });
|
|
478
|
-
console.log(c.ok(`Modelo: ${m}`) + c.muted(
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
} else console.log(c.err('Uso: /model top|main|fast (ou um ID do OpenRouter, ex: google/gemini-2.5-pro)'));
|
|
594
|
+
console.log(c.ok(`Modelo fixo: ${m}`) + c.muted(' precisa aceitar tools no modo primocode'));
|
|
595
|
+
console.log(c.dim(' Para voltar ao normal, use /effort.'));
|
|
596
|
+
} else {
|
|
597
|
+
console.log(c.muted(' O modelo agora vem do esforço. Use ') + c.brand('/effort') + c.muted('.'));
|
|
598
|
+
console.log(c.dim(` Agora: ${state.config.effort} → modelo ${effort.modeloDe(state.config.effort)}`));
|
|
599
|
+
}
|
|
485
600
|
break;
|
|
486
601
|
}
|
|
487
602
|
|
|
@@ -613,7 +728,8 @@ async function handleSlash(text) {
|
|
|
613
728
|
console.log('');
|
|
614
729
|
console.log(` ${c.muted('nome')} ${c.white(state.config.userName || '(nenhum)')}`);
|
|
615
730
|
console.log(` ${c.muted('mascote')} ${c.white(state.config.mascot || 'primo')}`);
|
|
616
|
-
console.log(` ${c.muted('
|
|
731
|
+
console.log(` ${c.muted('esforço')} ${c.white(state.config.effort)}${c.dim(' · /effort muda')}`);
|
|
732
|
+
console.log(` ${c.muted('modelo')} ${c.white(state.config.model)}${c.dim(' (vem do esforço)')}`);
|
|
617
733
|
console.log(` ${c.muted('modo')} ${c.white(state.mode)}`);
|
|
618
734
|
console.log(` ${c.muted('servidor')} ${c.dim(state.config.server)}`);
|
|
619
735
|
console.log(c.muted(`\n ${cfg.CONFIG_FILE}`));
|
|
@@ -733,6 +849,7 @@ async function main() {
|
|
|
733
849
|
await runAct(fullPrompt, {
|
|
734
850
|
server: state.config.server,
|
|
735
851
|
model: state.config.model,
|
|
852
|
+
effort: state.config.effort,
|
|
736
853
|
token: state.config.token,
|
|
737
854
|
history: [],
|
|
738
855
|
projectDir: plan.dir,
|
|
@@ -766,7 +883,7 @@ async function main() {
|
|
|
766
883
|
if (process.stdin.isTTY && process.stdout.isTTY) {
|
|
767
884
|
let fantasma = '';
|
|
768
885
|
const desenharFantasma = () => {
|
|
769
|
-
if (busy || pendingAsk || !rl) return;
|
|
886
|
+
if (busy || pendingAsk || sliderAtivo || !rl) return;
|
|
770
887
|
const linha = rl.line;
|
|
771
888
|
const noFim = rl.cursor === linha.length;
|
|
772
889
|
fantasma = '';
|
|
@@ -785,6 +902,7 @@ async function main() {
|
|
|
785
902
|
// sequence "\x1b\x1b" — vale como os dois.
|
|
786
903
|
let ultimoEsc = 0;
|
|
787
904
|
process.stdin.on('keypress', (ch, key) => {
|
|
905
|
+
if (sliderAtivo) return; // o slider é dono do teclado enquanto está no ar
|
|
788
906
|
if (key && (key.name === 'escape' || key.sequence === '\x1b\x1b')) {
|
|
789
907
|
if (!busy) { ultimoEsc = 0; return; } // sem agente rodando, Esc é só Esc
|
|
790
908
|
const agora = Date.now();
|
package/lib/act.js
CHANGED
|
@@ -105,6 +105,14 @@ function logResult(kind, result, profundidade) {
|
|
|
105
105
|
case 'browser_read':
|
|
106
106
|
console.log(p + c.muted(`${(result.text || '').length} caracteres lidos da página`));
|
|
107
107
|
return;
|
|
108
|
+
case 'pesquisar_referencia': {
|
|
109
|
+
console.log(p + c.muted(`${result.fontes} fonte${result.fontes === 1 ? '' : 's'} lida${result.fontes === 1 ? '' : 's'} sobre "${result.consulta}"`));
|
|
110
|
+
for (const r of (result.referencias || []).slice(0, 4)) {
|
|
111
|
+
console.log(p + c.dim(`${r.titulo}`.slice(0, 100)));
|
|
112
|
+
console.log(p + c.dim(` ${r.url}`.slice(0, 110)));
|
|
113
|
+
}
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
108
116
|
case 'desktop_windows': {
|
|
109
117
|
console.log(p + c.muted(`${result.total} janela${result.total === 1 ? '' : 's'}`));
|
|
110
118
|
for (const j of (result.janelas || []).slice(0, 5)) console.log(p + c.dim(`${j.app ? j.app + ' · ' : ''}${j.titulo}`.slice(0, 120)));
|
|
@@ -168,7 +176,7 @@ function buildMemoryContext(projectDir, projectName) {
|
|
|
168
176
|
async function runAct(userPrompt, opts) {
|
|
169
177
|
const {
|
|
170
178
|
server, model, token, history, projectDir, projectName,
|
|
171
|
-
ensureWorkDir, profundidade = 0, subagent = false,
|
|
179
|
+
ensureWorkDir, profundidade = 0, subagent = false, effort,
|
|
172
180
|
} = opts || {};
|
|
173
181
|
|
|
174
182
|
// A pasta de projeto só nasce quando o agente for mesmo MEXER EM ARQUIVO.
|
|
@@ -210,7 +218,7 @@ async function runAct(userPrompt, opts) {
|
|
|
210
218
|
let streaming = false;
|
|
211
219
|
|
|
212
220
|
try {
|
|
213
|
-
await api.requestAct(server, { messages, model, token, projectDir, projectName, subagent }, (evt) => {
|
|
221
|
+
await api.requestAct(server, { messages, model, effort, token, projectDir, projectName, subagent }, (evt) => {
|
|
214
222
|
if (evt.erro) { streamError = evt.erro; return; }
|
|
215
223
|
if (evt.tool_call) { toolCalls.push(evt.tool_call); return; }
|
|
216
224
|
if (evt.delta) {
|
package/lib/catalogo.js
CHANGED
|
@@ -78,6 +78,7 @@ const GRUPOS = [
|
|
|
78
78
|
resumo: 'como ele se organiza numa tarefa grande',
|
|
79
79
|
itens: [
|
|
80
80
|
{ tool: 'write_todos', escreve: false, faz: 'monta o plano e vai marcando o que terminou' },
|
|
81
|
+
{ tool: 'pesquisar_referencia', escreve: false, faz: 'lê docs e exemplos antes de escrever — sobe a qualidade' },
|
|
81
82
|
// Estas duas o loop do agente atende (lib/act.js), não o tools.execute.
|
|
82
83
|
{ tool: 'spawn_agent', escreve: false, faz: 'chama um subagente para uma parte da tarefa', noLoop: true },
|
|
83
84
|
{ tool: 'finish', escreve: false, faz: 'encerra dizendo o que fez', noLoop: true },
|
package/lib/config.js
CHANGED
|
@@ -160,6 +160,9 @@ function loadConfig() {
|
|
|
160
160
|
return {
|
|
161
161
|
server: process.env.PRIMOCODE_SERVER || file.server || 'https://primocode-server.onrender.com',
|
|
162
162
|
model: process.env.PRIMOCODE_MODEL || file.model || 'main',
|
|
163
|
+
// Esforço, estilo Claude Code (/effort). É o que o usuário escolhe; o
|
|
164
|
+
// modelo é derivado dele. Guardado para durar entre sessões.
|
|
165
|
+
effort: process.env.PRIMOCODE_EFFORT || file.effort || 'medium',
|
|
163
166
|
token: process.env.PRIMOCODE_TOKEN || file.token || null,
|
|
164
167
|
autoApply: file.autoApply === true,
|
|
165
168
|
currentProject: file.currentProject || null,
|
|
@@ -177,8 +180,8 @@ function saveConfig(partial) {
|
|
|
177
180
|
ensureDir(CONFIG_DIR);
|
|
178
181
|
const cur = (() => { try { return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8')); } catch { return {}; } })();
|
|
179
182
|
// Só persiste o que faz sentido guardar — nada de campos derivados.
|
|
180
|
-
const { server, model, token, autoApply, currentProject, userName, mascot, theme, mode } = { ...cur, ...partial };
|
|
181
|
-
const next = { server, model, token, autoApply, currentProject, userName, mascot, theme, mode };
|
|
183
|
+
const { server, model, effort, token, autoApply, currentProject, userName, mascot, theme, mode } = { ...cur, ...partial };
|
|
184
|
+
const next = { server, model, effort, token, autoApply, currentProject, userName, mascot, theme, mode };
|
|
182
185
|
fs.writeFileSync(CONFIG_FILE, JSON.stringify(next, null, 2), 'utf8');
|
|
183
186
|
return next;
|
|
184
187
|
}
|
package/lib/effort.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* effort.js — o "esforço" do agente, no espírito do /effort do Claude Code.
|
|
3
|
+
*
|
|
4
|
+
* O usuário não escolhe mais um modelo por nome (/model): escolhe um ESFORÇO
|
|
5
|
+
* num controle deslizante, do mais rápido ao mais esperto. O modelo é derivado
|
|
6
|
+
* daqui. Fica tudo num lugar só, puro (sem I/O), para o teste bater sem rede.
|
|
7
|
+
*
|
|
8
|
+
* A pesquisa de referência (abrir docs/exemplos e aprender com eles antes de
|
|
9
|
+
* escrever) liga a partir do "high": esforço maior = ele pesquisa antes de
|
|
10
|
+
* codar, o que melhora a qualidade. É VISÍVEL no painel — nada escondido.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
'use strict';
|
|
14
|
+
|
|
15
|
+
// Ordem = ordem do slider, do mais rápido ao mais esperto.
|
|
16
|
+
const NIVEIS = [
|
|
17
|
+
{ nome: 'low', model: 'fast', pesquisa: false, resumo: 'rápido — tarefa simples, resposta curta' },
|
|
18
|
+
{ nome: 'medium', model: 'main', pesquisa: false, resumo: 'equilíbrio — o padrão do dia a dia' },
|
|
19
|
+
{ nome: 'high', model: 'main', pesquisa: true, resumo: 'capricha no código e pesquisa referências' },
|
|
20
|
+
{ nome: 'xhigh', model: 'top', pesquisa: true, resumo: 'o mais capaz — 1M de contexto, pesquisa' },
|
|
21
|
+
{ nome: 'max', model: 'top', pesquisa: true, resumo: 'esforço máximo — pesquisa a fundo' },
|
|
22
|
+
{ nome: 'ultracode', model: 'top', pesquisa: true, resumo: 'tudo no talo — pesquisa e cruza referências' },
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
const NOMES = NIVEIS.map((n) => n.nome);
|
|
26
|
+
|
|
27
|
+
function normalizar(nome) {
|
|
28
|
+
const n = String(nome || '').toLowerCase().trim();
|
|
29
|
+
return NOMES.includes(n) ? n : 'medium';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function nivelDe(nome) {
|
|
33
|
+
return NIVEIS.find((n) => n.nome === normalizar(nome)) || NIVEIS[1];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function indiceDe(nome) {
|
|
37
|
+
const i = NOMES.indexOf(normalizar(nome));
|
|
38
|
+
return i < 0 ? 1 : i;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** O apelido de modelo que este esforço usa. */
|
|
42
|
+
function modeloDe(nome) {
|
|
43
|
+
return nivelDe(nome).model;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Este esforço pesquisa referências antes de codar? */
|
|
47
|
+
function pesquisaDe(nome) {
|
|
48
|
+
return nivelDe(nome).pesquisa;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
module.exports = { NIVEIS, NOMES, normalizar, nivelDe, indiceDe, modeloDe, pesquisaDe };
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* referencia.js — pesquisa de referência antes de escrever código.
|
|
3
|
+
*
|
|
4
|
+
* A ideia do dono: em vez de o agente inventar tudo da cabeça, ele consulta o
|
|
5
|
+
* que já existe sobre o pedido (documentação oficial, exemplos, respostas) e
|
|
6
|
+
* escreve com esse conhecimento na mão. A qualidade sobe porque ele para de
|
|
7
|
+
* chutar API que não existe e passa a seguir o padrão real da ferramenta.
|
|
8
|
+
*
|
|
9
|
+
* Duas decisões de projeto, deliberadas:
|
|
10
|
+
*
|
|
11
|
+
* 1. É VISÍVEL, não oculto. Cada pesquisa aparece no terminal e vira uma aba
|
|
12
|
+
* no /streaming, com as fontes. Um agente que abre coisas escondido do dono
|
|
13
|
+
* da máquina é um agente em que não dá para confiar — e o /streaming existe
|
|
14
|
+
* justamente para mostrar tudo.
|
|
15
|
+
*
|
|
16
|
+
* 2. Ele extrai CONHECIMENTO (como se faz, qual API, que cuidado tomar), não
|
|
17
|
+
* blocos de código para colar. Copiar trechos de terceiros para dentro do
|
|
18
|
+
* projeto do usuário sem olhar licença é problema jurídico dele, não nosso;
|
|
19
|
+
* e código colado de fontes que não conversam entre si é justamente o que
|
|
20
|
+
* produz o Frankenstein que não compila. O modelo lê, aprende e ESCREVE.
|
|
21
|
+
*
|
|
22
|
+
* Zero dependências: https nativo. Sem chave de API — usa as fontes abertas.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
'use strict';
|
|
26
|
+
|
|
27
|
+
const https = require('https');
|
|
28
|
+
const http = require('http');
|
|
29
|
+
const net = require('net');
|
|
30
|
+
const tls = require('tls');
|
|
31
|
+
const { URL } = require('url');
|
|
32
|
+
|
|
33
|
+
const TIMEOUT_MS = 12000;
|
|
34
|
+
const MAX_POR_FONTE = 4;
|
|
35
|
+
const MAX_TEXTO = 4500;
|
|
36
|
+
|
|
37
|
+
// Rede de empresa costuma exigir proxy. Sem isto, a pesquisa simplesmente não
|
|
38
|
+
// funciona em metade dos computadores corporativos — e o usuário nunca
|
|
39
|
+
// descobre por quê.
|
|
40
|
+
const PROXY = process.env.HTTPS_PROXY || process.env.https_proxy
|
|
41
|
+
|| process.env.HTTP_PROXY || process.env.http_proxy || null;
|
|
42
|
+
|
|
43
|
+
/** Abre um túnel CONNECT no proxy e devolve o socket TLS pronto. */
|
|
44
|
+
function viaProxy(alvo, { timeout }) {
|
|
45
|
+
return new Promise((resolve, reject) => {
|
|
46
|
+
const p = new URL(PROXY);
|
|
47
|
+
const porta = alvo.port || 443;
|
|
48
|
+
const req = http.request({
|
|
49
|
+
host: p.hostname,
|
|
50
|
+
port: p.port || 80,
|
|
51
|
+
method: 'CONNECT',
|
|
52
|
+
path: `${alvo.hostname}:${porta}`,
|
|
53
|
+
timeout,
|
|
54
|
+
headers: p.username
|
|
55
|
+
? { 'Proxy-Authorization': 'Basic ' + Buffer.from(`${p.username}:${p.password}`).toString('base64') }
|
|
56
|
+
: {},
|
|
57
|
+
});
|
|
58
|
+
req.on('connect', (res, socket) => {
|
|
59
|
+
if (res.statusCode !== 200) { socket.destroy(); return reject(new Error(`proxy ${res.statusCode}`)); }
|
|
60
|
+
resolve(tls.connect({ socket, servername: alvo.hostname }));
|
|
61
|
+
});
|
|
62
|
+
req.on('error', reject);
|
|
63
|
+
req.on('timeout', () => { req.destroy(); reject(new Error('timeout no proxy')); });
|
|
64
|
+
req.end();
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function pegar(url, { timeout = TIMEOUT_MS } = {}) {
|
|
69
|
+
return new Promise((resolve) => {
|
|
70
|
+
let req;
|
|
71
|
+
const alvo = (() => { try { return new URL(url); } catch { return null; } })();
|
|
72
|
+
if (!alvo) return resolve(null);
|
|
73
|
+
|
|
74
|
+
const opcoes = {
|
|
75
|
+
headers: {
|
|
76
|
+
'User-Agent': 'PrimoCode/1.0 (+https://github.com/ConectaPrimoAI/primocode)',
|
|
77
|
+
Accept: 'text/html,application/json;q=0.9,*/*;q=0.8',
|
|
78
|
+
},
|
|
79
|
+
timeout,
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// Com proxy, a conexão vai por um túnel CONNECT; sem proxy, direto.
|
|
83
|
+
const abrir = PROXY && alvo.protocol === 'https:'
|
|
84
|
+
? viaProxy(alvo, { timeout }).then((socket) => ({
|
|
85
|
+
...opcoes, createConnection: () => socket, host: alvo.hostname, servername: alvo.hostname,
|
|
86
|
+
})).catch(() => null)
|
|
87
|
+
: Promise.resolve(opcoes);
|
|
88
|
+
|
|
89
|
+
abrir.then((cfg) => {
|
|
90
|
+
if (!cfg) return resolve(null);
|
|
91
|
+
try {
|
|
92
|
+
req = https.get(url, cfg, (res) => {
|
|
93
|
+
// Redirecionamento: segue uma vez, o suficiente para docs.
|
|
94
|
+
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
95
|
+
res.resume();
|
|
96
|
+
const destino = new URL(res.headers.location, url).href;
|
|
97
|
+
return pegar(destino, { timeout }).then(resolve);
|
|
98
|
+
}
|
|
99
|
+
if (res.statusCode !== 200) { res.resume(); return resolve(null); }
|
|
100
|
+
let corpo = '';
|
|
101
|
+
res.setEncoding('utf8');
|
|
102
|
+
res.on('data', (c) => {
|
|
103
|
+
corpo += c;
|
|
104
|
+
if (corpo.length > 400000) { req.destroy(); } // página gigante não serve
|
|
105
|
+
});
|
|
106
|
+
res.on('end', () => resolve(corpo));
|
|
107
|
+
res.on('error', () => resolve(null));
|
|
108
|
+
});
|
|
109
|
+
} catch { return resolve(null); }
|
|
110
|
+
req.on('timeout', () => { req.destroy(); resolve(null); });
|
|
111
|
+
req.on('error', () => resolve(null));
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** HTML → texto legível. Sem dependência: corta script/style e as tags. */
|
|
117
|
+
function textoDe(html) {
|
|
118
|
+
return String(html || '')
|
|
119
|
+
.replace(/<script[\s\S]*?<\/script>/gi, ' ')
|
|
120
|
+
.replace(/<style[\s\S]*?<\/style>/gi, ' ')
|
|
121
|
+
.replace(/<nav[\s\S]*?<\/nav>/gi, ' ')
|
|
122
|
+
.replace(/<footer[\s\S]*?<\/footer>/gi, ' ')
|
|
123
|
+
.replace(/<[^>]+>/g, ' ')
|
|
124
|
+
.replace(/ /g, ' ').replace(/&/g, '&').replace(/</g, '<')
|
|
125
|
+
.replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, "'")
|
|
126
|
+
.replace(/\s+/g, ' ')
|
|
127
|
+
.trim();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Links que não são referência de nada — resultado de busca sujo.
|
|
131
|
+
const LIXO = /(facebook|twitter|x)\.com|instagram|pinterest|youtube\.com\/(?!watch)|\/search\?|duckduckgo|bing\.com\/(?!ck)|google\./i;
|
|
132
|
+
|
|
133
|
+
function limparLinks(pares, limite) {
|
|
134
|
+
const vistos = new Set();
|
|
135
|
+
const bons = [];
|
|
136
|
+
for (const { titulo, url } of pares) {
|
|
137
|
+
if (!/^https?:\/\//i.test(url) || LIXO.test(url)) continue;
|
|
138
|
+
const chave = url.replace(/[#?].*$/, '');
|
|
139
|
+
if (vistos.has(chave)) continue;
|
|
140
|
+
vistos.add(chave);
|
|
141
|
+
bons.push({ titulo: titulo.slice(0, 140), url });
|
|
142
|
+
if (bons.length >= limite) break;
|
|
143
|
+
}
|
|
144
|
+
return bons;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** DuckDuckGo (HTML). O destino vem embrulhado em /l/?uddg=<url>. */
|
|
148
|
+
async function buscarDDG(consulta, limite) {
|
|
149
|
+
const html = await pegar(`https://duckduckgo.com/html/?q=${encodeURIComponent(consulta)}`);
|
|
150
|
+
if (!html) return [];
|
|
151
|
+
const pares = [];
|
|
152
|
+
const re = /<a[^>]+class="[^"]*result__a[^"]*"[^>]+href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/gi;
|
|
153
|
+
let m;
|
|
154
|
+
while ((m = re.exec(html))) {
|
|
155
|
+
let href = m[1];
|
|
156
|
+
const embrulho = href.match(/[?&]uddg=([^&]+)/);
|
|
157
|
+
if (embrulho) href = decodeURIComponent(embrulho[1]);
|
|
158
|
+
pares.push({ titulo: textoDe(m[2]), url: href });
|
|
159
|
+
}
|
|
160
|
+
return limparLinks(pares, limite);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Bing (HTML) — a reserva quando o DDG não responde. */
|
|
164
|
+
async function buscarBing(consulta, limite) {
|
|
165
|
+
const html = await pegar(`https://www.bing.com/search?q=${encodeURIComponent(consulta)}&setlang=en`);
|
|
166
|
+
if (!html) return [];
|
|
167
|
+
const pares = [];
|
|
168
|
+
const re = /<h2>\s*<a[^>]+href="(https?:\/\/[^"]+)"[^>]*>([\s\S]*?)<\/a>/gi;
|
|
169
|
+
let m;
|
|
170
|
+
while ((m = re.exec(html))) pares.push({ titulo: textoDe(m[2]), url: m[1] });
|
|
171
|
+
return limparLinks(pares, limite);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** SearXNG público, em JSON — a terceira porta. */
|
|
175
|
+
async function buscarSearx(consulta, limite) {
|
|
176
|
+
const corpo = await pegar(`https://searx.be/search?q=${encodeURIComponent(consulta)}&format=json`);
|
|
177
|
+
if (!corpo) return [];
|
|
178
|
+
try {
|
|
179
|
+
const j = JSON.parse(corpo);
|
|
180
|
+
return limparLinks((j.results || []).map((r) => ({ titulo: String(r.title || ''), url: String(r.url || '') })), limite);
|
|
181
|
+
} catch { return []; }
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Busca na web. Tenta as portas em ordem e para na primeira que responder —
|
|
186
|
+
* um buscador fora do ar (ou bloqueado pela rede) não pode desligar a
|
|
187
|
+
* pesquisa inteira. Sem chave de API em nenhuma delas.
|
|
188
|
+
*/
|
|
189
|
+
async function buscar(consulta, limite = MAX_POR_FONTE) {
|
|
190
|
+
for (const porta of [buscarDDG, buscarBing, buscarSearx]) {
|
|
191
|
+
try {
|
|
192
|
+
const achados = await porta(consulta, limite);
|
|
193
|
+
if (achados.length) return achados;
|
|
194
|
+
} catch {}
|
|
195
|
+
}
|
|
196
|
+
return [];
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Respostas do StackOverflow — a API pública, sem chave. */
|
|
200
|
+
async function stackoverflow(consulta, limite = 3) {
|
|
201
|
+
const url = 'https://api.stackexchange.com/2.3/search/advanced'
|
|
202
|
+
+ `?order=desc&sort=relevance&q=${encodeURIComponent(consulta)}`
|
|
203
|
+
+ '&site=stackoverflow&filter=default&pagesize=' + limite;
|
|
204
|
+
const corpo = await pegar(url);
|
|
205
|
+
if (!corpo) return [];
|
|
206
|
+
try {
|
|
207
|
+
const j = JSON.parse(corpo);
|
|
208
|
+
return (j.items || []).slice(0, limite).map((i) => ({
|
|
209
|
+
titulo: textoDe(i.title).slice(0, 140),
|
|
210
|
+
url: i.link,
|
|
211
|
+
nota: `${i.score} votos${i.is_answered ? ', respondida' : ''}`,
|
|
212
|
+
}));
|
|
213
|
+
} catch { return []; }
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Última porta: a documentação oficial dos assuntos mais comuns, direto. Não
|
|
217
|
+
// depende de buscador nenhum — é o que salva a pesquisa em rede corporativa
|
|
218
|
+
// que bloqueia mecanismo de busca mas libera site de documentação.
|
|
219
|
+
const DOCS = [
|
|
220
|
+
[/\b(js|javascript|array|string|promise|fetch|dom|css|html|regex)\b/i,
|
|
221
|
+
(q) => `https://developer.mozilla.org/en-US/search?q=${encodeURIComponent(q)}`],
|
|
222
|
+
[/\b(react|jsx|hook|usestate|useeffect)\b/i, () => 'https://react.dev/reference/react'],
|
|
223
|
+
[/\b(node|npm|express|fs|require)\b/i, () => 'https://nodejs.org/docs/latest/api/'],
|
|
224
|
+
[/\b(python|django|flask|pandas)\b/i,
|
|
225
|
+
(q) => `https://docs.python.org/3/search.html?q=${encodeURIComponent(q)}`],
|
|
226
|
+
];
|
|
227
|
+
|
|
228
|
+
function docsConhecidas(consulta) {
|
|
229
|
+
const achados = [];
|
|
230
|
+
for (const [re, url] of DOCS) {
|
|
231
|
+
if (re.test(consulta)) achados.push({ titulo: 'documentação oficial', url: url(consulta) });
|
|
232
|
+
}
|
|
233
|
+
return achados;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* A pesquisa completa: busca, abre as melhores páginas e devolve o texto
|
|
238
|
+
* delas para o modelo LER. O modelo escreve o código dele com isso na mão.
|
|
239
|
+
*/
|
|
240
|
+
async function pesquisar({ consulta, paginas = 3 }) {
|
|
241
|
+
const termo = String(consulta || '').trim();
|
|
242
|
+
if (!termo) return { ok: false, error: 'diga o que pesquisar' };
|
|
243
|
+
|
|
244
|
+
const [web, so] = await Promise.all([
|
|
245
|
+
buscar(termo, MAX_POR_FONTE),
|
|
246
|
+
stackoverflow(termo, 2),
|
|
247
|
+
]);
|
|
248
|
+
|
|
249
|
+
// A documentação oficial entra SEMPRE na fila, não só quando as outras
|
|
250
|
+
// portas vieram vazias: ela é a melhor fonte, e é o que salva a pesquisa
|
|
251
|
+
// quando os links do buscador não abrem (rede corporativa, site fora do
|
|
252
|
+
// ar, bloqueio). Foi assim que a pesquisa falhava mesmo achando links.
|
|
253
|
+
const candidatos = [...web, ...so, ...docsConhecidas(termo)]
|
|
254
|
+
.slice(0, Math.max(2, Math.min(8, paginas + 4)));
|
|
255
|
+
if (!candidatos.length) {
|
|
256
|
+
return { ok: false, error: `nada encontrado para "${termo}" (sem internet?)`, consulta: termo };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const lidas = [];
|
|
260
|
+
for (const cand of candidatos) {
|
|
261
|
+
if (lidas.length >= paginas) break;
|
|
262
|
+
const html = await pegar(cand.url);
|
|
263
|
+
if (!html) continue;
|
|
264
|
+
const texto = textoDe(html);
|
|
265
|
+
if (texto.length < 200) continue;
|
|
266
|
+
lidas.push({
|
|
267
|
+
titulo: cand.titulo,
|
|
268
|
+
url: cand.url,
|
|
269
|
+
nota: cand.nota,
|
|
270
|
+
trecho: texto.slice(0, MAX_TEXTO),
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (!lidas.length) {
|
|
275
|
+
return { ok: false, error: 'achei os links mas não consegui abrir nenhum', consulta: termo, links: candidatos.map((c) => c.url) };
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return {
|
|
279
|
+
ok: true,
|
|
280
|
+
consulta: termo,
|
|
281
|
+
fontes: lidas.length,
|
|
282
|
+
referencias: lidas,
|
|
283
|
+
comoUsar: 'Leia, entenda o padrão e ESCREVA o seu próprio código. Não copie trechos literais: '
|
|
284
|
+
+ 'cite a fonte se aproveitar uma ideia, e confira se a API citada existe na versão que o projeto usa.',
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
module.exports = { pesquisar, buscar, stackoverflow, textoDe };
|
package/lib/streaming.js
CHANGED
|
@@ -26,8 +26,13 @@ const DIR = path.join(os.homedir(), '.primocode', 'streaming');
|
|
|
26
26
|
const MAX_LOG = 300;
|
|
27
27
|
// Fotos guardadas em disco para o "voltar no tempo" das abas. Além disso a
|
|
28
28
|
// pasta viraria um álbum infinito — as mais velhas saem.
|
|
29
|
-
const MAX_FRAMES =
|
|
29
|
+
const MAX_FRAMES = 60;
|
|
30
30
|
const MAX_ABAS = 12;
|
|
31
|
+
// Teto de disco: mesmo com poucos quadros, tela 4K dá PNG gordo. Passou
|
|
32
|
+
// disso, as fotos mais velhas saem até caber. 120 MB de screenshots numa
|
|
33
|
+
// pasta escondida é exatamente o tipo de coisa que ninguém vai limpar.
|
|
34
|
+
const MAX_BYTES = 120 * 1024 * 1024;
|
|
35
|
+
let bytesEmDisco = 0;
|
|
31
36
|
// Uma foto por ação no máximo a cada 600ms: digitar letra por letra não pode
|
|
32
37
|
// virar uma rajada de screenshots.
|
|
33
38
|
const INTERVALO_FOTO_MS = 600;
|
|
@@ -154,6 +159,7 @@ function descrever(name, args = {}) {
|
|
|
154
159
|
case 'run_shell': return `rodou: ${corta(args.command, 60)}`;
|
|
155
160
|
case 'run_in_new_terminal': return `terminal novo: ${corta(args.command, 50)}`;
|
|
156
161
|
case 'write_todos': return 'atualizou o plano';
|
|
162
|
+
case 'pesquisar_referencia': return `pesquisou referências: “${corta(args.consulta || args.query, 50)}”`;
|
|
157
163
|
default: return name.replace(/_/g, ' ');
|
|
158
164
|
}
|
|
159
165
|
}
|
|
@@ -170,21 +176,69 @@ function nomeDeAba(name, args = {}) {
|
|
|
170
176
|
try { return new URL(String(args.url)).hostname || 'navegador'; } catch { return 'navegador'; }
|
|
171
177
|
}
|
|
172
178
|
if (args && args.window) return String(args.window).trim();
|
|
179
|
+
|
|
180
|
+
// Trabalho que não é de janela também merece aba — senão o painel fica
|
|
181
|
+
// vazio de "o que ela fez" justamente no caso mais comum (mexer em
|
|
182
|
+
// arquivo, rodar comando, pesquisar referência).
|
|
183
|
+
if (['write_file', 'edit_file', 'multi_edit', 'read_file'].includes(name)) {
|
|
184
|
+
const arq = String(args.path || '').split(/[\\/]/).pop();
|
|
185
|
+
return arq ? `📄 ${arq}` : '📄 Arquivos';
|
|
186
|
+
}
|
|
187
|
+
if (['run_shell', 'run_in_new_terminal'].includes(name)) return '⌨ Terminal';
|
|
188
|
+
if (name === 'pesquisar_referencia') return '🔎 Pesquisa';
|
|
189
|
+
if (['glob', 'grep', 'list_dir'].includes(name)) return '🔍 Busca';
|
|
190
|
+
if (name.startsWith('browser_')) return '🌐 Navegador';
|
|
191
|
+
if (name.startsWith('desktop_')) return '🖥 Tela';
|
|
192
|
+
if (name === 'spawn_agent') return '🤖 Subagente';
|
|
173
193
|
return '';
|
|
174
194
|
}
|
|
175
195
|
|
|
196
|
+
let proximaAba = 1;
|
|
176
197
|
function entrarNaAba(nome) {
|
|
177
198
|
const alvo = corta(nome, 24);
|
|
178
199
|
if (!alvo) return;
|
|
179
200
|
let aba = state.abas.find((a) => a.nome.toLowerCase() === alvo.toLowerCase());
|
|
180
201
|
if (!aba) {
|
|
181
|
-
|
|
202
|
+
// id de contador próprio: com `abas.length + 1`, uma aba descartada
|
|
203
|
+
// fazia a próxima nascer com id repetido — e os passos da nova
|
|
204
|
+
// apareciam dentro da antiga.
|
|
205
|
+
aba = { id: proximaAba++, nome: alvo, passos: 0 };
|
|
182
206
|
state.abas.push(aba);
|
|
183
207
|
if (state.abas.length > MAX_ABAS) state.abas.shift();
|
|
184
208
|
}
|
|
185
209
|
state.abaAtual = aba;
|
|
186
210
|
}
|
|
187
211
|
|
|
212
|
+
// Guarda o quadro e cobra o aluguel: acima de MAX_FRAMES ou de MAX_BYTES, as
|
|
213
|
+
// fotos mais velhas são APAGADAS do disco. É o que impede a pasta de virar um
|
|
214
|
+
// álbum infinito na máquina do usuário.
|
|
215
|
+
function registrarFrame(meta, arquivo) {
|
|
216
|
+
let tamanho = 0;
|
|
217
|
+
try { tamanho = fs.statSync(arquivo).size; } catch {}
|
|
218
|
+
state.frames.push({ ...meta, bytes: tamanho });
|
|
219
|
+
bytesEmDisco += tamanho;
|
|
220
|
+
|
|
221
|
+
while (state.frames.length > MAX_FRAMES || (bytesEmDisco > MAX_BYTES && state.frames.length > 1)) {
|
|
222
|
+
const velho = state.frames.shift();
|
|
223
|
+
bytesEmDisco -= velho.bytes || 0;
|
|
224
|
+
try { fs.unlinkSync(path.join(DIR, `frame-${velho.id}.png`)); } catch {}
|
|
225
|
+
}
|
|
226
|
+
if (bytesEmDisco < 0) bytesEmDisco = 0;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** Apaga TUDO que o painel deixou no disco. Chamado ao desligar e ao sair. */
|
|
230
|
+
function limparDisco() {
|
|
231
|
+
try {
|
|
232
|
+
for (const f of fs.readdirSync(DIR)) {
|
|
233
|
+
if (/^(frame-\d+|live|live-tmp)\.png$/.test(f)) {
|
|
234
|
+
try { fs.unlinkSync(path.join(DIR, f)); } catch {}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
} catch {}
|
|
238
|
+
state.frames = [];
|
|
239
|
+
bytesEmDisco = 0;
|
|
240
|
+
}
|
|
241
|
+
|
|
188
242
|
/**
|
|
189
243
|
* A instrução que o usuário acabou de dar. A página autopreenche na barra de
|
|
190
244
|
* comando, letra por letra, e marca o começo dela na linha do tempo.
|
|
@@ -210,7 +264,7 @@ function step(name, args, result) {
|
|
|
210
264
|
|
|
211
265
|
const troca = nomeDeAba(name, args || {});
|
|
212
266
|
if (troca) entrarNaAba(troca);
|
|
213
|
-
if (!state.abaAtual
|
|
267
|
+
if (!state.abaAtual) entrarNaAba('🖥 Tela');
|
|
214
268
|
|
|
215
269
|
state.passo++;
|
|
216
270
|
if (state.abaAtual) state.abaAtual.passos++;
|
|
@@ -246,11 +300,7 @@ function capturar(entrada) {
|
|
|
246
300
|
if (agora - state.liveAt < 900) {
|
|
247
301
|
try { fs.copyFileSync(LIVE(), arquivo); } catch { return null; }
|
|
248
302
|
state.proximoFrame++;
|
|
249
|
-
|
|
250
|
-
while (state.frames.length > MAX_FRAMES) {
|
|
251
|
-
const velho = state.frames.shift();
|
|
252
|
-
try { fs.unlinkSync(path.join(DIR, `frame-${velho.id}.png`)); } catch {}
|
|
253
|
-
}
|
|
303
|
+
registrarFrame({ id, hora: entrada.hora, acao: entrada.acao, aba: entrada.aba }, arquivo);
|
|
254
304
|
return id;
|
|
255
305
|
}
|
|
256
306
|
|
|
@@ -259,11 +309,7 @@ function capturar(entrada) {
|
|
|
259
309
|
|
|
260
310
|
state.erroFoto = null;
|
|
261
311
|
state.proximoFrame++;
|
|
262
|
-
|
|
263
|
-
while (state.frames.length > MAX_FRAMES) {
|
|
264
|
-
const velho = state.frames.shift();
|
|
265
|
-
try { fs.unlinkSync(path.join(DIR, `frame-${velho.id}.png`)); } catch {}
|
|
266
|
-
}
|
|
312
|
+
registrarFrame({ id, hora: entrada.hora, acao: entrada.acao, aba: entrada.aba }, arquivo);
|
|
267
313
|
return id;
|
|
268
314
|
}
|
|
269
315
|
|
|
@@ -274,11 +320,38 @@ function capturar(entrada) {
|
|
|
274
320
|
|
|
275
321
|
// O mascote do CLI (▐▛███▜▌ …) redesenhado em SVG, célula a célula, na mesma
|
|
276
322
|
// paleta: bloco cheio na cor viva, contorno na apagada.
|
|
277
|
-
//
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
323
|
+
// O mascote: um robozinho PROGRAMADOR. A cabeça é um monitor com o prompt
|
|
324
|
+
// "❯_" na tela (é isso que ele faz: linha de comando), antena com luz de
|
|
325
|
+
// atividade e dois bracinhos que digitam num tecladinho. O anterior era um
|
|
326
|
+
// bloco de pixels sem leitura nenhuma — não dizia "programação", não dizia
|
|
327
|
+
// nada. Olhos, antena, mão e cursor têm classe própria: a animação é CSS.
|
|
328
|
+
const MASCOTE_SVG = `<svg viewBox="0 0 44 40" aria-hidden="true">
|
|
329
|
+
<!-- antena com a luz de atividade -->
|
|
330
|
+
<line x1="22" y1="1.5" x2="22" y2="6" stroke="#0891b2" stroke-width="1.6" stroke-linecap="round"/>
|
|
331
|
+
<circle class="antena" cx="22" cy="2" r="2.3" fill="#22d3ee"/>
|
|
332
|
+
<!-- corpo/monitor -->
|
|
333
|
+
<rect x="4" y="6" width="36" height="24" rx="5" fill="#0e2233" stroke="#22d3ee" stroke-width="1.8"/>
|
|
334
|
+
<rect x="7.5" y="9.5" width="29" height="17" rx="3" fill="#071522"/>
|
|
335
|
+
<!-- olhos -->
|
|
336
|
+
<g fill="#22d3ee">
|
|
337
|
+
<rect class="olho" x="12" y="14" width="5" height="6" rx="2.2"/>
|
|
338
|
+
<rect class="olho" x="27" y="14" width="5" height="6" rx="2.2"/>
|
|
339
|
+
</g>
|
|
340
|
+
<!-- o prompt: ❯ e o cursor piscando (é um programador, afinal) -->
|
|
341
|
+
<path d="M12.5 23.2 l2.4 1.5 -2.4 1.5" fill="none" stroke="#5eead4" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
|
342
|
+
<rect class="cursor" x="17" y="23" width="4.5" height="3.2" rx=".6" fill="#5eead4"/>
|
|
343
|
+
<!-- braços + teclado -->
|
|
344
|
+
<g stroke="#0891b2" stroke-width="1.8" stroke-linecap="round">
|
|
345
|
+
<path class="braco braco1" d="M6 27 L3.5 33"/>
|
|
346
|
+
<path class="braco braco2" d="M38 27 L40.5 33"/>
|
|
347
|
+
</g>
|
|
348
|
+
<rect x="8" y="33" width="28" height="5" rx="2" fill="#12384d" stroke="#0891b2" stroke-width="1.2"/>
|
|
349
|
+
<g fill="#22d3ee" opacity=".85">
|
|
350
|
+
<rect class="tecla t1" x="11" y="35" width="4" height="1.6" rx=".6"/>
|
|
351
|
+
<rect class="tecla t2" x="17" y="35" width="4" height="1.6" rx=".6"/>
|
|
352
|
+
<rect class="tecla t3" x="23" y="35" width="4" height="1.6" rx=".6"/>
|
|
353
|
+
<rect class="tecla t1" x="29" y="35" width="4" height="1.6" rx=".6"/>
|
|
354
|
+
</g>
|
|
282
355
|
</svg>`;
|
|
283
356
|
|
|
284
357
|
// A tela do painel, na identidade do app — a MESMA paleta do design system do
|
|
@@ -288,8 +361,10 @@ const MASCOTE_SVG = `<svg viewBox="0 0 9 3.6" aria-hidden="true">
|
|
|
288
361
|
// embaixo. Dentro da tela roda o VÍDEO ao vivo (o loop de captura acima); as
|
|
289
362
|
// abas em cima são tudo que o agente abriu — clicáveis, cada uma com seus
|
|
290
363
|
// quadros e passos; e a barra de comando autopreenche a instrução digitando.
|
|
364
|
+
// O favicon é o próprio mascote — o viewBox vem dele, não fixo (já ficou
|
|
365
|
+
// errado uma vez quando o desenho mudou de tamanho e o número aqui não).
|
|
291
366
|
const FAVICON = 'data:image/svg+xml,' + encodeURIComponent(
|
|
292
|
-
|
|
367
|
+
MASCOTE_SVG.replace('<svg ', '<svg xmlns="http://www.w3.org/2000/svg" '),
|
|
293
368
|
);
|
|
294
369
|
|
|
295
370
|
const PAGINA = `<!doctype html>
|
|
@@ -308,16 +383,24 @@ const PAGINA = `<!doctype html>
|
|
|
308
383
|
|
|
309
384
|
/* ── Cabeçalho: mascote inclinado + nome, como no desenho ── */
|
|
310
385
|
#topo { position: relative; width: min(1120px, 100%); padding: 44px 0 26px; text-align: center; }
|
|
311
|
-
#topo .mascote { position: absolute; left:
|
|
312
|
-
@keyframes flutua { 0%, 100% { transform: translateY(0) } 50% { transform: translateY(
|
|
386
|
+
#topo .mascote { position: absolute; left: 8px; top: 18px; width: 62px; height: 56px; filter: drop-shadow(0 8px 20px rgba(34,211,238,.35)); animation: flutua 3s ease-in-out infinite; }
|
|
387
|
+
@keyframes flutua { 0%, 100% { transform: translateY(0) } 50% { transform: translateY(5px) } }
|
|
313
388
|
#topo .mascote svg { width: 100%; height: 100%; display: block; overflow: visible; }
|
|
314
|
-
/* vivo: pisca os olhos
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
.mascote .
|
|
319
|
-
|
|
320
|
-
|
|
389
|
+
/* vivo: pisca os olhos, o cursor do prompt pisca, a antena respira e as
|
|
390
|
+
mãozinhas digitam no teclado */
|
|
391
|
+
.mascote .olho { transform-box: fill-box; transform-origin: center; animation: piscaOlho 4s infinite; }
|
|
392
|
+
@keyframes piscaOlho { 0%, 91%, 100% { transform: scaleY(1) } 94%, 96% { transform: scaleY(.1) } }
|
|
393
|
+
.mascote .cursor { animation: piscaCursor 1.05s steps(1) infinite; }
|
|
394
|
+
@keyframes piscaCursor { 50% { opacity: 0 } }
|
|
395
|
+
.mascote .antena { animation: respira 2.2s ease-in-out infinite; }
|
|
396
|
+
@keyframes respira { 0%, 100% { opacity: 1; r: 2.3 } 50% { opacity: .55; r: 1.9 } }
|
|
397
|
+
.mascote .braco { transform-box: fill-box; transform-origin: top center; }
|
|
398
|
+
.mascote .braco1 { animation: digita .62s ease-in-out infinite; }
|
|
399
|
+
.mascote .braco2 { animation: digita .62s ease-in-out infinite; animation-delay: .31s; }
|
|
400
|
+
@keyframes digita { 0%, 100% { transform: translateY(0) } 50% { transform: translateY(1.6px) } }
|
|
401
|
+
.mascote .tecla { animation: teclaAcende 1.24s steps(1) infinite; }
|
|
402
|
+
.mascote .t2 { animation-delay: .31s } .mascote .t3 { animation-delay: .62s }
|
|
403
|
+
@keyframes teclaAcende { 0%, 74% { opacity: .35 } 75%, 99% { opacity: 1 } }
|
|
321
404
|
#topo h1 { font-size: clamp(34px, 5.6vw, 54px); font-weight: 650; letter-spacing: .4px; color: #f1f5f9; text-shadow: 0 4px 28px rgba(34,211,238,.22); }
|
|
322
405
|
#topo h1 span { color: #22d3ee; }
|
|
323
406
|
#topo .vivo { position: absolute; right: 2px; top: 48px; display: inline-flex; align-items: center; gap: 8px; color: #94a3b8; font-size: 12.5px; background: rgba(15,23,42,.85); border: 1px solid #1e293b; border-radius: 999px; padding: 6px 14px; }
|
|
@@ -645,6 +728,7 @@ function start() {
|
|
|
645
728
|
// primeiro quadro está no ar — nada de tela vazia esperando ação.
|
|
646
729
|
state.viewerAt = Date.now();
|
|
647
730
|
agendarVideo(50);
|
|
731
|
+
armarFaxina();
|
|
648
732
|
resolve(state.url);
|
|
649
733
|
});
|
|
650
734
|
});
|
|
@@ -656,6 +740,20 @@ function stop() {
|
|
|
656
740
|
state.server = null;
|
|
657
741
|
state.on = false;
|
|
658
742
|
state.url = '';
|
|
743
|
+
limparDisco(); // desligou o painel, as fotos vão junto
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
// Fechou o PrimoCode (/exit, Ctrl+C, Ctrl+D): nada de print esquecido no
|
|
747
|
+
// disco. `exit` cobre a saída normal; os sinais cobrem o resto.
|
|
748
|
+
let faxinaArmada = false;
|
|
749
|
+
function armarFaxina() {
|
|
750
|
+
if (faxinaArmada) return;
|
|
751
|
+
faxinaArmada = true;
|
|
752
|
+
const faxina = () => { try { limparDisco(); } catch {} };
|
|
753
|
+
process.once('exit', faxina);
|
|
754
|
+
for (const sinal of ['SIGINT', 'SIGTERM', 'SIGHUP']) {
|
|
755
|
+
process.once(sinal, () => { faxina(); process.exit(0); });
|
|
756
|
+
}
|
|
659
757
|
}
|
|
660
758
|
|
|
661
|
-
module.exports = { start, stop, step, instrucao, isOn, url, descrever };
|
|
759
|
+
module.exports = { start, stop, step, instrucao, isOn, url, descrever, limparDisco };
|
package/lib/tools.js
CHANGED
|
@@ -13,6 +13,7 @@ const os = require('os');
|
|
|
13
13
|
const { exec } = require('child_process');
|
|
14
14
|
const browser = require('./browser');
|
|
15
15
|
const desktop = require('./desktop');
|
|
16
|
+
const referencia = require('./referencia');
|
|
16
17
|
|
|
17
18
|
const IGNORAR = new Set(['node_modules', '.git', 'dist', 'build', '.next', 'coverage', '.cache', 'vendor']);
|
|
18
19
|
const MAX_ARQUIVOS_VARREDURA = 5000;
|
|
@@ -388,6 +389,15 @@ async function despachar(name, args, ctx) {
|
|
|
388
389
|
case 'write_todos':
|
|
389
390
|
return writeTodos(args);
|
|
390
391
|
|
|
392
|
+
// Pesquisa de referência: abre documentação e exemplos sobre o
|
|
393
|
+
// pedido e devolve o TEXTO para o modelo aprender antes de
|
|
394
|
+
// escrever. Aparece no terminal e no /streaming — nada oculto.
|
|
395
|
+
case 'pesquisar_referencia':
|
|
396
|
+
return referencia.pesquisar({
|
|
397
|
+
consulta: args.consulta || args.query || '',
|
|
398
|
+
paginas: Math.max(1, Math.min(5, Number(args.paginas) || 3)),
|
|
399
|
+
});
|
|
400
|
+
|
|
391
401
|
// ── Navegador controlado (CDP) — o agente age numa página real ──
|
|
392
402
|
case 'browser_open': {
|
|
393
403
|
const alvo = /^https?:\/\//i.test(args.url || '')
|
package/lib/ui.js
CHANGED
|
@@ -157,13 +157,13 @@ const NOME = 'Primo Code';
|
|
|
157
157
|
const TIPS = [
|
|
158
158
|
'Peça um app novo, ou aponte para um repositório e mande clonar',
|
|
159
159
|
'/dir <pasta> para ele trabalhar numa pasta que já é sua',
|
|
160
|
-
'/help lista tudo · /
|
|
160
|
+
'/help lista tudo · /effort ajusta o esforço · /streaming mostra ao vivo',
|
|
161
161
|
];
|
|
162
162
|
|
|
163
163
|
const NEWS = [
|
|
164
|
-
'/
|
|
165
|
-
'
|
|
166
|
-
'
|
|
164
|
+
'/effort escolhe o esforço num slider — do rápido ao mais esperto',
|
|
165
|
+
'Do "high" para cima ele PESQUISA referências antes de escrever',
|
|
166
|
+
'Esc Esc para o agente · /streaming mostra tudo ao vivo, e limpa os prints',
|
|
167
167
|
];
|
|
168
168
|
|
|
169
169
|
// ── Cabeçalho ────────────────────────────────────────────────────────────
|
|
@@ -302,7 +302,7 @@ function actionHeader(name, args) {
|
|
|
302
302
|
read_file:'Read', write_file:'Write', edit_file:'Edit', multi_edit:'MultiEdit',
|
|
303
303
|
list_dir:'List', glob:'Glob', grep:'Grep', run_shell:'Bash',
|
|
304
304
|
run_in_new_terminal:'Terminal', open_in_browser:'Open', spawn_agent:'Agent',
|
|
305
|
-
write_todos:'Plano', finish:'Finish',
|
|
305
|
+
write_todos:'Plano', finish:'Finish', pesquisar_referencia:'Pesquisa',
|
|
306
306
|
browser_open:'Browser', browser_click:'Browser·click', browser_type:'Browser·type',
|
|
307
307
|
browser_key:'Browser·key', browser_read:'Browser·read', browser_screenshot:'Browser·shot',
|
|
308
308
|
browser_scroll:'Browser·scroll', browser_wait:'Browser·wait', browser_eval:'Browser·eval',
|
|
@@ -314,7 +314,7 @@ function actionHeader(name, args) {
|
|
|
314
314
|
desktop_elements:'Desktop·elementos', desktop_click_element:'Desktop·click',
|
|
315
315
|
};
|
|
316
316
|
const label = nice[name] || name;
|
|
317
|
-
const argStr = args ? (args.path || args.pattern || args.command || args.target || args.url || args.selector || args.text || args.app || args.window || args.name || args.description || (args.todos ? `${args.todos.length} tarefas` : '') || (args.x != null ? `${args.x},${args.y}` : '') || args.key || JSON.stringify(args).slice(0,60)) : '';
|
|
317
|
+
const argStr = args ? (args.consulta || args.path || args.pattern || args.command || args.target || args.url || args.selector || args.text || args.app || args.window || args.name || args.description || (args.todos ? `${args.todos.length} tarefas` : '') || (args.x != null ? `${args.x},${args.y}` : '') || args.key || JSON.stringify(args).slice(0,60)) : '';
|
|
318
318
|
return ` ${c.dim('⎿')} ${c.brand(label)}${argStr ? ' '+c.muted(argStr) : ''}`;
|
|
319
319
|
}
|
|
320
320
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "primocode",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.16.0",
|
|
4
4
|
"description": "PrimoCode — agente de engenharia com IA e cursor próprio. Modelos grátis. 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/modo.test.js && node test/pasta.test.js && node test/catalogo.test.js && node test/desktop.test.js"
|
|
11
|
+
"test": "node test/tools.test.js && node test/ui.test.js && node test/modo.test.js && node test/pasta.test.js && node test/catalogo.test.js && node test/desktop.test.js && node test/effort.test.js"
|
|
12
12
|
},
|
|
13
13
|
"engines": {
|
|
14
14
|
"node": ">=18.17.0"
|