primocode 7.1.0 → 7.3.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 +60 -0
- package/bin/primocode.js +54 -25
- package/lib/act.js +140 -39
- package/lib/config.js +9 -8
- package/lib/tools.js +343 -147
- package/lib/ui.js +23 -22
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -61,6 +61,66 @@ fim. É por isso que a IA não esquece o projeto entre sessões.
|
|
|
61
61
|
|
|
62
62
|
Alterne com `/mode primocode` ou `/mode chat`.
|
|
63
63
|
|
|
64
|
+
## Ferramentas do agente
|
|
65
|
+
|
|
66
|
+
| Tool | Para quê |
|
|
67
|
+
|---|---|
|
|
68
|
+
| `write_file` | Cria arquivo novo ou reescreve inteiro |
|
|
69
|
+
| `edit_file` | **Troca um trecho exato.** É o que evita reescrever 800 linhas pra mudar uma |
|
|
70
|
+
| `read_file` | Lê o arquivo, com `offset`/`limit` para os grandes |
|
|
71
|
+
| `glob` | Acha arquivos por padrão: `**/*.js`, `src/**/*.{ts,tsx}` |
|
|
72
|
+
| `grep` | Procura texto/regex **dentro** dos arquivos, com arquivo e linha |
|
|
73
|
+
| `list_dir` | Árvore de arquivos |
|
|
74
|
+
| `run_shell` | Roda comando na pasta do projeto |
|
|
75
|
+
| `open_in_browser` | Abre HTML ou URL |
|
|
76
|
+
| `spawn_agent` | Delega uma sub-tarefa a outro agente |
|
|
77
|
+
|
|
78
|
+
O `edit_file` é rigoroso de propósito: se o trecho não existe, ou aparece mais
|
|
79
|
+
de uma vez sem `replace_all`, ele **recusa** e devolve as linhas reais do
|
|
80
|
+
arquivo — em vez de editar o lugar errado.
|
|
81
|
+
|
|
82
|
+
## Subagentes
|
|
83
|
+
|
|
84
|
+
Para tarefas com partes independentes, o agente delega:
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
Primo Code site ❯ crie o visual do site
|
|
88
|
+
|
|
89
|
+
◆ Agente criar o CSS
|
|
90
|
+
⎿ Write style.css
|
|
91
|
+
⎿ criado · 40 B
|
|
92
|
+
◆ Criei style.css com fundo #111
|
|
93
|
+
|
|
94
|
+
Pronto: CSS delegado e criado
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
O subagente tem conversa própria — **não vê o seu diálogo** — e devolve só o
|
|
98
|
+
resumo final. Ele não pode criar outro subagente, então a árvore nunca cresce
|
|
99
|
+
sem controle.
|
|
100
|
+
|
|
101
|
+
## Nunca "pronto!" falso
|
|
102
|
+
|
|
103
|
+
Se uma ferramenta falha e o agente tenta encerrar dizendo que deu certo, o
|
|
104
|
+
PrimoCode barra e devolve a tarefa para ele:
|
|
105
|
+
|
|
106
|
+
```
|
|
107
|
+
⎿ Edit app.js
|
|
108
|
+
⎿ Trecho não encontrado
|
|
109
|
+
→ agente tenta finish("Porta alterada")
|
|
110
|
+
→ BARRADO: "NÃO terminou. Estas tools falharam..."
|
|
111
|
+
⎿ Edit app.js
|
|
112
|
+
⎿ 1 troca · ±0 linhas ← agora foi de verdade
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Testes
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
npm test
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
25 verificações sobre glob, grep, edit_file, leitura por partes e bloqueio de
|
|
122
|
+
caminho fora do projeto.
|
|
123
|
+
|
|
64
124
|
## Uso
|
|
65
125
|
|
|
66
126
|
```bash
|
package/bin/primocode.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
const readline = require('readline');
|
|
11
11
|
const path = require('path');
|
|
12
12
|
const fs = require('fs');
|
|
13
|
+
const os = require('os');
|
|
13
14
|
|
|
14
15
|
const pkg = require('../package.json');
|
|
15
16
|
const { c, welcomeBox, Spinner } = require('../lib/ui');
|
|
@@ -27,6 +28,7 @@ const state = {
|
|
|
27
28
|
attachedFiles: [],
|
|
28
29
|
lastResponse: '',
|
|
29
30
|
currentProject: null,
|
|
31
|
+
workDir: null, // pasta onde o agente trabalha nesta sessão
|
|
30
32
|
};
|
|
31
33
|
|
|
32
34
|
const HELP = `
|
|
@@ -96,33 +98,40 @@ function deriveProjectName(text) {
|
|
|
96
98
|
return picked || 'projeto';
|
|
97
99
|
}
|
|
98
100
|
|
|
99
|
-
//
|
|
100
|
-
//
|
|
101
|
-
|
|
102
|
-
|
|
101
|
+
// Como o Claude Code: se você abriu o PrimoCode dentro de uma pasta de
|
|
102
|
+
// trabalho, é ali que ele trabalha. Home e raiz de disco não contam — ninguém
|
|
103
|
+
// quer um agente soltando arquivo no meio de C:\Users\você.
|
|
104
|
+
function isUsableCwd(dir) {
|
|
105
|
+
const home = os.homedir();
|
|
106
|
+
const resolved = path.resolve(dir);
|
|
107
|
+
if (resolved === path.resolve(home)) return false;
|
|
108
|
+
if (resolved === path.parse(resolved).root) return false;
|
|
109
|
+
if (resolved.startsWith(path.join(home, 'primocode'))) return false;
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
103
112
|
|
|
113
|
+
// Escolhe onde trabalhar SEM criar nada. A pasta só é criada de verdade em
|
|
114
|
+
// createPendingProject(), quando o agente for mexer em arquivo.
|
|
115
|
+
function planWorkDir(userText) {
|
|
116
|
+
if (state.workDir) return { dir: state.workDir, pending: false };
|
|
104
117
|
const proposed = deriveProjectName(userText);
|
|
105
118
|
const similar = cfg.findSimilarProject(proposed);
|
|
119
|
+
const name = similar ? similar.name : proposed;
|
|
120
|
+
return { dir: cfg.getProjectDir(name), pending: true, name, similar: !!similar };
|
|
121
|
+
}
|
|
106
122
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
state.currentProject = similar.name;
|
|
111
|
-
console.log(c.ok(` Projeto: ${similar.name}`));
|
|
112
|
-
refreshPrompt();
|
|
113
|
-
return state.currentProject;
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
const dir = cfg.createProjectDir(proposed);
|
|
123
|
+
function createPendingProject(plan) {
|
|
124
|
+
const dir = cfg.createProjectDir(plan.name);
|
|
125
|
+
state.workDir = dir;
|
|
118
126
|
state.currentProject = path.basename(dir);
|
|
119
|
-
|
|
127
|
+
const rotulo = plan.similar ? 'Continuando em' : 'Projeto novo:';
|
|
128
|
+
console.log(c.ok(` ${rotulo} ${state.currentProject}`) + c.muted(` (${dir})`));
|
|
120
129
|
refreshPrompt();
|
|
121
|
-
return
|
|
130
|
+
return dir;
|
|
122
131
|
}
|
|
123
132
|
|
|
124
133
|
function projectDir() {
|
|
125
|
-
return state.
|
|
134
|
+
return state.workDir || process.cwd();
|
|
126
135
|
}
|
|
127
136
|
|
|
128
137
|
// ── Envio ──────────────────────────────────────────────────────────────
|
|
@@ -135,14 +144,16 @@ async function sendPrompt(userText) {
|
|
|
135
144
|
}
|
|
136
145
|
|
|
137
146
|
if (state.mode === 'primocode') {
|
|
138
|
-
|
|
147
|
+
const plan = planWorkDir(userText);
|
|
139
148
|
const r = await runAct(prompt, {
|
|
140
149
|
server: state.config.server,
|
|
141
150
|
model: state.config.model,
|
|
142
151
|
token: state.config.token,
|
|
143
152
|
history: state.history,
|
|
144
|
-
projectDir:
|
|
145
|
-
projectName: state.currentProject,
|
|
153
|
+
projectDir: plan.dir,
|
|
154
|
+
projectName: plan.pending ? plan.name : state.currentProject,
|
|
155
|
+
// Só chamado se o agente for mesmo mexer em arquivo.
|
|
156
|
+
ensureWorkDir: plan.pending ? () => createPendingProject(plan) : null,
|
|
146
157
|
});
|
|
147
158
|
state.history.push({ role: 'user', content: userText });
|
|
148
159
|
state.history.push({ role: 'assistant', content: r.assistantMessage.content });
|
|
@@ -214,6 +225,7 @@ async function handleSlash(text) {
|
|
|
214
225
|
break;
|
|
215
226
|
}
|
|
216
227
|
const dir = cfg.createProjectDir(rest);
|
|
228
|
+
state.workDir = dir;
|
|
217
229
|
state.currentProject = path.basename(dir);
|
|
218
230
|
console.log(c.ok(`Projeto: ${state.currentProject}`) + c.muted(` (${dir})`));
|
|
219
231
|
refreshPrompt();
|
|
@@ -314,8 +326,24 @@ async function handleSlash(text) {
|
|
|
314
326
|
|
|
315
327
|
// ── Main ───────────────────────────────────────────────────────────────
|
|
316
328
|
|
|
329
|
+
function shortPath(p) {
|
|
330
|
+
// Normaliza as barras antes de comparar — no Windows o cwd vem com "\"
|
|
331
|
+
// e a home pode vir com "/", e aí o prefixo nunca batia.
|
|
332
|
+
const norm = (s) => path.resolve(s).replace(/\\/g, '/');
|
|
333
|
+
const home = norm(os.homedir());
|
|
334
|
+
const full = norm(p);
|
|
335
|
+
return full.startsWith(home + '/') ? '~' + full.slice(home.length) : full;
|
|
336
|
+
}
|
|
337
|
+
|
|
317
338
|
async function main() {
|
|
318
339
|
cfg.ensureStructure();
|
|
340
|
+
|
|
341
|
+
// Abriu dentro de uma pasta de trabalho? Então é ali que ele atua.
|
|
342
|
+
if (isUsableCwd(process.cwd())) {
|
|
343
|
+
state.workDir = process.cwd();
|
|
344
|
+
state.currentProject = path.basename(state.workDir);
|
|
345
|
+
}
|
|
346
|
+
|
|
319
347
|
const args = process.argv.slice(2);
|
|
320
348
|
|
|
321
349
|
// Modo one-shot: primocode "crie um jogo de carro"
|
|
@@ -332,19 +360,20 @@ async function main() {
|
|
|
332
360
|
} catch { console.log(c.err(`Não consegui ler: ${args[fileFlag + 1]}`)); process.exit(1); }
|
|
333
361
|
}
|
|
334
362
|
|
|
335
|
-
|
|
363
|
+
const plan = planWorkDir(promptText);
|
|
336
364
|
await runAct(fullPrompt, {
|
|
337
365
|
server: state.config.server,
|
|
338
366
|
model: state.config.model,
|
|
339
367
|
token: state.config.token,
|
|
340
368
|
history: [],
|
|
341
|
-
projectDir:
|
|
342
|
-
projectName: state.currentProject,
|
|
369
|
+
projectDir: plan.dir,
|
|
370
|
+
projectName: plan.pending ? plan.name : state.currentProject,
|
|
371
|
+
ensureWorkDir: plan.pending ? () => createPendingProject(plan) : null,
|
|
343
372
|
});
|
|
344
373
|
process.exit(0);
|
|
345
374
|
}
|
|
346
375
|
|
|
347
|
-
console.log(welcomeBox(VERSION, state.
|
|
376
|
+
console.log(welcomeBox(VERSION, state.workDir ? shortPath(state.workDir) : null));
|
|
348
377
|
rl = readline.createInterface({ input: process.stdin, output: process.stdout, prompt: makePrompt() });
|
|
349
378
|
rl.prompt();
|
|
350
379
|
|
package/lib/act.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
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
|
|
|
@@ -13,67 +16,106 @@ 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)}`));
|
|
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'));
|
|
25
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}`));
|
|
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}`));
|
|
37
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
|
-
console.log(prefix + c.muted(result.opener || 'ok'));
|
|
72
|
+
console.log(p + c.muted(result.opener || 'aberto'));
|
|
41
73
|
return;
|
|
42
74
|
}
|
|
43
75
|
}
|
|
44
76
|
|
|
45
|
-
// O que
|
|
46
|
-
//
|
|
77
|
+
// O que volta para o modelo. Cortamos saídas gigantes para não estourar a
|
|
78
|
+
// janela de contexto na iteração seguinte.
|
|
47
79
|
function summarizeForModel(result) {
|
|
48
80
|
const json = JSON.stringify(result);
|
|
49
|
-
return json.length >
|
|
81
|
+
return json.length > 6000 ? json.slice(0, 6000) + '…(truncado)' : json;
|
|
50
82
|
}
|
|
51
83
|
|
|
52
|
-
function buildMemoryContext(projectName) {
|
|
84
|
+
function buildMemoryContext(projectDir, projectName) {
|
|
53
85
|
const parts = [];
|
|
54
86
|
const geral = cfg.readGeneralMemory();
|
|
55
87
|
if (geral.trim()) parts.push(`[memory-geral.md]\n${geral}`);
|
|
56
|
-
const projeto = cfg.readProjectMemory(
|
|
88
|
+
const projeto = cfg.readProjectMemory(projectDir);
|
|
57
89
|
if (projeto.trim()) parts.push(`[${projectName}/memory.md]\n${projeto}`);
|
|
58
90
|
return parts.join('\n\n');
|
|
59
91
|
}
|
|
60
92
|
|
|
61
93
|
async function runAct(userPrompt, opts) {
|
|
62
|
-
const {
|
|
94
|
+
const {
|
|
95
|
+
server, model, token, history, projectDir, projectName,
|
|
96
|
+
ensureWorkDir, profundidade = 0, subagent = false,
|
|
97
|
+
} = opts || {};
|
|
98
|
+
|
|
99
|
+
// A pasta só nasce quando o agente for mesmo mexer em arquivo. Um "oi"
|
|
100
|
+
// não deve deixar diretório vazio espalhado pelo disco.
|
|
101
|
+
let workDirReady = !ensureWorkDir;
|
|
102
|
+
const falhasPendentes = [];
|
|
103
|
+
let tentativasDeEncerrar = 0;
|
|
63
104
|
|
|
64
|
-
// Conversa completa desta tarefa: histórico recente + memória + pedido.
|
|
65
105
|
const messages = [];
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
106
|
+
if (!subagent) {
|
|
107
|
+
const memory = buildMemoryContext(projectDir, projectName);
|
|
108
|
+
if (memory) messages.push({ role: 'system', content: `Memória persistente:\n\n${memory}` });
|
|
109
|
+
for (const m of (history || []).slice(-20)) {
|
|
110
|
+
if (m && m.role && m.content) messages.push({ role: m.role, content: m.content });
|
|
111
|
+
}
|
|
70
112
|
}
|
|
71
113
|
messages.push({ role: 'user', content: userPrompt });
|
|
72
114
|
|
|
73
115
|
let finalText = '';
|
|
74
116
|
|
|
75
117
|
for (let iteration = 0; iteration < MAX_ITERATIONS; iteration++) {
|
|
76
|
-
const sp = new Spinner('Primo Code pensando').start();
|
|
118
|
+
const sp = new Spinner(subagent ? 'subagente trabalhando' : 'Primo Code pensando').start();
|
|
77
119
|
|
|
78
120
|
const toolCalls = [];
|
|
79
121
|
let assistantText = '';
|
|
@@ -81,24 +123,26 @@ async function runAct(userPrompt, opts) {
|
|
|
81
123
|
let streaming = false;
|
|
82
124
|
|
|
83
125
|
try {
|
|
84
|
-
await api.requestAct(server, { messages, model, token, projectDir, projectName }, (evt) => {
|
|
126
|
+
await api.requestAct(server, { messages, model, token, projectDir, projectName, subagent }, (evt) => {
|
|
85
127
|
if (evt.erro) { streamError = evt.erro; return; }
|
|
86
128
|
if (evt.tool_call) { toolCalls.push(evt.tool_call); return; }
|
|
87
129
|
if (evt.delta) {
|
|
88
130
|
if (!streaming) { sp.stop(); streaming = true; }
|
|
89
131
|
assistantText += evt.delta;
|
|
90
|
-
process.stdout.write(c.white(evt.delta));
|
|
132
|
+
if (!subagent) process.stdout.write(c.white(evt.delta));
|
|
91
133
|
}
|
|
92
134
|
if (evt.assistant_message) assistantText = evt.assistant_message.content || assistantText;
|
|
93
135
|
});
|
|
94
136
|
} catch (e) {
|
|
95
137
|
sp.stop();
|
|
138
|
+
if (subagent) return { assistantMessage: { role: 'assistant', content: `Subagente falhou: ${e.message || e}` } };
|
|
96
139
|
console.log(c.err(`\nErro: ${e.message || e}`));
|
|
97
140
|
throw e;
|
|
98
141
|
}
|
|
99
142
|
sp.stop();
|
|
100
143
|
|
|
101
144
|
if (streamError) {
|
|
145
|
+
if (subagent) return { assistantMessage: { role: 'assistant', content: `Subagente falhou: ${streamError}` } };
|
|
102
146
|
console.log(c.err(`\nErro: ${streamError}`));
|
|
103
147
|
return { assistantMessage: { role: 'assistant', content: `Erro: ${streamError}` } };
|
|
104
148
|
}
|
|
@@ -106,15 +150,17 @@ async function runAct(userPrompt, opts) {
|
|
|
106
150
|
// Sem tool_calls: o modelo respondeu e acabou.
|
|
107
151
|
if (!toolCalls.length) {
|
|
108
152
|
finalText = assistantText.trim();
|
|
109
|
-
if (
|
|
110
|
-
|
|
153
|
+
if (!subagent) {
|
|
154
|
+
if (finalText && !streaming) console.log('\n' + c.white(finalText));
|
|
155
|
+
else if (streaming) process.stdout.write('\n');
|
|
156
|
+
}
|
|
111
157
|
break;
|
|
112
158
|
}
|
|
113
159
|
|
|
114
|
-
if (streaming) process.stdout.write('\n');
|
|
160
|
+
if (streaming && !subagent) process.stdout.write('\n');
|
|
115
161
|
|
|
116
|
-
// Registra
|
|
117
|
-
//
|
|
162
|
+
// Registra o turno do assistente COM os tool_calls. A API exige que
|
|
163
|
+
// cada mensagem role:'tool' venha logo depois deste turno.
|
|
118
164
|
messages.push({
|
|
119
165
|
role: 'assistant',
|
|
120
166
|
content: assistantText || null,
|
|
@@ -128,24 +174,56 @@ async function runAct(userPrompt, opts) {
|
|
|
128
174
|
let finished = null;
|
|
129
175
|
for (const tc of toolCalls) {
|
|
130
176
|
if (tc.name === 'finish') {
|
|
177
|
+
// Encerrar com falha pendente é como o agente dizia "pronto!"
|
|
178
|
+
// sem ter mudado nada. Devolvemos a tarefa para ele.
|
|
179
|
+
if (falhasPendentes.length && tentativasDeEncerrar < 2) {
|
|
180
|
+
tentativasDeEncerrar++;
|
|
181
|
+
messages.push({
|
|
182
|
+
role: 'tool',
|
|
183
|
+
tool_call_id: tc.id,
|
|
184
|
+
content: `NÃO terminou. Estas tools falharam e você não corrigiu: ${falhasPendentes.join('; ')}. `
|
|
185
|
+
+ 'Conserte agora (leia o arquivo antes de editar) ou, se for impossível, diga claramente ao usuário o que falhou.',
|
|
186
|
+
});
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
131
189
|
finished = (tc.arguments && tc.arguments.summary) || 'Concluído.';
|
|
132
190
|
messages.push({ role: 'tool', tool_call_id: tc.id, content: 'ok' });
|
|
133
191
|
continue;
|
|
134
192
|
}
|
|
135
193
|
|
|
136
|
-
|
|
194
|
+
if (tc.name === 'spawn_agent') {
|
|
195
|
+
const r = await runSubagent(tc.arguments || {}, {
|
|
196
|
+
server, model, token, projectDir, projectName, profundidade,
|
|
197
|
+
});
|
|
198
|
+
messages.push({ role: 'tool', tool_call_id: tc.id, content: r });
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (!workDirReady) { ensureWorkDir(); workDirReady = true; }
|
|
203
|
+
|
|
204
|
+
console.log(ident(profundidade) + actionHeader(tc.name, tc.arguments));
|
|
137
205
|
const result = await tools.execute(tc.name, tc.arguments, { projectDir });
|
|
138
|
-
logResult(tc.name, result);
|
|
206
|
+
logResult(tc.name, result, profundidade);
|
|
207
|
+
|
|
208
|
+
// Uma escrita bem-sucedida limpa a dívida: o agente se corrigiu.
|
|
209
|
+
if (result && result.ok === false) falhasPendentes.push(`${tc.name}(${tc.arguments?.path || ''}): ${result.error}`);
|
|
210
|
+
else if (['write_file', 'edit_file'].includes(tc.name)) falhasPendentes.length = 0;
|
|
211
|
+
|
|
139
212
|
messages.push({ role: 'tool', tool_call_id: tc.id, content: summarizeForModel(result) });
|
|
140
213
|
}
|
|
141
214
|
|
|
142
|
-
if (finished) {
|
|
215
|
+
if (finished) {
|
|
216
|
+
finalText = finished;
|
|
217
|
+
if (!subagent) console.log('\n' + c.white(finished));
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
143
220
|
}
|
|
144
221
|
|
|
145
222
|
if (!finalText) finalText = 'Cheguei ao limite de passos sem concluir. Refaça o pedido em partes menores.';
|
|
146
223
|
|
|
147
|
-
|
|
148
|
-
|
|
224
|
+
// Nada de arquivo mexido = nada para lembrar.
|
|
225
|
+
if (!subagent && projectName && workDirReady) {
|
|
226
|
+
cfg.appendProjectMemory(projectDir, `**Pedido:** ${userPrompt.slice(0, 300)}\n**Resultado:** ${finalText.slice(0, 500)}`);
|
|
149
227
|
}
|
|
150
228
|
|
|
151
229
|
return {
|
|
@@ -154,4 +232,27 @@ async function runAct(userPrompt, opts) {
|
|
|
154
232
|
};
|
|
155
233
|
}
|
|
156
234
|
|
|
235
|
+
async function runSubagent(args, ctx) {
|
|
236
|
+
const { server, model, token, projectDir, projectName, profundidade } = ctx;
|
|
237
|
+
const descricao = args.description || 'sub-tarefa';
|
|
238
|
+
|
|
239
|
+
if (profundidade + 1 >= MAX_PROFUNDIDADE) {
|
|
240
|
+
return 'Recusado: subagente não pode criar outro subagente. Faça você mesmo.';
|
|
241
|
+
}
|
|
242
|
+
if (!args.prompt) return 'Recusado: prompt do subagente vazio.';
|
|
243
|
+
|
|
244
|
+
console.log(ident(profundidade) + ` ${c.magenta('◆')} ${c.bold(c.magenta('Agente'))} ${c.muted(descricao)}`);
|
|
245
|
+
|
|
246
|
+
const r = await runAct(args.prompt, {
|
|
247
|
+
server, model, token, projectDir, projectName,
|
|
248
|
+
history: [],
|
|
249
|
+
profundidade: profundidade + 1,
|
|
250
|
+
subagent: true,
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
const resumo = r.assistantMessage.content || 'sem resultado';
|
|
254
|
+
console.log(ident(profundidade) + ` ${c.magenta('◆')} ${c.muted(resumo.slice(0, 120))}`);
|
|
255
|
+
return resumo;
|
|
256
|
+
}
|
|
257
|
+
|
|
157
258
|
module.exports = { runAct };
|
package/lib/config.js
CHANGED
|
@@ -135,18 +135,19 @@ function readGeneralMemory(limit = 2000) {
|
|
|
135
135
|
catch { return ''; }
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
138
|
+
// Recebem o DIRETÓRIO de trabalho, não o nome do projeto: quando o PrimoCode
|
|
139
|
+
// roda dentro de uma pasta sua, a memória fica lá, junto do código.
|
|
140
|
+
function readProjectMemory(dir, limit = 2000) {
|
|
141
|
+
if (!dir) return '';
|
|
142
|
+
try { return fs.readFileSync(path.join(dir, 'memory.md'), 'utf-8').slice(0, limit); }
|
|
141
143
|
catch { return ''; }
|
|
142
144
|
}
|
|
143
145
|
|
|
144
|
-
function appendProjectMemory(
|
|
145
|
-
if (!
|
|
146
|
-
const memoryPath = path.join(getProjectDir(projectName), 'memory.md');
|
|
146
|
+
function appendProjectMemory(dir, entry) {
|
|
147
|
+
if (!dir || !entry) return;
|
|
147
148
|
try {
|
|
148
|
-
ensureDir(
|
|
149
|
-
fs.appendFileSync(
|
|
149
|
+
ensureDir(dir);
|
|
150
|
+
fs.appendFileSync(path.join(dir, 'memory.md'), `\n### ${today()}\n${entry}\n`, 'utf-8');
|
|
150
151
|
} catch {}
|
|
151
152
|
}
|
|
152
153
|
|
package/lib/tools.js
CHANGED
|
@@ -1,147 +1,343 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Ferramentas
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
return
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
if (
|
|
66
|
-
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Ferramentas locais do agente.
|
|
3
|
+
*
|
|
4
|
+
* Tudo roda na máquina do usuário — o servidor só decide o que chamar.
|
|
5
|
+
* Todo caminho é resolvido dentro da pasta de trabalho da sessão.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
'use strict';
|
|
9
|
+
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const os = require('os');
|
|
13
|
+
const { exec } = require('child_process');
|
|
14
|
+
|
|
15
|
+
const IGNORAR = new Set(['node_modules', '.git', 'dist', 'build', '.next', 'coverage', '.cache', 'vendor']);
|
|
16
|
+
const MAX_ARQUIVOS_VARREDURA = 5000;
|
|
17
|
+
|
|
18
|
+
function fmtBytes(n) {
|
|
19
|
+
if (n < 1024) return `${n} B`;
|
|
20
|
+
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
|
|
21
|
+
return `${(n / (1024 * 1024)).toFixed(1)} MB`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Todo caminho vindo do modelo é resolvido DENTRO da pasta de trabalho.
|
|
25
|
+
// Sem isso o agente espalha arquivo por qualquer lugar do disco — e um
|
|
26
|
+
// "../../.." viraria escrita fora da área do projeto.
|
|
27
|
+
function resolveInProject(rawPath, projectDir) {
|
|
28
|
+
const base = projectDir || process.cwd();
|
|
29
|
+
const target = path.resolve(base, rawPath || '.');
|
|
30
|
+
const rel = path.relative(base, target);
|
|
31
|
+
if (rel.startsWith('..') || path.isAbsolute(rel)) {
|
|
32
|
+
throw new Error(`Caminho fora da pasta de trabalho: ${rawPath}`);
|
|
33
|
+
}
|
|
34
|
+
return target;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function treeDir(dir, maxDepth = 2, currentDepth = 0, prefix = '') {
|
|
38
|
+
if (currentDepth > maxDepth) return '';
|
|
39
|
+
let result = '';
|
|
40
|
+
try {
|
|
41
|
+
const items = fs.readdirSync(dir);
|
|
42
|
+
const dirs = [], files = [];
|
|
43
|
+
for (const item of items) {
|
|
44
|
+
if (item.startsWith('.') && item !== '.env') continue;
|
|
45
|
+
if (IGNORAR.has(item)) continue;
|
|
46
|
+
(fs.statSync(path.join(dir, item)).isDirectory() ? dirs : files).push(item);
|
|
47
|
+
}
|
|
48
|
+
dirs.sort(); files.sort();
|
|
49
|
+
const all = [...dirs, ...files];
|
|
50
|
+
for (let i = 0; i < all.length; i++) {
|
|
51
|
+
const item = all[i];
|
|
52
|
+
const isLast = i === all.length - 1;
|
|
53
|
+
result += prefix + (isLast ? '└── ' : '├── ') + item + '\n';
|
|
54
|
+
const full = path.join(dir, item);
|
|
55
|
+
if (fs.statSync(full).isDirectory() && currentDepth < maxDepth) {
|
|
56
|
+
result += treeDir(full, maxDepth, currentDepth + 1, prefix + (isLast ? ' ' : '│ '));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
} catch { return ''; }
|
|
60
|
+
return result;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Percorre a árvore uma vez só; grep e glob se apoiam nisto.
|
|
64
|
+
function* walk(dir, base, profundidade = 0) {
|
|
65
|
+
if (profundidade > 12) return;
|
|
66
|
+
let entradas;
|
|
67
|
+
try { entradas = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
|
68
|
+
for (const e of entradas) {
|
|
69
|
+
if (IGNORAR.has(e.name)) continue;
|
|
70
|
+
if (e.name.startsWith('.') && e.name !== '.env') continue;
|
|
71
|
+
const full = path.join(dir, e.name);
|
|
72
|
+
if (e.isDirectory()) yield* walk(full, base, profundidade + 1);
|
|
73
|
+
else yield path.relative(base, full).replace(/\\/g, '/');
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Glob sem dependência externa: **, *, ? e {a,b}.
|
|
78
|
+
function globToRegex(pattern) {
|
|
79
|
+
let re = '';
|
|
80
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
81
|
+
const ch = pattern[i];
|
|
82
|
+
if (ch === '*') {
|
|
83
|
+
if (pattern[i + 1] === '*') {
|
|
84
|
+
// **/ some caminho inteiro, inclusive "nenhuma pasta"
|
|
85
|
+
if (pattern[i + 2] === '/') { re += '(?:.*/)?'; i += 2; }
|
|
86
|
+
else { re += '.*'; i += 1; }
|
|
87
|
+
} else re += '[^/]*';
|
|
88
|
+
} else if (ch === '?') re += '[^/]';
|
|
89
|
+
else if (ch === '{') {
|
|
90
|
+
const fim = pattern.indexOf('}', i);
|
|
91
|
+
if (fim === -1) re += '\\{';
|
|
92
|
+
else {
|
|
93
|
+
re += '(?:' + pattern.slice(i + 1, fim).split(',').map((s) => s.replace(/[.+^${}()|[\]\\]/g, '\\$&')).join('|') + ')';
|
|
94
|
+
i = fim;
|
|
95
|
+
}
|
|
96
|
+
} else re += ch.replace(/[.+^${}()|[\]\\]/g, '\\$&');
|
|
97
|
+
}
|
|
98
|
+
return new RegExp(`^${re}$`, 'i');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function globFiles(pattern, projectDir) {
|
|
102
|
+
const re = globToRegex(pattern);
|
|
103
|
+
const achados = [];
|
|
104
|
+
for (const rel of walk(projectDir, projectDir)) {
|
|
105
|
+
if (achados.length >= MAX_ARQUIVOS_VARREDURA) break;
|
|
106
|
+
if (re.test(rel)) achados.push(rel);
|
|
107
|
+
}
|
|
108
|
+
return achados.sort();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function pareceBinario(buf) {
|
|
112
|
+
const amostra = buf.slice(0, 1024);
|
|
113
|
+
for (let i = 0; i < amostra.length; i++) if (amostra[i] === 0) return true;
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function grepFiles({ pattern, glob, ignoreCase, contexto = 0, maxResultados = 100 }, projectDir) {
|
|
118
|
+
let re;
|
|
119
|
+
try { re = new RegExp(pattern, ignoreCase ? 'i' : ''); }
|
|
120
|
+
catch (e) { return { ok: false, error: `Regex inválida: ${e.message}` }; }
|
|
121
|
+
|
|
122
|
+
const filtro = glob ? globToRegex(glob) : null;
|
|
123
|
+
const resultados = [];
|
|
124
|
+
let arquivosVistos = 0;
|
|
125
|
+
|
|
126
|
+
for (const rel of walk(projectDir, projectDir)) {
|
|
127
|
+
if (resultados.length >= maxResultados) break;
|
|
128
|
+
if (filtro && !filtro.test(rel)) continue;
|
|
129
|
+
arquivosVistos++;
|
|
130
|
+
|
|
131
|
+
let buf;
|
|
132
|
+
try { buf = fs.readFileSync(path.join(projectDir, rel)); } catch { continue; }
|
|
133
|
+
if (buf.length > 2 * 1024 * 1024 || pareceBinario(buf)) continue;
|
|
134
|
+
|
|
135
|
+
const linhas = buf.toString('utf-8').split('\n');
|
|
136
|
+
for (let i = 0; i < linhas.length && resultados.length < maxResultados; i++) {
|
|
137
|
+
if (!re.test(linhas[i])) continue;
|
|
138
|
+
const de = Math.max(0, i - contexto);
|
|
139
|
+
const ate = Math.min(linhas.length - 1, i + contexto);
|
|
140
|
+
const trecho = [];
|
|
141
|
+
for (let j = de; j <= ate; j++) trecho.push(`${j + 1}${j === i ? ':' : '-'}${linhas[j]}`.slice(0, 300));
|
|
142
|
+
resultados.push({ arquivo: rel, linha: i + 1, trecho: trecho.join('\n') });
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return { ok: true, arquivosVistos, total: resultados.length, resultados, truncado: resultados.length >= maxResultados };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function runShell(command, timeout = 60000, cwd) {
|
|
150
|
+
return new Promise((resolve) => {
|
|
151
|
+
exec(command, { timeout, cwd: cwd || process.cwd(), maxBuffer: 10 * 1024 * 1024 }, (error, stdout, stderr) => {
|
|
152
|
+
if (error) resolve({ ok: false, code: error.code ?? 1, stdout, stderr, killed: error.killed });
|
|
153
|
+
else resolve({ ok: true, code: 0, stdout, stderr });
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function openInBrowser(target) {
|
|
159
|
+
const p = os.platform();
|
|
160
|
+
const cmd = p === 'darwin' ? `open "${target}"` : p === 'win32' ? `start "" "${target}"` : `xdg-open "${target}"`;
|
|
161
|
+
const r = await runShell(cmd);
|
|
162
|
+
return r.ok ? { ok: true, opener: cmd.split(' ')[0] } : { ok: false, error: r.stderr || 'falhou ao abrir' };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function runInNewTerminal(command, cwd) {
|
|
166
|
+
const p = os.platform();
|
|
167
|
+
let cmd;
|
|
168
|
+
if (p === 'darwin') cmd = `osascript -e 'tell application "Terminal" to do script "cd ${cwd || '.'} && ${command}"'`;
|
|
169
|
+
else if (p === 'win32') cmd = `start cmd /k "cd /d ${cwd || '.'} && ${command}"`;
|
|
170
|
+
else cmd = `x-terminal-emulator -e "bash -c 'cd ${cwd || '.'} && ${command}; exec bash'"`;
|
|
171
|
+
const r = await runShell(cmd);
|
|
172
|
+
return r.ok ? { ok: true, opener: cmd.split(' ')[0] } : { ok: false, error: r.stderr || 'falhou' };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Quando o old_string não bate, mostra as linhas do arquivo mais parecidas —
|
|
176
|
+
// com número — para o modelo copiar o texto certo em vez de chutar de novo.
|
|
177
|
+
function trechosParecidos(conteudo, alvo, quantas = 8) {
|
|
178
|
+
const termos = alvo
|
|
179
|
+
.split(/[\s{}();,=]+/)
|
|
180
|
+
.map((t) => t.trim().toLowerCase())
|
|
181
|
+
.filter((t) => t.length > 2);
|
|
182
|
+
if (!termos.length) return conteudo.split('\n').slice(0, quantas).map((l, i) => `${i + 1}\t${l}`);
|
|
183
|
+
|
|
184
|
+
const linhas = conteudo.split('\n');
|
|
185
|
+
const pontuadas = linhas.map((linha, i) => {
|
|
186
|
+
const baixa = linha.toLowerCase();
|
|
187
|
+
let pontos = 0;
|
|
188
|
+
for (const t of termos) if (baixa.includes(t)) pontos++;
|
|
189
|
+
return { i, linha, pontos };
|
|
190
|
+
}).filter((x) => x.pontos > 0);
|
|
191
|
+
|
|
192
|
+
pontuadas.sort((a, b) => b.pontos - a.pontos || a.i - b.i);
|
|
193
|
+
return pontuadas
|
|
194
|
+
.slice(0, quantas)
|
|
195
|
+
.sort((a, b) => a.i - b.i)
|
|
196
|
+
.map((x) => `${x.i + 1}\t${x.linha.slice(0, 200)}`);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Edição cirúrgica: troca um trecho exato em vez de reescrever o arquivo.
|
|
200
|
+
// É o que impede o agente de reescrever 800 linhas para mudar uma.
|
|
201
|
+
function editFile({ path: rawPath, old_string, new_string, replace_all }, projectDir) {
|
|
202
|
+
const filePath = resolveInProject(rawPath, projectDir);
|
|
203
|
+
if (!fs.existsSync(filePath)) return { ok: false, error: `Arquivo não existe: ${rawPath}` };
|
|
204
|
+
if (typeof old_string !== 'string' || !old_string) return { ok: false, error: 'old_string é obrigatório' };
|
|
205
|
+
if (old_string === new_string) return { ok: false, error: 'old_string e new_string são iguais' };
|
|
206
|
+
|
|
207
|
+
const original = fs.readFileSync(filePath, 'utf-8');
|
|
208
|
+
const ocorrencias = original.split(old_string).length - 1;
|
|
209
|
+
|
|
210
|
+
if (ocorrencias === 0) {
|
|
211
|
+
// Erro seco faz o modelo chutar de novo. Devolvendo o que EXISTE no
|
|
212
|
+
// arquivo, ele acerta na segunda tentativa.
|
|
213
|
+
return {
|
|
214
|
+
ok: false,
|
|
215
|
+
error: 'Trecho não encontrado — o arquivo não contém esse texto exato.',
|
|
216
|
+
comoCorrigir: 'Copie o old_string LITERALMENTE de "trechosParecidos" abaixo, com a mesma indentação.',
|
|
217
|
+
trechosParecidos: trechosParecidos(original, old_string),
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
if (ocorrencias > 1 && !replace_all) {
|
|
221
|
+
return { ok: false, error: `Trecho aparece ${ocorrencias} vezes. Inclua mais linhas em volta para ficar único, ou use replace_all.` };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const atualizado = replace_all
|
|
225
|
+
? original.split(old_string).join(new_string ?? '')
|
|
226
|
+
: original.replace(old_string, new_string ?? '');
|
|
227
|
+
|
|
228
|
+
fs.writeFileSync(filePath, atualizado, 'utf-8');
|
|
229
|
+
return {
|
|
230
|
+
ok: true,
|
|
231
|
+
path: path.relative(projectDir, filePath).replace(/\\/g, '/'),
|
|
232
|
+
substituicoes: replace_all ? ocorrencias : 1,
|
|
233
|
+
linhasAntes: original.split('\n').length,
|
|
234
|
+
linhasDepois: atualizado.split('\n').length,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function readFile({ path: rawPath, offset, limit }, projectDir) {
|
|
239
|
+
const filePath = resolveInProject(rawPath, projectDir);
|
|
240
|
+
const bruto = fs.readFileSync(filePath, 'utf-8');
|
|
241
|
+
const todas = bruto.split('\n');
|
|
242
|
+
|
|
243
|
+
// Sem recorte pedido: devolve o começo, avisando que cortou.
|
|
244
|
+
if (offset == null && limit == null) {
|
|
245
|
+
const truncado = bruto.length > 40000;
|
|
246
|
+
return {
|
|
247
|
+
ok: true,
|
|
248
|
+
content: truncado ? bruto.slice(0, 40000) : bruto,
|
|
249
|
+
linhas: todas.length,
|
|
250
|
+
bytes: bruto.length,
|
|
251
|
+
truncado,
|
|
252
|
+
dica: truncado ? 'Use offset/limit para ler o resto.' : undefined,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const inicio = Math.max(0, (offset || 1) - 1);
|
|
257
|
+
const fim = Math.min(todas.length, inicio + (limit || 200));
|
|
258
|
+
const recorte = todas.slice(inicio, fim).map((l, i) => `${inicio + i + 1}\t${l}`).join('\n');
|
|
259
|
+
return { ok: true, content: recorte, linhas: todas.length, de: inicio + 1, ate: fim, bytes: bruto.length };
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
async function execute(name, args, ctx) {
|
|
263
|
+
const projectDir = (ctx && ctx.projectDir) || process.cwd();
|
|
264
|
+
args = args || {};
|
|
265
|
+
|
|
266
|
+
try {
|
|
267
|
+
switch (name) {
|
|
268
|
+
case 'write_file': {
|
|
269
|
+
const filePath = resolveInProject(args.path, projectDir);
|
|
270
|
+
const content = args.content || '';
|
|
271
|
+
const created = !fs.existsSync(filePath);
|
|
272
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
273
|
+
fs.writeFileSync(filePath, content, 'utf-8');
|
|
274
|
+
return { ok: true, created, path: path.relative(projectDir, filePath).replace(/\\/g, '/'), bytes: content.length };
|
|
275
|
+
}
|
|
276
|
+
case 'edit_file':
|
|
277
|
+
return editFile(args, projectDir);
|
|
278
|
+
case 'read_file':
|
|
279
|
+
return readFile(args, projectDir);
|
|
280
|
+
case 'list_dir':
|
|
281
|
+
return { ok: true, tree: treeDir(resolveInProject(args.path, projectDir), args.depth || 2) };
|
|
282
|
+
case 'glob': {
|
|
283
|
+
const arquivos = globFiles(args.pattern || '**/*', projectDir);
|
|
284
|
+
return { ok: true, total: arquivos.length, arquivos: arquivos.slice(0, 200) };
|
|
285
|
+
}
|
|
286
|
+
case 'grep':
|
|
287
|
+
return grepFiles(args, projectDir);
|
|
288
|
+
case 'run_shell':
|
|
289
|
+
return runShell(args.command, args.timeout_ms || 60000, projectDir);
|
|
290
|
+
case 'run_in_new_terminal':
|
|
291
|
+
return runInNewTerminal(args.command, args.cwd || projectDir);
|
|
292
|
+
case 'open_in_browser': {
|
|
293
|
+
const alvo = /^https?:\/\//i.test(args.target || '')
|
|
294
|
+
? args.target
|
|
295
|
+
: resolveInProject(args.target, projectDir);
|
|
296
|
+
return openInBrowser(alvo);
|
|
297
|
+
}
|
|
298
|
+
default:
|
|
299
|
+
return { ok: false, error: `Tool não implementada: ${name}` };
|
|
300
|
+
}
|
|
301
|
+
} catch (e) {
|
|
302
|
+
return { ok: false, error: e.message };
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function applyCodeBlocks(response) {
|
|
307
|
+
const regex = /```[\w]*\n([\s\S]*?)```/g;
|
|
308
|
+
const applied = [];
|
|
309
|
+
let match;
|
|
310
|
+
while ((match = regex.exec(response || '')) !== null) {
|
|
311
|
+
const block = match[1];
|
|
312
|
+
const nome = block.match(/\/\/\s*filename:\s*(.+)/);
|
|
313
|
+
if (!nome) continue;
|
|
314
|
+
const filename = nome[1].trim();
|
|
315
|
+
const content = block.replace(/\/\/\s*filename:.+\n?/, '');
|
|
316
|
+
const created = !fs.existsSync(filename);
|
|
317
|
+
fs.mkdirSync(path.dirname(filename), { recursive: true });
|
|
318
|
+
fs.writeFileSync(filename, content, 'utf-8');
|
|
319
|
+
applied.push({ path: filename, created });
|
|
320
|
+
}
|
|
321
|
+
return applied;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
async function uninstall() {
|
|
325
|
+
const configDir = path.join(os.homedir(), '.primocode');
|
|
326
|
+
console.log('Removendo PrimoCode…');
|
|
327
|
+
try {
|
|
328
|
+
if (fs.existsSync(configDir)) {
|
|
329
|
+
fs.rmSync(configDir, { recursive: true, force: true });
|
|
330
|
+
console.log(' configurações removidas');
|
|
331
|
+
}
|
|
332
|
+
} catch (e) {
|
|
333
|
+
console.log(' não consegui remover as configurações:', e.message);
|
|
334
|
+
}
|
|
335
|
+
const r = await runShell('npm uninstall -g primocode');
|
|
336
|
+
console.log(r.ok ? ' pacote global removido' : ' remova com: npm uninstall -g primocode');
|
|
337
|
+
console.log('\nSeus projetos em ~/primocode continuam lá.');
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
module.exports = {
|
|
341
|
+
fmtBytes, treeDir, applyCodeBlocks, runShell, openInBrowser, runInNewTerminal,
|
|
342
|
+
execute, uninstall, resolveInProject, globToRegex, globFiles, grepFiles, editFile,
|
|
343
|
+
};
|
package/lib/ui.js
CHANGED
|
@@ -36,24 +36,31 @@ const c = {
|
|
|
36
36
|
bold: (s) => `${PALETTE.bold}${s}${PALETTE.reset}`,
|
|
37
37
|
};
|
|
38
38
|
|
|
39
|
-
// Primo — o mascote.
|
|
40
|
-
|
|
39
|
+
// Primo — o mascote. Antena que brilha, olhos acesos, sorriso discreto.
|
|
40
|
+
function mascot() {
|
|
41
|
+
return [
|
|
42
|
+
` ${c.dim('·')} ${c.magenta('✦')} ${c.dim('·')} `,
|
|
43
|
+
` ${c.brand('╭─────╮')} `,
|
|
44
|
+
`${c.brandDim('─')}${c.brand('┤')} ${c.cyan('◉')} ${c.cyan('◉')} ${c.brand('├')}${c.brandDim('─')}`,
|
|
45
|
+
` ${c.brand('│')} ${c.magenta('‿')} ${c.brand('│')} `,
|
|
46
|
+
` ${c.brand('╰─┬─┬─╯')} `,
|
|
47
|
+
];
|
|
48
|
+
}
|
|
41
49
|
|
|
42
|
-
function welcomeBox(version,
|
|
50
|
+
function welcomeBox(version, contexto) {
|
|
43
51
|
const side = [
|
|
44
|
-
|
|
45
|
-
c.
|
|
46
|
-
c.muted(
|
|
52
|
+
'',
|
|
53
|
+
`${c.bold(c.brand('P R I M O C O D E'))}`,
|
|
54
|
+
`${c.muted('agente de engenharia')} ${c.dim('v' + version)}`,
|
|
55
|
+
contexto ? c.brandDim(contexto) : c.dim('nenhuma pasta ativa'),
|
|
56
|
+
'',
|
|
47
57
|
];
|
|
48
|
-
const rows =
|
|
49
|
-
const text = i === 0 ? '' : side[i - 1] || '';
|
|
50
|
-
return ` ${c.brand(art)} ${text}`;
|
|
51
|
-
});
|
|
58
|
+
const rows = mascot().map((art, i) => ` ${art} ${side[i] || ''}`);
|
|
52
59
|
return [
|
|
53
60
|
'',
|
|
54
61
|
...rows,
|
|
55
62
|
'',
|
|
56
|
-
` ${c.magenta('●')} ${c.muted('Primo Code')} ${c.brand('●')} ${c.muted('Chat')} ${c.dim('/help
|
|
63
|
+
` ${c.magenta('●')} ${c.muted('Primo Code')} ${c.brand('●')} ${c.muted('Chat')} ${c.dim('/help')}`,
|
|
57
64
|
'',
|
|
58
65
|
].join('\n');
|
|
59
66
|
}
|
|
@@ -112,19 +119,13 @@ class ProgressBar {
|
|
|
112
119
|
}
|
|
113
120
|
|
|
114
121
|
function actionHeader(name, args) {
|
|
115
|
-
const nice = {
|
|
116
|
-
read_file:'Read', write_file:'Write',
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
browser_key:'Key', browser_wait_for:'Wait', browser_wait:'Wait',
|
|
120
|
-
browser_screenshot:'Screenshot', browser_eval:'Eval', browser_scroll:'Scroll',
|
|
121
|
-
browser_url:'URL', finish:'Finish',
|
|
122
|
-
desktop_click:'Click', desktop_move:'Move', desktop_drag:'Drag',
|
|
123
|
-
desktop_type:'Type', desktop_key:'Key', desktop_scroll:'Scroll',
|
|
124
|
-
desktop_screenshot:'Screenshot', desktop_screen_size:'Screen',
|
|
122
|
+
const nice = {
|
|
123
|
+
read_file:'Read', write_file:'Write', edit_file:'Edit', list_dir:'List',
|
|
124
|
+
glob:'Glob', grep:'Grep', run_shell:'Bash', run_in_new_terminal:'Terminal',
|
|
125
|
+
open_in_browser:'Open', spawn_agent:'Agent', finish:'Finish',
|
|
125
126
|
};
|
|
126
127
|
const label = nice[name] || name;
|
|
127
|
-
const argStr = args ? (args.path || args.
|
|
128
|
+
const argStr = args ? (args.path || args.pattern || args.command || args.target || args.description || JSON.stringify(args).slice(0,60)) : '';
|
|
128
129
|
return ` ${c.dim('⎿')} ${c.brand(label)}${argStr ? ' '+c.muted(argStr) : ''}`;
|
|
129
130
|
}
|
|
130
131
|
|
package/package.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "primocode",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.3.0",
|
|
4
4
|
"description": "PrimoCode — agente de engenharia com IA e cursor próprio. Modo primocode cria arquivos, controla o desktop e navegador. Modo chat é conversa pura.",
|
|
5
5
|
"main": "bin/primocode.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"primocode": "bin/primocode.js"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
|
-
"start": "node bin/primocode.js"
|
|
10
|
+
"start": "node bin/primocode.js",
|
|
11
|
+
"test": "node test/tools.test.js"
|
|
11
12
|
},
|
|
12
13
|
"engines": {
|
|
13
14
|
"node": ">=18.17.0"
|