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/lib/tools.js
CHANGED
|
@@ -1,147 +1,456 @@
|
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
try {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
const
|
|
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
|
+
const browser = require('./browser');
|
|
15
|
+
const desktop = require('./desktop');
|
|
16
|
+
|
|
17
|
+
const IGNORAR = new Set(['node_modules', '.git', 'dist', 'build', '.next', 'coverage', '.cache', 'vendor']);
|
|
18
|
+
const MAX_ARQUIVOS_VARREDURA = 5000;
|
|
19
|
+
|
|
20
|
+
function fmtBytes(n) {
|
|
21
|
+
if (n < 1024) return `${n} B`;
|
|
22
|
+
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
|
|
23
|
+
return `${(n / (1024 * 1024)).toFixed(1)} MB`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Todo caminho vindo do modelo é resolvido DENTRO da pasta de trabalho.
|
|
27
|
+
// Sem isso o agente espalha arquivo por qualquer lugar do disco — e um
|
|
28
|
+
// "../../.." viraria escrita fora da área do projeto.
|
|
29
|
+
function resolveInProject(rawPath, projectDir) {
|
|
30
|
+
const base = projectDir || process.cwd();
|
|
31
|
+
// Letra de drive (C:\, D:/) é sempre absoluta — mesmo no Linux, onde o
|
|
32
|
+
// path.resolve a trataria como nome relativo e deixaria escapar da pasta.
|
|
33
|
+
if (/^[a-zA-Z]:[\\/]/.test(String(rawPath || ''))) {
|
|
34
|
+
throw new Error(`Caminho fora da pasta de trabalho: ${rawPath}`);
|
|
35
|
+
}
|
|
36
|
+
const target = path.resolve(base, rawPath || '.');
|
|
37
|
+
const rel = path.relative(base, target);
|
|
38
|
+
if (rel.startsWith('..') || path.isAbsolute(rel)) {
|
|
39
|
+
throw new Error(`Caminho fora da pasta de trabalho: ${rawPath}`);
|
|
40
|
+
}
|
|
41
|
+
return target;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function treeDir(dir, maxDepth = 2, currentDepth = 0, prefix = '') {
|
|
45
|
+
if (currentDepth > maxDepth) return '';
|
|
46
|
+
let result = '';
|
|
47
|
+
try {
|
|
48
|
+
const items = fs.readdirSync(dir);
|
|
49
|
+
const dirs = [], files = [];
|
|
50
|
+
for (const item of items) {
|
|
51
|
+
if (item.startsWith('.') && item !== '.env') continue;
|
|
52
|
+
if (IGNORAR.has(item)) continue;
|
|
53
|
+
(fs.statSync(path.join(dir, item)).isDirectory() ? dirs : files).push(item);
|
|
54
|
+
}
|
|
55
|
+
dirs.sort(); files.sort();
|
|
56
|
+
const all = [...dirs, ...files];
|
|
57
|
+
for (let i = 0; i < all.length; i++) {
|
|
58
|
+
const item = all[i];
|
|
59
|
+
const isLast = i === all.length - 1;
|
|
60
|
+
result += prefix + (isLast ? '└── ' : '├── ') + item + '\n';
|
|
61
|
+
const full = path.join(dir, item);
|
|
62
|
+
if (fs.statSync(full).isDirectory() && currentDepth < maxDepth) {
|
|
63
|
+
result += treeDir(full, maxDepth, currentDepth + 1, prefix + (isLast ? ' ' : '│ '));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
} catch { return ''; }
|
|
67
|
+
return result;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Percorre a árvore uma vez só; grep e glob se apoiam nisto.
|
|
71
|
+
function* walk(dir, base, profundidade = 0) {
|
|
72
|
+
if (profundidade > 12) return;
|
|
73
|
+
let entradas;
|
|
74
|
+
try { entradas = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
|
75
|
+
for (const e of entradas) {
|
|
76
|
+
if (IGNORAR.has(e.name)) continue;
|
|
77
|
+
if (e.name.startsWith('.') && e.name !== '.env') continue;
|
|
78
|
+
const full = path.join(dir, e.name);
|
|
79
|
+
if (e.isDirectory()) yield* walk(full, base, profundidade + 1);
|
|
80
|
+
else yield path.relative(base, full).replace(/\\/g, '/');
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Glob sem dependência externa: **, *, ? e {a,b}.
|
|
85
|
+
function globToRegex(pattern) {
|
|
86
|
+
let re = '';
|
|
87
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
88
|
+
const ch = pattern[i];
|
|
89
|
+
if (ch === '*') {
|
|
90
|
+
if (pattern[i + 1] === '*') {
|
|
91
|
+
// **/ some caminho inteiro, inclusive "nenhuma pasta"
|
|
92
|
+
if (pattern[i + 2] === '/') { re += '(?:.*/)?'; i += 2; }
|
|
93
|
+
else { re += '.*'; i += 1; }
|
|
94
|
+
} else re += '[^/]*';
|
|
95
|
+
} else if (ch === '?') re += '[^/]';
|
|
96
|
+
else if (ch === '{') {
|
|
97
|
+
const fim = pattern.indexOf('}', i);
|
|
98
|
+
if (fim === -1) re += '\\{';
|
|
99
|
+
else {
|
|
100
|
+
re += '(?:' + pattern.slice(i + 1, fim).split(',').map((s) => s.replace(/[.+^${}()|[\]\\]/g, '\\$&')).join('|') + ')';
|
|
101
|
+
i = fim;
|
|
102
|
+
}
|
|
103
|
+
} else re += ch.replace(/[.+^${}()|[\]\\]/g, '\\$&');
|
|
104
|
+
}
|
|
105
|
+
return new RegExp(`^${re}$`, 'i');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function globFiles(pattern, projectDir) {
|
|
109
|
+
const re = globToRegex(pattern);
|
|
110
|
+
const achados = [];
|
|
111
|
+
for (const rel of walk(projectDir, projectDir)) {
|
|
112
|
+
if (achados.length >= MAX_ARQUIVOS_VARREDURA) break;
|
|
113
|
+
if (re.test(rel)) achados.push(rel);
|
|
114
|
+
}
|
|
115
|
+
return achados.sort();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function pareceBinario(buf) {
|
|
119
|
+
const amostra = buf.slice(0, 1024);
|
|
120
|
+
for (let i = 0; i < amostra.length; i++) if (amostra[i] === 0) return true;
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function grepFiles({ pattern, glob, ignoreCase, contexto = 0, maxResultados = 100 }, projectDir) {
|
|
125
|
+
let re;
|
|
126
|
+
try { re = new RegExp(pattern, ignoreCase ? 'i' : ''); }
|
|
127
|
+
catch (e) { return { ok: false, error: `Regex inválida: ${e.message}` }; }
|
|
128
|
+
|
|
129
|
+
const filtro = glob ? globToRegex(glob) : null;
|
|
130
|
+
const resultados = [];
|
|
131
|
+
let arquivosVistos = 0;
|
|
132
|
+
|
|
133
|
+
for (const rel of walk(projectDir, projectDir)) {
|
|
134
|
+
if (resultados.length >= maxResultados) break;
|
|
135
|
+
if (filtro && !filtro.test(rel)) continue;
|
|
136
|
+
arquivosVistos++;
|
|
137
|
+
|
|
138
|
+
let buf;
|
|
139
|
+
try { buf = fs.readFileSync(path.join(projectDir, rel)); } catch { continue; }
|
|
140
|
+
if (buf.length > 2 * 1024 * 1024 || pareceBinario(buf)) continue;
|
|
141
|
+
|
|
142
|
+
const linhas = buf.toString('utf-8').split('\n');
|
|
143
|
+
for (let i = 0; i < linhas.length && resultados.length < maxResultados; i++) {
|
|
144
|
+
if (!re.test(linhas[i])) continue;
|
|
145
|
+
const de = Math.max(0, i - contexto);
|
|
146
|
+
const ate = Math.min(linhas.length - 1, i + contexto);
|
|
147
|
+
const trecho = [];
|
|
148
|
+
for (let j = de; j <= ate; j++) trecho.push(`${j + 1}${j === i ? ':' : '-'}${linhas[j]}`.slice(0, 300));
|
|
149
|
+
resultados.push({ arquivo: rel, linha: i + 1, trecho: trecho.join('\n') });
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return { ok: true, arquivosVistos, total: resultados.length, resultados, truncado: resultados.length >= maxResultados };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async function runShell(command, timeout = 60000, cwd) {
|
|
157
|
+
return new Promise((resolve) => {
|
|
158
|
+
exec(command, { timeout, cwd: cwd || process.cwd(), maxBuffer: 10 * 1024 * 1024 }, (error, stdout, stderr) => {
|
|
159
|
+
if (error) resolve({ ok: false, code: error.code ?? 1, stdout, stderr, killed: error.killed });
|
|
160
|
+
else resolve({ ok: true, code: 0, stdout, stderr });
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function openInBrowser(target) {
|
|
166
|
+
const p = os.platform();
|
|
167
|
+
const cmd = p === 'darwin' ? `open "${target}"` : p === 'win32' ? `start "" "${target}"` : `xdg-open "${target}"`;
|
|
168
|
+
const r = await runShell(cmd);
|
|
169
|
+
return r.ok ? { ok: true, opener: cmd.split(' ')[0] } : { ok: false, error: r.stderr || 'falhou ao abrir' };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function runInNewTerminal(command, cwd) {
|
|
173
|
+
const p = os.platform();
|
|
174
|
+
let cmd;
|
|
175
|
+
if (p === 'darwin') cmd = `osascript -e 'tell application "Terminal" to do script "cd ${cwd || '.'} && ${command}"'`;
|
|
176
|
+
else if (p === 'win32') cmd = `start cmd /k "cd /d ${cwd || '.'} && ${command}"`;
|
|
177
|
+
else cmd = `x-terminal-emulator -e "bash -c 'cd ${cwd || '.'} && ${command}; exec bash'"`;
|
|
178
|
+
const r = await runShell(cmd);
|
|
179
|
+
return r.ok ? { ok: true, opener: cmd.split(' ')[0] } : { ok: false, error: r.stderr || 'falhou' };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Quando o old_string não bate, mostra as linhas do arquivo mais parecidas —
|
|
183
|
+
// com número — para o modelo copiar o texto certo em vez de chutar de novo.
|
|
184
|
+
function trechosParecidos(conteudo, alvo, quantas = 8) {
|
|
185
|
+
const termos = alvo
|
|
186
|
+
.split(/[\s{}();,=]+/)
|
|
187
|
+
.map((t) => t.trim().toLowerCase())
|
|
188
|
+
.filter((t) => t.length > 2);
|
|
189
|
+
if (!termos.length) return conteudo.split('\n').slice(0, quantas).map((l, i) => `${i + 1}\t${l}`);
|
|
190
|
+
|
|
191
|
+
const linhas = conteudo.split('\n');
|
|
192
|
+
const pontuadas = linhas.map((linha, i) => {
|
|
193
|
+
const baixa = linha.toLowerCase();
|
|
194
|
+
let pontos = 0;
|
|
195
|
+
for (const t of termos) if (baixa.includes(t)) pontos++;
|
|
196
|
+
return { i, linha, pontos };
|
|
197
|
+
}).filter((x) => x.pontos > 0);
|
|
198
|
+
|
|
199
|
+
pontuadas.sort((a, b) => b.pontos - a.pontos || a.i - b.i);
|
|
200
|
+
return pontuadas
|
|
201
|
+
.slice(0, quantas)
|
|
202
|
+
.sort((a, b) => a.i - b.i)
|
|
203
|
+
.map((x) => `${x.i + 1}\t${x.linha.slice(0, 200)}`);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Edição cirúrgica: troca um trecho exato em vez de reescrever o arquivo.
|
|
207
|
+
// É o que impede o agente de reescrever 800 linhas para mudar uma.
|
|
208
|
+
function editFile({ path: rawPath, old_string, new_string, replace_all }, projectDir) {
|
|
209
|
+
const filePath = resolveInProject(rawPath, projectDir);
|
|
210
|
+
if (!fs.existsSync(filePath)) return { ok: false, error: `Arquivo não existe: ${rawPath}` };
|
|
211
|
+
if (typeof old_string !== 'string' || !old_string) return { ok: false, error: 'old_string é obrigatório' };
|
|
212
|
+
if (old_string === new_string) return { ok: false, error: 'old_string e new_string são iguais' };
|
|
213
|
+
|
|
214
|
+
const original = fs.readFileSync(filePath, 'utf-8');
|
|
215
|
+
const ocorrencias = original.split(old_string).length - 1;
|
|
216
|
+
|
|
217
|
+
if (ocorrencias === 0) {
|
|
218
|
+
// Erro seco faz o modelo chutar de novo. Devolvendo o que EXISTE no
|
|
219
|
+
// arquivo, ele acerta na segunda tentativa.
|
|
220
|
+
return {
|
|
221
|
+
ok: false,
|
|
222
|
+
error: 'Trecho não encontrado — o arquivo não contém esse texto exato.',
|
|
223
|
+
comoCorrigir: 'Copie o old_string LITERALMENTE de "trechosParecidos" abaixo, com a mesma indentação.',
|
|
224
|
+
trechosParecidos: trechosParecidos(original, old_string),
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
if (ocorrencias > 1 && !replace_all) {
|
|
228
|
+
return { ok: false, error: `Trecho aparece ${ocorrencias} vezes. Inclua mais linhas em volta para ficar único, ou use replace_all.` };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const atualizado = replace_all
|
|
232
|
+
? original.split(old_string).join(new_string ?? '')
|
|
233
|
+
: original.replace(old_string, new_string ?? '');
|
|
234
|
+
|
|
235
|
+
fs.writeFileSync(filePath, atualizado, 'utf-8');
|
|
236
|
+
return {
|
|
237
|
+
ok: true,
|
|
238
|
+
path: path.relative(projectDir, filePath).replace(/\\/g, '/'),
|
|
239
|
+
substituicoes: replace_all ? ocorrencias : 1,
|
|
240
|
+
linhasAntes: original.split('\n').length,
|
|
241
|
+
linhasDepois: atualizado.split('\n').length,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function readFile({ path: rawPath, offset, limit }, projectDir) {
|
|
246
|
+
const filePath = resolveInProject(rawPath, projectDir);
|
|
247
|
+
const bruto = fs.readFileSync(filePath, 'utf-8');
|
|
248
|
+
const todas = bruto.split('\n');
|
|
249
|
+
|
|
250
|
+
// Sem recorte pedido: devolve o começo, avisando que cortou.
|
|
251
|
+
if (offset == null && limit == null) {
|
|
252
|
+
const truncado = bruto.length > 40000;
|
|
253
|
+
return {
|
|
254
|
+
ok: true,
|
|
255
|
+
content: truncado ? bruto.slice(0, 40000) : bruto,
|
|
256
|
+
linhas: todas.length,
|
|
257
|
+
bytes: bruto.length,
|
|
258
|
+
truncado,
|
|
259
|
+
dica: truncado ? 'Use offset/limit para ler o resto.' : undefined,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const inicio = Math.max(0, (offset || 1) - 1);
|
|
264
|
+
const fim = Math.min(todas.length, inicio + (limit || 200));
|
|
265
|
+
const recorte = todas.slice(inicio, fim).map((l, i) => `${inicio + i + 1}\t${l}`).join('\n');
|
|
266
|
+
return { ok: true, content: recorte, linhas: todas.length, de: inicio + 1, ate: fim, bytes: bruto.length };
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// ── multi_edit: várias trocas no mesmo arquivo, em ordem, tudo-ou-nada ────
|
|
270
|
+
function multiEdit({ path: rawPath, edits }, projectDir) {
|
|
271
|
+
const filePath = resolveInProject(rawPath, projectDir);
|
|
272
|
+
if (!fs.existsSync(filePath)) return { ok: false, error: `Arquivo não existe: ${rawPath}` };
|
|
273
|
+
if (!Array.isArray(edits) || !edits.length) return { ok: false, error: 'edits (lista) é obrigatório' };
|
|
274
|
+
|
|
275
|
+
let conteudo = fs.readFileSync(filePath, 'utf-8');
|
|
276
|
+
const linhasAntes = conteudo.split('\n').length;
|
|
277
|
+
let substituicoes = 0;
|
|
278
|
+
|
|
279
|
+
for (let i = 0; i < edits.length; i++) {
|
|
280
|
+
const { old_string, new_string, replace_all } = edits[i] || {};
|
|
281
|
+
if (typeof old_string !== 'string' || !old_string) return { ok: false, error: `edit ${i + 1}: old_string é obrigatório` };
|
|
282
|
+
if (old_string === new_string) return { ok: false, error: `edit ${i + 1}: old_string e new_string são iguais` };
|
|
283
|
+
const ocorrencias = conteudo.split(old_string).length - 1;
|
|
284
|
+
if (ocorrencias === 0) {
|
|
285
|
+
return { ok: false, error: `edit ${i + 1}: trecho não encontrado`, trechosParecidos: trechosParecidos(conteudo, old_string) };
|
|
286
|
+
}
|
|
287
|
+
if (ocorrencias > 1 && !replace_all) {
|
|
288
|
+
return { ok: false, error: `edit ${i + 1}: trecho aparece ${ocorrencias}x — use replace_all ou inclua mais contexto para torná-lo único` };
|
|
289
|
+
}
|
|
290
|
+
conteudo = replace_all ? conteudo.split(old_string).join(new_string) : conteudo.replace(old_string, new_string);
|
|
291
|
+
substituicoes++;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
fs.writeFileSync(filePath, conteudo, 'utf-8');
|
|
295
|
+
return {
|
|
296
|
+
ok: true,
|
|
297
|
+
path: path.relative(projectDir, filePath).replace(/\\/g, '/'),
|
|
298
|
+
substituicoes,
|
|
299
|
+
linhasAntes,
|
|
300
|
+
linhasDepois: conteudo.split('\n').length,
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// ── write_todos: lista de tarefas da sessão (estilo Claude Code) ──────────
|
|
305
|
+
// Só estado + exibição — não chama nada externo. O agente planeja em voz alta
|
|
306
|
+
// e o usuário acompanha o progresso.
|
|
307
|
+
let TODOS = [];
|
|
308
|
+
function writeTodos({ todos }) {
|
|
309
|
+
if (!Array.isArray(todos)) return { ok: false, error: 'todos (lista) é obrigatório' };
|
|
310
|
+
const status = new Set(['pending', 'in_progress', 'completed']);
|
|
311
|
+
TODOS = todos.map((t) => ({
|
|
312
|
+
content: String((t && (t.content || t.task || t.text)) || '').trim(),
|
|
313
|
+
status: status.has(t && t.status) ? t.status : 'pending',
|
|
314
|
+
})).filter((t) => t.content);
|
|
315
|
+
const feitos = TODOS.filter((t) => t.status === 'completed').length;
|
|
316
|
+
return { ok: true, todos: TODOS, total: TODOS.length, feitos };
|
|
317
|
+
}
|
|
318
|
+
function getTodos() { return TODOS; }
|
|
319
|
+
|
|
320
|
+
async function execute(name, args, ctx) {
|
|
321
|
+
const projectDir = (ctx && ctx.projectDir) || process.cwd();
|
|
322
|
+
args = args || {};
|
|
323
|
+
|
|
324
|
+
try {
|
|
325
|
+
switch (name) {
|
|
326
|
+
case 'write_file': {
|
|
327
|
+
const filePath = resolveInProject(args.path, projectDir);
|
|
328
|
+
const content = args.content || '';
|
|
329
|
+
const created = !fs.existsSync(filePath);
|
|
330
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
331
|
+
fs.writeFileSync(filePath, content, 'utf-8');
|
|
332
|
+
return { ok: true, created, path: path.relative(projectDir, filePath).replace(/\\/g, '/'), bytes: content.length };
|
|
333
|
+
}
|
|
334
|
+
case 'edit_file':
|
|
335
|
+
return editFile(args, projectDir);
|
|
336
|
+
case 'read_file':
|
|
337
|
+
return readFile(args, projectDir);
|
|
338
|
+
case 'list_dir':
|
|
339
|
+
return { ok: true, tree: treeDir(resolveInProject(args.path, projectDir), args.depth || 2) };
|
|
340
|
+
case 'glob': {
|
|
341
|
+
const arquivos = globFiles(args.pattern || '**/*', projectDir);
|
|
342
|
+
return { ok: true, total: arquivos.length, arquivos: arquivos.slice(0, 200) };
|
|
343
|
+
}
|
|
344
|
+
case 'grep':
|
|
345
|
+
return grepFiles(args, projectDir);
|
|
346
|
+
case 'run_shell':
|
|
347
|
+
return runShell(args.command, args.timeout_ms || 60000, projectDir);
|
|
348
|
+
case 'run_in_new_terminal':
|
|
349
|
+
return runInNewTerminal(args.command, args.cwd || projectDir);
|
|
350
|
+
case 'open_in_browser': {
|
|
351
|
+
const alvo = /^https?:\/\//i.test(args.target || '')
|
|
352
|
+
? args.target
|
|
353
|
+
: resolveInProject(args.target, projectDir);
|
|
354
|
+
return openInBrowser(alvo);
|
|
355
|
+
}
|
|
356
|
+
case 'multi_edit':
|
|
357
|
+
return multiEdit(args, projectDir);
|
|
358
|
+
case 'write_todos':
|
|
359
|
+
return writeTodos(args);
|
|
360
|
+
|
|
361
|
+
// ── Navegador controlado (CDP) — o agente age numa página real ──
|
|
362
|
+
case 'browser_open': {
|
|
363
|
+
const alvo = /^https?:\/\//i.test(args.url || '')
|
|
364
|
+
? args.url
|
|
365
|
+
: 'file://' + resolveInProject(args.url, projectDir);
|
|
366
|
+
return browser.open(alvo);
|
|
367
|
+
}
|
|
368
|
+
case 'browser_click':
|
|
369
|
+
if (args.selector) return browser.clickSelector(args.selector);
|
|
370
|
+
if (args.text) return browser.clickText(args.text);
|
|
371
|
+
if (args.x != null && args.y != null) return browser.click(args.x, args.y);
|
|
372
|
+
return { ok: false, error: 'browser_click precisa de selector, text ou x/y' };
|
|
373
|
+
case 'browser_type':
|
|
374
|
+
if (args.selector) return browser.typeInSelector(args.selector, args.text || '', args.clear !== false);
|
|
375
|
+
return browser.type(args.text || '');
|
|
376
|
+
case 'browser_key':
|
|
377
|
+
return browser.key(args.key);
|
|
378
|
+
case 'browser_read': {
|
|
379
|
+
const sel = args.selector ? `document.querySelector(${JSON.stringify(args.selector)})` : 'document.body';
|
|
380
|
+
const texto = await browser.evaluate(`(function(){const e=${sel};return e?e.innerText.slice(0,6000):null;})()`);
|
|
381
|
+
return texto == null ? { ok: false, error: 'nada na página (ou seletor não encontrado)' } : { ok: true, text: texto };
|
|
382
|
+
}
|
|
383
|
+
case 'browser_screenshot':
|
|
384
|
+
return browser.screenshot(args.path ? resolveInProject(args.path, projectDir) : undefined);
|
|
385
|
+
case 'browser_scroll':
|
|
386
|
+
return browser.scroll(args.dy != null ? args.dy : 600);
|
|
387
|
+
case 'browser_wait':
|
|
388
|
+
return browser.waitFor(args.selector, args.timeout_ms || 15000);
|
|
389
|
+
case 'browser_eval':
|
|
390
|
+
return { ok: true, value: await browser.evaluate(args.js || 'null') };
|
|
391
|
+
|
|
392
|
+
// ── Desktop físico — mouse, teclado e tela do usuário ───────────
|
|
393
|
+
case 'desktop_screenshot':
|
|
394
|
+
return desktop.screenshot(args.path ? resolveInProject(args.path, projectDir) : undefined);
|
|
395
|
+
case 'desktop_click':
|
|
396
|
+
return desktop.click(args.x, args.y, args.button || 'left');
|
|
397
|
+
case 'desktop_move':
|
|
398
|
+
return desktop.move(args.x, args.y);
|
|
399
|
+
case 'desktop_drag':
|
|
400
|
+
return desktop.drag(args.from_x, args.from_y, args.to_x, args.to_y);
|
|
401
|
+
case 'desktop_type':
|
|
402
|
+
return desktop.type(args.text || '');
|
|
403
|
+
case 'desktop_key':
|
|
404
|
+
return desktop.key(args.key);
|
|
405
|
+
case 'desktop_scroll':
|
|
406
|
+
return desktop.scroll(args.dy != null ? args.dy : 300);
|
|
407
|
+
case 'desktop_size':
|
|
408
|
+
return desktop.getScreenSize();
|
|
409
|
+
|
|
410
|
+
default:
|
|
411
|
+
return { ok: false, error: `Tool não implementada: ${name}` };
|
|
412
|
+
}
|
|
413
|
+
} catch (e) {
|
|
414
|
+
return { ok: false, error: e.message };
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function applyCodeBlocks(response) {
|
|
419
|
+
const regex = /```[\w]*\n([\s\S]*?)```/g;
|
|
420
|
+
const applied = [];
|
|
421
|
+
let match;
|
|
422
|
+
while ((match = regex.exec(response || '')) !== null) {
|
|
423
|
+
const block = match[1];
|
|
424
|
+
const nome = block.match(/\/\/\s*filename:\s*(.+)/);
|
|
425
|
+
if (!nome) continue;
|
|
426
|
+
const filename = nome[1].trim();
|
|
427
|
+
const content = block.replace(/\/\/\s*filename:.+\n?/, '');
|
|
428
|
+
const created = !fs.existsSync(filename);
|
|
429
|
+
fs.mkdirSync(path.dirname(filename), { recursive: true });
|
|
430
|
+
fs.writeFileSync(filename, content, 'utf-8');
|
|
431
|
+
applied.push({ path: filename, created });
|
|
432
|
+
}
|
|
433
|
+
return applied;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
async function uninstall() {
|
|
437
|
+
const configDir = path.join(os.homedir(), '.primocode');
|
|
438
|
+
console.log('Removendo PrimoCode…');
|
|
439
|
+
try {
|
|
440
|
+
if (fs.existsSync(configDir)) {
|
|
441
|
+
fs.rmSync(configDir, { recursive: true, force: true });
|
|
442
|
+
console.log(' configurações removidas');
|
|
443
|
+
}
|
|
444
|
+
} catch (e) {
|
|
445
|
+
console.log(' não consegui remover as configurações:', e.message);
|
|
446
|
+
}
|
|
447
|
+
const r = await runShell('npm uninstall -g primocode');
|
|
448
|
+
console.log(r.ok ? ' pacote global removido' : ' remova com: npm uninstall -g primocode');
|
|
449
|
+
console.log('\nSeus projetos em ~/primocode continuam lá.');
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
module.exports = {
|
|
453
|
+
fmtBytes, treeDir, applyCodeBlocks, runShell, openInBrowser, runInNewTerminal,
|
|
454
|
+
execute, uninstall, resolveInProject, globToRegex, globFiles, grepFiles, editFile,
|
|
455
|
+
multiEdit, writeTodos, getTodos,
|
|
456
|
+
};
|