primocode 7.2.0 → 7.4.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/README.md +124 -7
- package/bin/primocode.js +54 -5
- package/lib/act.js +177 -40
- package/lib/browser.js +507 -492
- package/lib/config.js +5 -2
- package/lib/desktop.js +189 -142
- package/lib/tools.js +456 -147
- package/lib/ui.js +237 -172
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -1,16 +1,35 @@
|
|
|
1
|
-
# PrimoCode v7.
|
|
1
|
+
# PrimoCode v7.4.0
|
|
2
2
|
|
|
3
3
|
Agente de engenharia brasileiro para o terminal, no estilo Claude Code.
|
|
4
|
-
Cria arquivos de verdade, roda comandos,
|
|
5
|
-
entre uma sessão e outra.
|
|
4
|
+
Cria arquivos de verdade, roda comandos, **controla o navegador e o desktop** —
|
|
5
|
+
e lembra do que fez entre uma sessão e outra.
|
|
6
6
|
|
|
7
7
|
```
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
╭◠╮
|
|
9
|
+
╭─┴─┴─╮
|
|
10
|
+
◖┫ ● ● ┣◗ PrimoCode v7.4.0
|
|
11
|
+
┃ ‿ ┃ projeto: jogo-de-carro
|
|
12
|
+
╰──┬──╯
|
|
13
|
+
╰─┴─╯
|
|
12
14
|
```
|
|
13
15
|
|
|
16
|
+
Personalize o mascote (`primo`, `fox`, `cat`) e o nome com que ele te chama —
|
|
17
|
+
veja `/mascot` e `/name`.
|
|
18
|
+
|
|
19
|
+
## Por que trocar o Claude Code pelo PrimoCode
|
|
20
|
+
|
|
21
|
+
| | PrimoCode | Assistentes gringos de terminal |
|
|
22
|
+
|---|---|---|
|
|
23
|
+
| **Idioma** | Nasceu em português do Brasil — fala como gente, não como manual traduzido | Inglês primeiro; PT-BR é tradução |
|
|
24
|
+
| **Chave de API** | **Não precisa.** Instala e usa — o modelo já vem hospedado | Exige conta e chave de API própria |
|
|
25
|
+
| **Controla a máquina** | Navegador (Chrome real) **e** desktop: mouse, teclado, tela | Em geral só arquivos e shell |
|
|
26
|
+
| **Memória** | Cada projeto lembra o que foi feito, entre sessões | Esquece ao fechar |
|
|
27
|
+
| **Instalação** | Node puro, **zero dependências** — segundos | Pacotes e dependências |
|
|
28
|
+
| **Cara própria** | Mascote, nome e temas que você escolhe | Fixo |
|
|
29
|
+
| **Custo** | Roda em modelos rápidos e baratos (Groq/Llama) | Cobrança por token da API |
|
|
30
|
+
|
|
31
|
+
Em uma frase: **é o poder de um agente de código no terminal, em português, controlando o seu computador de verdade — sem você precisar configurar chave de API nenhuma.**
|
|
32
|
+
|
|
14
33
|
## Instalação
|
|
15
34
|
|
|
16
35
|
```bash
|
|
@@ -61,6 +80,101 @@ fim. É por isso que a IA não esquece o projeto entre sessões.
|
|
|
61
80
|
|
|
62
81
|
Alterne com `/mode primocode` ou `/mode chat`.
|
|
63
82
|
|
|
83
|
+
## Ferramentas do agente
|
|
84
|
+
|
|
85
|
+
| Tool | Para quê |
|
|
86
|
+
|---|---|
|
|
87
|
+
| `write_file` | Cria arquivo novo ou reescreve inteiro |
|
|
88
|
+
| `edit_file` | **Troca um trecho exato.** É o que evita reescrever 800 linhas pra mudar uma |
|
|
89
|
+
| `multi_edit` | Várias trocas no mesmo arquivo, em ordem, tudo-ou-nada |
|
|
90
|
+
| `read_file` | Lê o arquivo, com `offset`/`limit` para os grandes |
|
|
91
|
+
| `glob` | Acha arquivos por padrão: `**/*.js`, `src/**/*.{ts,tsx}` |
|
|
92
|
+
| `grep` | Procura texto/regex **dentro** dos arquivos, com arquivo e linha |
|
|
93
|
+
| `list_dir` | Árvore de arquivos |
|
|
94
|
+
| `run_shell` | Roda comando na pasta do projeto |
|
|
95
|
+
| `run_in_new_terminal` | Sobe servidor de dev / watch em outro terminal |
|
|
96
|
+
| `write_todos` | Plano de tarefas da sessão, visível no terminal |
|
|
97
|
+
| `open_in_browser` | Abre HTML ou URL no navegador padrão |
|
|
98
|
+
| `spawn_agent` | Delega uma sub-tarefa a outro agente |
|
|
99
|
+
|
|
100
|
+
O `edit_file` é rigoroso de propósito: se o trecho não existe, ou aparece mais
|
|
101
|
+
de uma vez sem `replace_all`, ele **recusa** e devolve as linhas reais do
|
|
102
|
+
arquivo — em vez de editar o lugar errado.
|
|
103
|
+
|
|
104
|
+
### Controle de navegador e desktop
|
|
105
|
+
|
|
106
|
+
O agente não só abre o navegador — ele **age** nele, e no computador também.
|
|
107
|
+
|
|
108
|
+
| Tool | Para quê |
|
|
109
|
+
|---|---|
|
|
110
|
+
| `browser_open` | Abre a página num Chrome controlado (CDP) e espera carregar |
|
|
111
|
+
| `browser_click` | Clica por texto visível, seletor CSS ou coordenada |
|
|
112
|
+
| `browser_type` | Digita num campo (por seletor) ou onde o foco estiver |
|
|
113
|
+
| `browser_key` | Enter, Tab, Escape, setas… |
|
|
114
|
+
| `browser_read` | Lê o texto visível da página (conferir o resultado) |
|
|
115
|
+
| `browser_screenshot` | Print da página, salvo no projeto |
|
|
116
|
+
| `browser_scroll` / `browser_wait` / `browser_eval` | Rolar, esperar seletor, rodar JS |
|
|
117
|
+
| `desktop_screenshot` | Print da tela inteira do usuário |
|
|
118
|
+
| `desktop_click` / `desktop_move` / `desktop_drag` | Mouse na tela real |
|
|
119
|
+
| `desktop_type` / `desktop_key` / `desktop_scroll` | Teclado e rolagem do sistema |
|
|
120
|
+
| `desktop_size` | Resolução da tela, para calcular coordenadas |
|
|
121
|
+
|
|
122
|
+
O navegador roda via Chrome DevTools Protocol (sem dependências npm).
|
|
123
|
+
|
|
124
|
+
**Requisitos por plataforma** (as ferramentas dão um erro claro se faltar algo):
|
|
125
|
+
|
|
126
|
+
| | Navegador | Desktop (mouse/teclado) | Screenshot da tela |
|
|
127
|
+
|---|---|---|---|
|
|
128
|
+
| **Windows** | Chrome ou Edge | nativo (PowerShell) | nativo (PowerShell) |
|
|
129
|
+
| **macOS** | Chrome/Chromium | `brew install cliclick` | nativo (`screencapture`) |
|
|
130
|
+
| **Linux** | Chrome/Chromium | `apt install xdotool` | `scrot`, `gnome-screenshot`, `spectacle`, `maim` ou ImageMagick |
|
|
131
|
+
|
|
132
|
+
Variáveis de ambiente úteis:
|
|
133
|
+
- `CHROME_PATH` / `PRIMOCODE_CHROME` — aponta para um Chrome fora do caminho padrão
|
|
134
|
+
- `PRIMOCODE_BROWSER_HEADLESS=1` — roda o navegador sem janela (CI/servidor)
|
|
135
|
+
|
|
136
|
+
## Subagentes
|
|
137
|
+
|
|
138
|
+
Para tarefas com partes independentes, o agente delega:
|
|
139
|
+
|
|
140
|
+
```
|
|
141
|
+
Primo Code site ❯ crie o visual do site
|
|
142
|
+
|
|
143
|
+
◆ Agente criar o CSS
|
|
144
|
+
⎿ Write style.css
|
|
145
|
+
⎿ criado · 40 B
|
|
146
|
+
◆ Criei style.css com fundo #111
|
|
147
|
+
|
|
148
|
+
Pronto: CSS delegado e criado
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
O subagente tem conversa própria — **não vê o seu diálogo** — e devolve só o
|
|
152
|
+
resumo final. Ele não pode criar outro subagente, então a árvore nunca cresce
|
|
153
|
+
sem controle.
|
|
154
|
+
|
|
155
|
+
## Nunca "pronto!" falso
|
|
156
|
+
|
|
157
|
+
Se uma ferramenta falha e o agente tenta encerrar dizendo que deu certo, o
|
|
158
|
+
PrimoCode barra e devolve a tarefa para ele:
|
|
159
|
+
|
|
160
|
+
```
|
|
161
|
+
⎿ Edit app.js
|
|
162
|
+
⎿ Trecho não encontrado
|
|
163
|
+
→ agente tenta finish("Porta alterada")
|
|
164
|
+
→ BARRADO: "NÃO terminou. Estas tools falharam..."
|
|
165
|
+
⎿ Edit app.js
|
|
166
|
+
⎿ 1 troca · ±0 linhas ← agora foi de verdade
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## Testes
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
npm test
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
25 verificações sobre glob, grep, edit_file, leitura por partes e bloqueio de
|
|
176
|
+
caminho fora do projeto.
|
|
177
|
+
|
|
64
178
|
## Uso
|
|
65
179
|
|
|
66
180
|
```bash
|
|
@@ -83,6 +197,9 @@ primocode -f app.js "onde está o bug?" # com arquivo anexado
|
|
|
83
197
|
| `/run <comando>` | Executa no shell, dentro da pasta do projeto |
|
|
84
198
|
| `/open <arquivo\|url>` | Abre no navegador |
|
|
85
199
|
| `/status` | Health check do servidor |
|
|
200
|
+
| `/name <nome>` | Como o Primo te chama na saudação |
|
|
201
|
+
| `/mascot primo\|fox\|cat` | Escolhe o mascote (mostra os três) |
|
|
202
|
+
| `/config` | Mostra suas preferências e onde ficam salvas |
|
|
86
203
|
| `/clear` · `/cls` | Limpa conversa / tela |
|
|
87
204
|
| `/uninstall` | Remove o PrimoCode (seus projetos ficam) |
|
|
88
205
|
| `/exit` | Sair |
|
package/bin/primocode.js
CHANGED
|
@@ -13,7 +13,8 @@ const fs = require('fs');
|
|
|
13
13
|
const os = require('os');
|
|
14
14
|
|
|
15
15
|
const pkg = require('../package.json');
|
|
16
|
-
const
|
|
16
|
+
const ui = require('../lib/ui');
|
|
17
|
+
const { c, welcomeBox, Spinner } = ui;
|
|
17
18
|
const api = require('../lib/api');
|
|
18
19
|
const tools = require('../lib/tools');
|
|
19
20
|
const cfg = require('../lib/config');
|
|
@@ -35,6 +36,7 @@ const HELP = `
|
|
|
35
36
|
${c.bold('Comandos')}
|
|
36
37
|
${c.brand('/project')} lista projetos ou muda: /project <nome>
|
|
37
38
|
${c.brand('/mode')} alterna: /mode primocode|chat
|
|
39
|
+
${c.brand('/model')} alterna: /model main|fast
|
|
38
40
|
${c.brand('/attach')} <arquivo> anexa arquivo ao próximo pedido
|
|
39
41
|
${c.brand('/files')} lista anexos ${c.brand('/detach')} remove
|
|
40
42
|
${c.brand('/tree')} [n] árvore do projeto
|
|
@@ -42,6 +44,9 @@ ${c.bold('Comandos')}
|
|
|
42
44
|
${c.brand('/run')} <comando> executa no shell do projeto
|
|
43
45
|
${c.brand('/open')} <arquivo|url> abre no navegador
|
|
44
46
|
${c.brand('/status')} checa o servidor
|
|
47
|
+
${c.brand('/name')} <nome> como o Primo te chama
|
|
48
|
+
${c.brand('/mascot')} <estilo> mascote: primo|fox|cat
|
|
49
|
+
${c.brand('/config')} mostra suas preferências
|
|
45
50
|
${c.brand('/clear')} limpa a conversa ${c.brand('/cls')} limpa a tela
|
|
46
51
|
${c.brand('/uninstall')} remove o PrimoCode
|
|
47
52
|
${c.brand('/exit')} sair
|
|
@@ -178,7 +183,7 @@ async function runChat(promptForModel, userTextForMemory) {
|
|
|
178
183
|
model: state.config.model,
|
|
179
184
|
token: state.config.token,
|
|
180
185
|
}, (evt) => {
|
|
181
|
-
if (evt.erro) { if (!started) sp.stop(); console.log(c.err(
|
|
186
|
+
if (evt.erro) { if (!started) sp.stop(); console.log('\n' + c.err(ui.friendlyError(evt.erro))); return; }
|
|
182
187
|
if (evt.delta) {
|
|
183
188
|
if (!started) { sp.stop(); process.stdout.write('\n'); started = true; }
|
|
184
189
|
text += evt.delta;
|
|
@@ -188,7 +193,7 @@ async function runChat(promptForModel, userTextForMemory) {
|
|
|
188
193
|
});
|
|
189
194
|
} catch (e) {
|
|
190
195
|
sp.stop();
|
|
191
|
-
console.log(c.err(
|
|
196
|
+
console.log('\n' + c.err(ui.friendlyError(e)));
|
|
192
197
|
return;
|
|
193
198
|
}
|
|
194
199
|
sp.stop();
|
|
@@ -294,6 +299,47 @@ async function handleSlash(text) {
|
|
|
294
299
|
break;
|
|
295
300
|
}
|
|
296
301
|
|
|
302
|
+
case 'name': {
|
|
303
|
+
if (!rest) { console.log(c.muted(` Nome atual: ${state.config.userName || '(nenhum)'}`)); break; }
|
|
304
|
+
state.config.userName = rest;
|
|
305
|
+
cfg.saveConfig({ userName: rest });
|
|
306
|
+
console.log(c.ok(`Feito! Vou te chamar de ${rest}.`));
|
|
307
|
+
break;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
case 'mascot': {
|
|
311
|
+
const estilos = ['primo', 'fox', 'cat'];
|
|
312
|
+
const m = rest.toLowerCase();
|
|
313
|
+
if (!m) {
|
|
314
|
+
console.log('');
|
|
315
|
+
for (const e of estilos) {
|
|
316
|
+
console.log(` ${e === state.config.mascot ? c.brand('● ' + e) : c.muted('○ ' + e)}`);
|
|
317
|
+
ui.mascot(e).forEach((l) => console.log(' ' + l));
|
|
318
|
+
console.log('');
|
|
319
|
+
}
|
|
320
|
+
console.log(c.muted(' Escolha: /mascot <estilo>'));
|
|
321
|
+
break;
|
|
322
|
+
}
|
|
323
|
+
if (!estilos.includes(m)) { console.log(c.err(`Uso: /mascot ${estilos.join('|')}`)); break; }
|
|
324
|
+
state.config.mascot = m;
|
|
325
|
+
cfg.saveConfig({ mascot: m });
|
|
326
|
+
console.log('');
|
|
327
|
+
ui.mascot(m).forEach((l) => console.log(' ' + l));
|
|
328
|
+
console.log(c.ok(`\nMascote: ${m}`));
|
|
329
|
+
break;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
case 'config': {
|
|
333
|
+
console.log('');
|
|
334
|
+
console.log(` ${c.muted('nome')} ${c.white(state.config.userName || '(nenhum)')}`);
|
|
335
|
+
console.log(` ${c.muted('mascote')} ${c.white(state.config.mascot || 'primo')}`);
|
|
336
|
+
console.log(` ${c.muted('modelo')} ${c.white(state.config.model)}`);
|
|
337
|
+
console.log(` ${c.muted('modo')} ${c.white(state.mode)}`);
|
|
338
|
+
console.log(` ${c.muted('servidor')} ${c.dim(state.config.server)}`);
|
|
339
|
+
console.log(c.muted(`\n ${cfg.CONFIG_FILE}`));
|
|
340
|
+
break;
|
|
341
|
+
}
|
|
342
|
+
|
|
297
343
|
case 'clear': state.history = []; console.log(c.ok('Conversa limpa.')); break;
|
|
298
344
|
case 'cls': process.stdout.write('\x1Bc'); break;
|
|
299
345
|
|
|
@@ -373,7 +419,10 @@ async function main() {
|
|
|
373
419
|
process.exit(0);
|
|
374
420
|
}
|
|
375
421
|
|
|
376
|
-
console.log(welcomeBox(VERSION, state.workDir ? shortPath(state.workDir) : null
|
|
422
|
+
console.log(welcomeBox(VERSION, state.workDir ? shortPath(state.workDir) : null, {
|
|
423
|
+
mascot: state.config.mascot,
|
|
424
|
+
userName: state.config.userName,
|
|
425
|
+
}));
|
|
377
426
|
rl = readline.createInterface({ input: process.stdin, output: process.stdout, prompt: makePrompt() });
|
|
378
427
|
rl.prompt();
|
|
379
428
|
|
|
@@ -400,7 +449,7 @@ async function main() {
|
|
|
400
449
|
if (isSlash(text)) await handleSlash(text);
|
|
401
450
|
else await sendPrompt(text);
|
|
402
451
|
} catch (e) {
|
|
403
|
-
console.log(c.err(
|
|
452
|
+
console.log('\n' + c.err(ui.friendlyError(e)));
|
|
404
453
|
}
|
|
405
454
|
}
|
|
406
455
|
busy = false;
|
package/lib/act.js
CHANGED
|
@@ -1,52 +1,124 @@
|
|
|
1
1
|
// Loop do agente autônomo — streaming via /api/act/stream.
|
|
2
2
|
//
|
|
3
3
|
// O contrato com o servidor é uma conversa completa (`messages`, estilo OpenAI).
|
|
4
|
-
// Cada volta
|
|
5
|
-
//
|
|
6
|
-
// a mesma tool até estourar o limite.
|
|
4
|
+
// Cada volta: o servidor devolve tool_calls -> executamos AQUI, com await, e
|
|
5
|
+
// devolvemos o resultado na volta seguinte. Sem isso o modelo fica cego e
|
|
6
|
+
// repete a mesma tool até estourar o limite.
|
|
7
|
+
//
|
|
8
|
+
// spawn_agent roda este mesmo loop recursivamente, com conversa própria: o
|
|
9
|
+
// subagente não enxerga o diálogo do usuário e devolve só o resumo final.
|
|
7
10
|
|
|
8
11
|
'use strict';
|
|
9
12
|
|
|
10
|
-
const { c, actionHeader, Spinner } = require('./ui');
|
|
13
|
+
const { c, actionHeader, Spinner, friendlyError } = require('./ui');
|
|
11
14
|
const tools = require('./tools');
|
|
12
15
|
const api = require('./api');
|
|
13
16
|
const cfg = require('./config');
|
|
14
17
|
|
|
15
18
|
const MAX_ITERATIONS = 15;
|
|
19
|
+
const MAX_PROFUNDIDADE = 2; // principal + 1 nível de subagente
|
|
20
|
+
|
|
21
|
+
function ident(profundidade) {
|
|
22
|
+
return ' '.repeat(profundidade);
|
|
23
|
+
}
|
|
16
24
|
|
|
17
|
-
function logResult(kind, result) {
|
|
18
|
-
const
|
|
25
|
+
function logResult(kind, result, profundidade) {
|
|
26
|
+
const p = ident(profundidade) + c.muted(' ⎿ ');
|
|
19
27
|
if (!result) return;
|
|
20
|
-
if (result.ok === false) { console.log(
|
|
28
|
+
if (result.ok === false) { console.log(p + c.err(result.error || `exit ${result.code ?? 'erro'}`)); return; }
|
|
21
29
|
|
|
22
30
|
switch (kind) {
|
|
23
31
|
case 'write_file':
|
|
24
|
-
console.log(
|
|
32
|
+
console.log(p + c.muted(`${result.created ? 'criado' : 'atualizado'} · ${tools.fmtBytes(result.bytes)}`));
|
|
25
33
|
return;
|
|
34
|
+
case 'edit_file': {
|
|
35
|
+
const delta = result.linhasDepois - result.linhasAntes;
|
|
36
|
+
const sinal = delta > 0 ? c.ok(`+${delta}`) : delta < 0 ? c.err(`${delta}`) : c.muted('±0');
|
|
37
|
+
console.log(p + c.muted(`${result.substituicoes} troca${result.substituicoes > 1 ? 's' : ''} · `) + sinal + c.muted(' linhas'));
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
26
40
|
case 'read_file':
|
|
27
|
-
console.log(
|
|
41
|
+
console.log(p + c.muted(result.de
|
|
42
|
+
? `linhas ${result.de}–${result.ate} de ${result.linhas}`
|
|
43
|
+
: `${result.linhas} linhas · ${tools.fmtBytes(result.bytes || 0)}${result.truncado ? ' (cortado)' : ''}`));
|
|
28
44
|
return;
|
|
29
45
|
case 'list_dir': {
|
|
30
|
-
const
|
|
31
|
-
for (const l of
|
|
32
|
-
if (
|
|
46
|
+
const linhas = (result.tree || '').split('\n').filter(Boolean);
|
|
47
|
+
for (const l of linhas.slice(0, 8)) console.log(p + c.muted(l));
|
|
48
|
+
if (linhas.length > 8) console.log(p + c.muted(`… +${linhas.length - 8}`));
|
|
33
49
|
return;
|
|
34
50
|
}
|
|
35
|
-
case '
|
|
36
|
-
console.log(
|
|
51
|
+
case 'glob':
|
|
52
|
+
console.log(p + c.muted(`${result.total} arquivo${result.total === 1 ? '' : 's'}`));
|
|
53
|
+
for (const f of (result.arquivos || []).slice(0, 5)) console.log(p + c.dim(f));
|
|
54
|
+
if ((result.arquivos || []).length > 5) console.log(p + c.dim(`… +${result.arquivos.length - 5}`));
|
|
37
55
|
return;
|
|
56
|
+
case 'grep': {
|
|
57
|
+
console.log(p + c.muted(`${result.total} ocorrência${result.total === 1 ? '' : 's'} em ${result.arquivosVistos} arquivos`));
|
|
58
|
+
for (const r of (result.resultados || []).slice(0, 5)) {
|
|
59
|
+
console.log(p + c.dim(`${r.arquivo}:${r.linha}`));
|
|
60
|
+
}
|
|
61
|
+
if ((result.resultados || []).length > 5) console.log(p + c.dim(`… +${result.resultados.length - 5}`));
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
case 'run_shell': {
|
|
65
|
+
const saida = (result.stdout || '').trim().split('\n').filter(Boolean);
|
|
66
|
+
for (const l of saida.slice(0, 6)) console.log(p + c.muted(l.slice(0, 160)));
|
|
67
|
+
if (saida.length > 6) console.log(p + c.muted(`… +${saida.length - 6} linhas`));
|
|
68
|
+
if (!saida.length) console.log(p + c.ok('exit 0'));
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
38
71
|
case 'open_in_browser':
|
|
39
|
-
|
|
40
|
-
|
|
72
|
+
console.log(p + c.muted(result.opener || 'aberto'));
|
|
73
|
+
return;
|
|
74
|
+
case 'multi_edit': {
|
|
75
|
+
const delta = result.linhasDepois - result.linhasAntes;
|
|
76
|
+
const sinal = delta > 0 ? c.ok(`+${delta}`) : delta < 0 ? c.err(`${delta}`) : c.muted('±0');
|
|
77
|
+
console.log(p + c.muted(`${result.substituicoes} trocas · `) + sinal + c.muted(' linhas'));
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
case 'write_todos': {
|
|
81
|
+
const marca = { completed: c.ok('✔'), in_progress: c.brand('▶'), pending: c.dim('○') };
|
|
82
|
+
for (const t of result.todos || []) {
|
|
83
|
+
const texto = t.status === 'completed' ? c.dim(t.content) : c.white(t.content);
|
|
84
|
+
console.log(p + `${marca[t.status] || c.dim('○')} ${texto}`);
|
|
85
|
+
}
|
|
86
|
+
console.log(p + c.muted(`${result.feitos}/${result.total} concluídas`));
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
case 'browser_open':
|
|
90
|
+
console.log(p + c.muted(result.url || 'aberto'));
|
|
91
|
+
return;
|
|
92
|
+
case 'browser_read':
|
|
93
|
+
console.log(p + c.muted(`${(result.text || '').length} caracteres lidos da página`));
|
|
94
|
+
return;
|
|
95
|
+
case 'browser_screenshot':
|
|
96
|
+
case 'desktop_screenshot':
|
|
97
|
+
console.log(p + c.muted(`${result.path} · ${tools.fmtBytes(result.bytes || 0)}`));
|
|
98
|
+
return;
|
|
99
|
+
case 'browser_click':
|
|
100
|
+
case 'browser_type':
|
|
101
|
+
case 'browser_key':
|
|
102
|
+
case 'browser_scroll':
|
|
103
|
+
case 'browser_wait':
|
|
104
|
+
case 'browser_eval':
|
|
105
|
+
case 'desktop_click':
|
|
106
|
+
case 'desktop_move':
|
|
107
|
+
case 'desktop_drag':
|
|
108
|
+
case 'desktop_type':
|
|
109
|
+
case 'desktop_key':
|
|
110
|
+
case 'desktop_scroll':
|
|
111
|
+
case 'desktop_size':
|
|
112
|
+
console.log(p + c.ok('ok') + (result.text || result.tag || result.selector || result.width ? c.muted(' · ' + (result.selector || result.tag || (result.width ? `${result.width}×${result.height}` : ''))) : ''));
|
|
41
113
|
return;
|
|
42
114
|
}
|
|
43
115
|
}
|
|
44
116
|
|
|
45
|
-
// O que
|
|
46
|
-
//
|
|
117
|
+
// O que volta para o modelo. Cortamos saídas gigantes para não estourar a
|
|
118
|
+
// janela de contexto na iteração seguinte.
|
|
47
119
|
function summarizeForModel(result) {
|
|
48
120
|
const json = JSON.stringify(result);
|
|
49
|
-
return json.length >
|
|
121
|
+
return json.length > 6000 ? json.slice(0, 6000) + '…(truncado)' : json;
|
|
50
122
|
}
|
|
51
123
|
|
|
52
124
|
function buildMemoryContext(projectDir, projectName) {
|
|
@@ -59,24 +131,31 @@ function buildMemoryContext(projectDir, projectName) {
|
|
|
59
131
|
}
|
|
60
132
|
|
|
61
133
|
async function runAct(userPrompt, opts) {
|
|
62
|
-
const {
|
|
134
|
+
const {
|
|
135
|
+
server, model, token, history, projectDir, projectName,
|
|
136
|
+
ensureWorkDir, profundidade = 0, subagent = false,
|
|
137
|
+
} = opts || {};
|
|
138
|
+
|
|
63
139
|
// A pasta só nasce quando o agente for mesmo mexer em arquivo. Um "oi"
|
|
64
140
|
// não deve deixar diretório vazio espalhado pelo disco.
|
|
65
141
|
let workDirReady = !ensureWorkDir;
|
|
142
|
+
const falhasPendentes = [];
|
|
143
|
+
let tentativasDeEncerrar = 0;
|
|
66
144
|
|
|
67
|
-
// Conversa completa desta tarefa: histórico recente + memória + pedido.
|
|
68
145
|
const messages = [];
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
146
|
+
if (!subagent) {
|
|
147
|
+
const memory = buildMemoryContext(projectDir, projectName);
|
|
148
|
+
if (memory) messages.push({ role: 'system', content: `Memória persistente:\n\n${memory}` });
|
|
149
|
+
for (const m of (history || []).slice(-20)) {
|
|
150
|
+
if (m && m.role && m.content) messages.push({ role: m.role, content: m.content });
|
|
151
|
+
}
|
|
73
152
|
}
|
|
74
153
|
messages.push({ role: 'user', content: userPrompt });
|
|
75
154
|
|
|
76
155
|
let finalText = '';
|
|
77
156
|
|
|
78
157
|
for (let iteration = 0; iteration < MAX_ITERATIONS; iteration++) {
|
|
79
|
-
const sp = new Spinner('Primo Code pensando').start();
|
|
158
|
+
const sp = new Spinner(subagent ? 'subagente trabalhando' : 'Primo Code pensando').start();
|
|
80
159
|
|
|
81
160
|
const toolCalls = [];
|
|
82
161
|
let assistantText = '';
|
|
@@ -84,40 +163,46 @@ async function runAct(userPrompt, opts) {
|
|
|
84
163
|
let streaming = false;
|
|
85
164
|
|
|
86
165
|
try {
|
|
87
|
-
await api.requestAct(server, { messages, model, token, projectDir, projectName }, (evt) => {
|
|
166
|
+
await api.requestAct(server, { messages, model, token, projectDir, projectName, subagent }, (evt) => {
|
|
88
167
|
if (evt.erro) { streamError = evt.erro; return; }
|
|
89
168
|
if (evt.tool_call) { toolCalls.push(evt.tool_call); return; }
|
|
90
169
|
if (evt.delta) {
|
|
91
170
|
if (!streaming) { sp.stop(); streaming = true; }
|
|
92
171
|
assistantText += evt.delta;
|
|
93
|
-
process.stdout.write(c.white(evt.delta));
|
|
172
|
+
if (!subagent) process.stdout.write(c.white(evt.delta));
|
|
94
173
|
}
|
|
95
174
|
if (evt.assistant_message) assistantText = evt.assistant_message.content || assistantText;
|
|
96
175
|
});
|
|
97
176
|
} catch (e) {
|
|
98
177
|
sp.stop();
|
|
99
|
-
|
|
178
|
+
const amigavel = friendlyError(e);
|
|
179
|
+
if (subagent) return { assistantMessage: { role: 'assistant', content: `Subagente falhou: ${amigavel}` } };
|
|
180
|
+
console.log('\n' + c.err(amigavel));
|
|
100
181
|
throw e;
|
|
101
182
|
}
|
|
102
183
|
sp.stop();
|
|
103
184
|
|
|
104
185
|
if (streamError) {
|
|
105
|
-
|
|
106
|
-
return { assistantMessage: { role: 'assistant', content: `
|
|
186
|
+
const amigavel = friendlyError(streamError);
|
|
187
|
+
if (subagent) return { assistantMessage: { role: 'assistant', content: `Subagente falhou: ${amigavel}` } };
|
|
188
|
+
console.log('\n' + c.err(amigavel));
|
|
189
|
+
return { assistantMessage: { role: 'assistant', content: amigavel } };
|
|
107
190
|
}
|
|
108
191
|
|
|
109
192
|
// Sem tool_calls: o modelo respondeu e acabou.
|
|
110
193
|
if (!toolCalls.length) {
|
|
111
194
|
finalText = assistantText.trim();
|
|
112
|
-
if (
|
|
113
|
-
|
|
195
|
+
if (!subagent) {
|
|
196
|
+
if (finalText && !streaming) console.log('\n' + c.white(finalText));
|
|
197
|
+
else if (streaming) process.stdout.write('\n');
|
|
198
|
+
}
|
|
114
199
|
break;
|
|
115
200
|
}
|
|
116
201
|
|
|
117
|
-
if (streaming) process.stdout.write('\n');
|
|
202
|
+
if (streaming && !subagent) process.stdout.write('\n');
|
|
118
203
|
|
|
119
|
-
// Registra
|
|
120
|
-
//
|
|
204
|
+
// Registra o turno do assistente COM os tool_calls. A API exige que
|
|
205
|
+
// cada mensagem role:'tool' venha logo depois deste turno.
|
|
121
206
|
messages.push({
|
|
122
207
|
role: 'assistant',
|
|
123
208
|
content: assistantText || null,
|
|
@@ -131,26 +216,55 @@ async function runAct(userPrompt, opts) {
|
|
|
131
216
|
let finished = null;
|
|
132
217
|
for (const tc of toolCalls) {
|
|
133
218
|
if (tc.name === 'finish') {
|
|
219
|
+
// Encerrar com falha pendente é como o agente dizia "pronto!"
|
|
220
|
+
// sem ter mudado nada. Devolvemos a tarefa para ele.
|
|
221
|
+
if (falhasPendentes.length && tentativasDeEncerrar < 2) {
|
|
222
|
+
tentativasDeEncerrar++;
|
|
223
|
+
messages.push({
|
|
224
|
+
role: 'tool',
|
|
225
|
+
tool_call_id: tc.id,
|
|
226
|
+
content: `NÃO terminou. Estas tools falharam e você não corrigiu: ${falhasPendentes.join('; ')}. `
|
|
227
|
+
+ 'Conserte agora (leia o arquivo antes de editar) ou, se for impossível, diga claramente ao usuário o que falhou.',
|
|
228
|
+
});
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
134
231
|
finished = (tc.arguments && tc.arguments.summary) || 'Concluído.';
|
|
135
232
|
messages.push({ role: 'tool', tool_call_id: tc.id, content: 'ok' });
|
|
136
233
|
continue;
|
|
137
234
|
}
|
|
138
235
|
|
|
236
|
+
if (tc.name === 'spawn_agent') {
|
|
237
|
+
const r = await runSubagent(tc.arguments || {}, {
|
|
238
|
+
server, model, token, projectDir, projectName, profundidade,
|
|
239
|
+
});
|
|
240
|
+
messages.push({ role: 'tool', tool_call_id: tc.id, content: r });
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
|
|
139
244
|
if (!workDirReady) { ensureWorkDir(); workDirReady = true; }
|
|
140
245
|
|
|
141
|
-
console.log(actionHeader(tc.name, tc.arguments));
|
|
246
|
+
console.log(ident(profundidade) + actionHeader(tc.name, tc.arguments));
|
|
142
247
|
const result = await tools.execute(tc.name, tc.arguments, { projectDir });
|
|
143
|
-
logResult(tc.name, result);
|
|
248
|
+
logResult(tc.name, result, profundidade);
|
|
249
|
+
|
|
250
|
+
// Uma escrita bem-sucedida limpa a dívida: o agente se corrigiu.
|
|
251
|
+
if (result && result.ok === false) falhasPendentes.push(`${tc.name}(${tc.arguments?.path || ''}): ${result.error}`);
|
|
252
|
+
else if (['write_file', 'edit_file'].includes(tc.name)) falhasPendentes.length = 0;
|
|
253
|
+
|
|
144
254
|
messages.push({ role: 'tool', tool_call_id: tc.id, content: summarizeForModel(result) });
|
|
145
255
|
}
|
|
146
256
|
|
|
147
|
-
if (finished) {
|
|
257
|
+
if (finished) {
|
|
258
|
+
finalText = finished;
|
|
259
|
+
if (!subagent) console.log('\n' + c.white(finished));
|
|
260
|
+
break;
|
|
261
|
+
}
|
|
148
262
|
}
|
|
149
263
|
|
|
150
264
|
if (!finalText) finalText = 'Cheguei ao limite de passos sem concluir. Refaça o pedido em partes menores.';
|
|
151
265
|
|
|
152
266
|
// Nada de arquivo mexido = nada para lembrar.
|
|
153
|
-
if (projectName && workDirReady) {
|
|
267
|
+
if (!subagent && projectName && workDirReady) {
|
|
154
268
|
cfg.appendProjectMemory(projectDir, `**Pedido:** ${userPrompt.slice(0, 300)}\n**Resultado:** ${finalText.slice(0, 500)}`);
|
|
155
269
|
}
|
|
156
270
|
|
|
@@ -160,4 +274,27 @@ async function runAct(userPrompt, opts) {
|
|
|
160
274
|
};
|
|
161
275
|
}
|
|
162
276
|
|
|
277
|
+
async function runSubagent(args, ctx) {
|
|
278
|
+
const { server, model, token, projectDir, projectName, profundidade } = ctx;
|
|
279
|
+
const descricao = args.description || 'sub-tarefa';
|
|
280
|
+
|
|
281
|
+
if (profundidade + 1 >= MAX_PROFUNDIDADE) {
|
|
282
|
+
return 'Recusado: subagente não pode criar outro subagente. Faça você mesmo.';
|
|
283
|
+
}
|
|
284
|
+
if (!args.prompt) return 'Recusado: prompt do subagente vazio.';
|
|
285
|
+
|
|
286
|
+
console.log(ident(profundidade) + ` ${c.magenta('◆')} ${c.bold(c.magenta('Agente'))} ${c.muted(descricao)}`);
|
|
287
|
+
|
|
288
|
+
const r = await runAct(args.prompt, {
|
|
289
|
+
server, model, token, projectDir, projectName,
|
|
290
|
+
history: [],
|
|
291
|
+
profundidade: profundidade + 1,
|
|
292
|
+
subagent: true,
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
const resumo = r.assistantMessage.content || 'sem resultado';
|
|
296
|
+
console.log(ident(profundidade) + ` ${c.magenta('◆')} ${c.muted(resumo.slice(0, 120))}`);
|
|
297
|
+
return resumo;
|
|
298
|
+
}
|
|
299
|
+
|
|
163
300
|
module.exports = { runAct };
|