primocode 7.3.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/lib/config.js CHANGED
@@ -163,6 +163,9 @@ function loadConfig() {
163
163
  token: process.env.PRIMOCODE_TOKEN || file.token || null,
164
164
  autoApply: file.autoApply === true,
165
165
  currentProject: file.currentProject || null,
166
+ userName: file.userName || null,
167
+ mascot: file.mascot || 'primo',
168
+ theme: file.theme || 'primo',
166
169
  rootDir: ROOT_DIR,
167
170
  _file: file,
168
171
  };
@@ -172,8 +175,8 @@ function saveConfig(partial) {
172
175
  ensureDir(CONFIG_DIR);
173
176
  const cur = (() => { try { return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8')); } catch { return {}; } })();
174
177
  // Só persiste o que faz sentido guardar — nada de campos derivados.
175
- const { server, model, token, autoApply, currentProject } = { ...cur, ...partial };
176
- const next = { server, model, token, autoApply, currentProject };
178
+ const { server, model, token, autoApply, currentProject, userName, mascot, theme } = { ...cur, ...partial };
179
+ const next = { server, model, token, autoApply, currentProject, userName, mascot, theme };
177
180
  fs.writeFileSync(CONFIG_FILE, JSON.stringify(next, null, 2), 'utf8');
178
181
  return next;
179
182
  }
package/lib/desktop.js CHANGED
@@ -1,142 +1,189 @@
1
- /**
2
- * desktop.js — Controle nativo do mouse/teclado do sistema operacional.
3
- * Zero dependências npm. Usa ferramentas nativas do SO:
4
- * Linux: xdotool
5
- * macOS: cliclick (brew install cliclick) ou osascript
6
- * Windows: powershell + System.Windows.Forms
7
- */
8
-
9
- 'use strict';
10
-
11
- const { execSync } = require('child_process');
12
- const os = require('os');
13
-
14
- const PLATFORM = os.platform();
15
-
16
- function hasCmd(cmd) {
17
- try { execSync(`which ${cmd} || where ${cmd}`, { stdio: 'ignore' }); return true; } catch { return false; }
18
- }
19
-
20
- // Detectar ferramenta disponível
21
- const TOOLS = {
22
- xdotool: PLATFORM === 'linux' && hasCmd('xdotool'),
23
- cliclick: PLATFORM === 'darwin' && hasCmd('cliclick'),
24
- nircmd: PLATFORM === 'win32' && (() => { try { execSync('where nircmd', {stdio:'ignore'}); return true; } catch { return false; } })(),
25
- };
26
-
27
- function run(cmd) {
28
- try { execSync(cmd, { stdio: 'ignore', timeout: 10000 }); return { ok: true }; }
29
- catch (e) { return { ok: false, error: e.message }; }
30
- }
31
-
32
- // ── Mouse ─────────────────────────────────────────────
33
- function click(x, y, button = 'left') {
34
- if (TOOLS.xdotool) return run(`xdotool mousemove ${x} ${y} click ${button === 'right' ? 3 : 1}`);
35
- if (TOOLS.cliclick) return run(`cliclick c:${x},${y}`);
36
- if (PLATFORM === 'win32') {
37
- const b = button === 'right' ? 'Right' : 'Left';
38
- const ps = `Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.Cursor]::Position = New-Object System.Drawing.Point(${x}, ${y}); [System.Windows.Forms.MouseButtons]::${b} -bor 0`;
39
- return run(`powershell -Command "${ps}"`);
40
- }
41
- return { ok: false, error: 'Nenhuma ferramenta de automação encontrada. Instale xdotool (Linux), cliclick (Mac) ou configure o modo browser.' };
42
- }
43
-
44
- function move(x, y) {
45
- if (TOOLS.xdotool) return run(`xdotool mousemove ${x} ${y}`);
46
- if (TOOLS.cliclick) return run(`cliclick m:${x},${y}`);
47
- if (PLATFORM === 'win32') {
48
- const ps = `Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.Cursor]::Position = New-Object System.Drawing.Point(${x}, ${y})`;
49
- return run(`powershell -Command "${ps}"`);
50
- }
51
- return { ok: false, error: 'Automação não disponível' };
52
- }
53
-
54
- function drag(fromX, fromY, toX, toY) {
55
- if (TOOLS.xdotool) {
56
- return run(`xdotool mousemove ${fromX} ${fromY} mousedown 1 mousemove ${toX} ${toY} mouseup 1`);
57
- }
58
- if (TOOLS.cliclick) {
59
- return run(`cliclick dd:${fromX},${fromY} du:${toX},${toY}`);
60
- }
61
- return { ok: false, error: 'Drag não suportado nesta plataforma sem xdotool/cliclick' };
62
- }
63
-
64
- function scroll(dy) {
65
- if (TOOLS.xdotool) return run(`xdotool click --repeat ${Math.abs(Math.round(dy / 50))} ${dy > 0 ? 5 : 4}`);
66
- if (TOOLS.cliclick) return run(`cliclick scroll:${Math.round(dy / 50)}`);
67
- return { ok: false, error: 'Scroll não suportado' };
68
- }
69
-
70
- // ── Teclado ───────────────────────────────────────────
71
- function type(text) {
72
- if (TOOLS.xdotool) return run(`xdotool type --delay 10 '${text.replace(/'/g, "'\''")}'`);
73
- if (TOOLS.cliclick) {
74
- const safe = text.replace(/"/g, '\\"');
75
- return run(`osascript -e 'tell application "System Events" to keystroke "${safe}"'`);
76
- }
77
- if (PLATFORM === 'win32') {
78
- const ps = `Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.SendKeys]::SendWait('${text.replace(/'/g, "''")}')`;
79
- return run(`powershell -Command "${ps}"`);
80
- }
81
- return { ok: false, error: 'Automação de teclado não disponível' };
82
- }
83
-
84
- function key(name) {
85
- const map = {
86
- Enter: 'Return', Tab: 'Tab', Escape: 'Escape', Backspace: 'Backspace',
87
- ArrowUp: 'Up', ArrowDown: 'Down', ArrowLeft: 'Left', ArrowRight: 'Right',
88
- Space: 'space', Delete: 'Delete',
89
- };
90
- const k = map[name] || name;
91
- if (TOOLS.xdotool) return run(`xdotool key ${k}`);
92
- if (PLATFORM === 'darwin') return run(`osascript -e 'tell application "System Events" to key code ${k}'`);
93
- if (PLATFORM === 'win32') {
94
- const ps = `Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.SendKeys]::SendWait('{${name.toUpperCase()}}')`;
95
- return run(`powershell -Command "${ps}"`);
96
- }
97
- return { ok: false, error: 'Key não suportado' };
98
- }
99
-
100
- // ── Screenshot ────────────────────────────────────────
101
- function screenshot(saveTo) {
102
- const filename = saveTo || `desktop-${Date.now()}.png`;
103
- if (PLATFORM === 'darwin') {
104
- return run(`screencapture -x ${filename}`) ? { ok: true, path: filename } : { ok: false, error: 'screencapture falhou' };
105
- }
106
- if (PLATFORM === 'win32') {
107
- const ps = `Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.SendKeys]::SendWait('{PRTSC}'); Add-Type -AssemblyName System.Drawing; \$bmp = New-Object System.Drawing.Bitmap([System.Windows.Forms.Screen]::PrimaryScreen.Bounds.Width, [System.Windows.Forms.Screen]::PrimaryScreen.Bounds.Height); \$graphics = [System.Drawing.Graphics]::FromImage(\$bmp); \$graphics.CopyFromScreen([System.Windows.Forms.Screen]::PrimaryScreen.Bounds.Location, [System.Drawing.Point]::Empty, \$bmp.Size); \$bmp.Save('${filename}'); \$graphics.Dispose(); \$bmp.Dispose()`;
108
- return run(`powershell -Command "${ps}"`) ? { ok: true, path: filename } : { ok: false, error: 'Screenshot falhou' };
109
- }
110
- return { ok: false, error: 'Screenshot desktop não suportado nesta plataforma. Instale scrot ou gnome-screenshot.' };
111
- }
112
-
113
- function getScreenSize() {
114
- if (PLATFORM === 'win32') {
115
- try {
116
- const out = execSync(`powershell -Command "Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.Screen]::PrimaryScreen.Bounds.Size.Width.ToString() + ',' + [System.Windows.Forms.Screen]::PrimaryScreen.Bounds.Size.Height.ToString()"`).toString().trim();
117
- const [w, h] = out.split(',').map(Number);
118
- return { ok: true, width: w, height: h };
119
- } catch { }
120
- }
121
- if (PLATFORM === 'darwin') {
122
- try {
123
- const out = execSync(`system_profiler SPDisplaysDataType | grep Resolution`).toString();
124
- const m = out.match(/(\d+)\s*x\s*(\d+)/);
125
- if (m) return { ok: true, width: parseInt(m[1]), height: parseInt(m[2]) };
126
- } catch { }
127
- }
128
- if (TOOLS.xdotool) {
129
- try {
130
- const wh = execSync(`xdotool getdisplaygeometry`).toString().trim().split(' ');
131
- return { ok: true, width: parseInt(wh[0]), height: parseInt(wh[1]) };
132
- } catch { }
133
- }
134
- return { ok: false, error: 'Não foi possível detectar resolução', width: 1920, height: 1080 };
135
- }
136
-
137
- module.exports = {
138
- click, move, drag, scroll,
139
- type, key,
140
- screenshot, getScreenSize,
141
- TOOLS,
142
- };
1
+ /**
2
+ * desktop.js — Controle nativo do mouse/teclado do sistema operacional.
3
+ * Zero dependências npm. Usa ferramentas nativas do SO:
4
+ * Linux: xdotool
5
+ * macOS: cliclick (brew install cliclick) ou osascript
6
+ * Windows: powershell + System.Windows.Forms
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ const { execSync } = require('child_process');
12
+ const os = require('os');
13
+
14
+ const PLATFORM = os.platform();
15
+
16
+ function hasCmd(cmd) {
17
+ try { execSync(`which ${cmd} || where ${cmd}`, { stdio: 'ignore' }); return true; } catch { return false; }
18
+ }
19
+
20
+ // Detectar ferramenta disponível
21
+ const TOOLS = {
22
+ xdotool: PLATFORM === 'linux' && hasCmd('xdotool'),
23
+ cliclick: PLATFORM === 'darwin' && hasCmd('cliclick'),
24
+ nircmd: PLATFORM === 'win32' && (() => { try { execSync('where nircmd', {stdio:'ignore'}); return true; } catch { return false; } })(),
25
+ };
26
+
27
+ function run(cmd) {
28
+ try { execSync(cmd, { stdio: 'ignore', timeout: 10000 }); return { ok: true }; }
29
+ catch (e) { return { ok: false, error: e.message }; }
30
+ }
31
+
32
+ // PowerShell sem perfil; escapa aspas duplas para caber em -Command "...".
33
+ function pwsh(script) {
34
+ return run(`powershell -NoProfile -Command "${script.replace(/"/g, '\\"')}"`);
35
+ }
36
+
37
+ // Clique/scroll de verdade no Windows precisam de mouse_event (user32).
38
+ // A abordagem antiga só posicionava o cursor — nunca clicava.
39
+ const WIN_USER32 = "Add-Type -Name U -Namespace W -MemberDefinition '[DllImport(\\\"user32.dll\\\")] public static extern bool SetCursorPos(int X,int Y); [DllImport(\\\"user32.dll\\\")] public static extern void mouse_event(uint f,uint dx,uint dy,uint d,int e);';";
40
+
41
+ // Primeira ferramenta cujo comando existe no PATH.
42
+ function firstCmd(list) {
43
+ for (const c of list) if (hasCmd(c)) return c;
44
+ return null;
45
+ }
46
+
47
+ // ── Mouse ─────────────────────────────────────────────
48
+ function click(x, y, button = 'left') {
49
+ if (TOOLS.xdotool) return run(`xdotool mousemove ${x} ${y} click ${button === 'right' ? 3 : 1}`);
50
+ if (TOOLS.cliclick) return run(`cliclick c:${x},${y}`);
51
+ if (PLATFORM === 'win32') {
52
+ const down = button === 'right' ? '0x0008' : '0x0002';
53
+ const up = button === 'right' ? '0x0010' : '0x0004';
54
+ return pwsh(`${WIN_USER32} [W.U]::SetCursorPos(${x},${y}); [W.U]::mouse_event(${down},0,0,0,0); [W.U]::mouse_event(${up},0,0,0,0)`);
55
+ }
56
+ return { ok: false, error: 'Nenhuma ferramenta de automação encontrada. Instale xdotool (Linux) ou cliclick (macOS), ou use as tools browser_*.' };
57
+ }
58
+
59
+ function move(x, y) {
60
+ if (TOOLS.xdotool) return run(`xdotool mousemove ${x} ${y}`);
61
+ if (TOOLS.cliclick) return run(`cliclick m:${x},${y}`);
62
+ if (PLATFORM === 'win32') {
63
+ return pwsh(`${WIN_USER32} [W.U]::SetCursorPos(${x},${y})`);
64
+ }
65
+ return { ok: false, error: 'Automação não disponível' };
66
+ }
67
+
68
+ function drag(fromX, fromY, toX, toY) {
69
+ if (TOOLS.xdotool) {
70
+ return run(`xdotool mousemove ${fromX} ${fromY} mousedown 1 mousemove ${toX} ${toY} mouseup 1`);
71
+ }
72
+ if (TOOLS.cliclick) {
73
+ return run(`cliclick dd:${fromX},${fromY} du:${toX},${toY}`);
74
+ }
75
+ return { ok: false, error: 'Drag não suportado nesta plataforma sem xdotool/cliclick' };
76
+ }
77
+
78
+ function scroll(dy) {
79
+ if (TOOLS.xdotool) return run(`xdotool click --repeat ${Math.max(1, Math.abs(Math.round(dy / 50)))} ${dy > 0 ? 5 : 4}`);
80
+ if (TOOLS.cliclick) return run(`cliclick scroll:${Math.round(dy / 50)}`);
81
+ if (PLATFORM === 'win32') {
82
+ // Roda do mouse: 120 = um "clique". dy>0 rola para baixo (delta negativo).
83
+ const delta = -Math.round(dy / 50) * 120;
84
+ return pwsh(`${WIN_USER32} [W.U]::mouse_event(0x0800,0,0,${delta},0)`);
85
+ }
86
+ return { ok: false, error: 'Scroll não suportado nesta plataforma' };
87
+ }
88
+
89
+ // ── Teclado ───────────────────────────────────────────
90
+ function type(text) {
91
+ if (TOOLS.xdotool) return run(`xdotool type --delay 10 '${text.replace(/'/g, "'\''")}'`);
92
+ if (TOOLS.cliclick) {
93
+ const safe = text.replace(/"/g, '\\"');
94
+ return run(`osascript -e 'tell application "System Events" to keystroke "${safe}"'`);
95
+ }
96
+ if (PLATFORM === 'win32') {
97
+ const ps = `Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.SendKeys]::SendWait('${text.replace(/'/g, "''")}')`;
98
+ return run(`powershell -Command "${ps}"`);
99
+ }
100
+ return { ok: false, error: 'Automação de teclado não disponível' };
101
+ }
102
+
103
+ function key(name) {
104
+ // xdotool usa nomes; cada SO tem seu dialeto — traduzimos por plataforma.
105
+ const xmap = {
106
+ Enter: 'Return', Return: 'Return', Tab: 'Tab', Escape: 'Escape', Esc: 'Escape',
107
+ Backspace: 'BackSpace', ArrowUp: 'Up', ArrowDown: 'Down', ArrowLeft: 'Left',
108
+ ArrowRight: 'Right', Space: 'space', Delete: 'Delete',
109
+ };
110
+ if (TOOLS.xdotool) return run(`xdotool key ${xmap[name] || name}`);
111
+
112
+ if (PLATFORM === 'darwin') {
113
+ // macOS: key code exige NÚMERO. Mapeamos as teclas comuns.
114
+ const mac = { Enter: 36, Return: 36, Tab: 48, Escape: 53, Esc: 53, Backspace: 51, Delete: 51, Space: 49, ArrowLeft: 123, ArrowRight: 124, ArrowDown: 125, ArrowUp: 126 };
115
+ if (mac[name] != null) return run(`osascript -e 'tell application "System Events" to key code ${mac[name]}'`);
116
+ // Atalho com modificadores, ex "cmd+c": usa keystroke.
117
+ return run(`osascript -e 'tell application "System Events" to keystroke "${String(name).replace(/"/g, '\\"')}"'`);
118
+ }
119
+ if (PLATFORM === 'win32') {
120
+ const win = { Enter: '{ENTER}', Return: '{ENTER}', Tab: '{TAB}', Escape: '{ESC}', Esc: '{ESC}', Backspace: '{BACKSPACE}', Delete: '{DEL}', Space: ' ', ArrowUp: '{UP}', ArrowDown: '{DOWN}', ArrowLeft: '{LEFT}', ArrowRight: '{RIGHT}' };
121
+ const seq = win[name] || name;
122
+ return pwsh(`Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.SendKeys]::SendWait('${seq.replace(/'/g, "''")}')`);
123
+ }
124
+ return { ok: false, error: 'Key não suportado nesta plataforma' };
125
+ }
126
+
127
+ // ── Screenshot ────────────────────────────────────────
128
+ function screenshot(saveTo) {
129
+ const filename = saveTo || `desktop-${Date.now()}.png`;
130
+ const done = (r) => (r.ok ? { ok: true, path: filename, bytes: sizeOf(filename) } : r);
131
+
132
+ if (PLATFORM === 'darwin') {
133
+ return done(run(`screencapture -x "${filename}"`));
134
+ }
135
+ if (PLATFORM === 'win32') {
136
+ const ps = `Add-Type -AssemblyName System.Drawing; Add-Type -AssemblyName System.Windows.Forms; $b=[System.Windows.Forms.Screen]::PrimaryScreen.Bounds; $bmp=New-Object System.Drawing.Bitmap($b.Width,$b.Height); $g=[System.Drawing.Graphics]::FromImage($bmp); $g.CopyFromScreen($b.Location,[System.Drawing.Point]::Empty,$bmp.Size); $bmp.Save('${filename}'); $g.Dispose(); $bmp.Dispose()`;
137
+ return done(pwsh(ps));
138
+ }
139
+ // Linux: tenta as ferramentas comuns, na ordem.
140
+ const scrot = firstCmd(['scrot', 'gnome-screenshot', 'spectacle', 'import', 'maim']);
141
+ if (scrot === 'scrot') return done(run(`scrot -o "${filename}"`));
142
+ if (scrot === 'maim') return done(run(`maim "${filename}"`));
143
+ if (scrot === 'gnome-screenshot') return done(run(`gnome-screenshot -f "${filename}"`));
144
+ if (scrot === 'spectacle') return done(run(`spectacle -b -n -o "${filename}"`));
145
+ if (scrot === 'import') return done(run(`import -window root "${filename}"`)); // ImageMagick
146
+ return { ok: false, error: 'Screenshot indisponível no Linux. Instale scrot, gnome-screenshot, spectacle, maim ou ImageMagick (import).' };
147
+ }
148
+
149
+ function sizeOf(f) {
150
+ try { return require('fs').statSync(f).size; } catch { return 0; }
151
+ }
152
+
153
+ function getScreenSize() {
154
+ if (PLATFORM === 'win32') {
155
+ try {
156
+ const out = execSync(`powershell -Command "Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.Screen]::PrimaryScreen.Bounds.Size.Width.ToString() + ',' + [System.Windows.Forms.Screen]::PrimaryScreen.Bounds.Size.Height.ToString()"`).toString().trim();
157
+ const [w, h] = out.split(',').map(Number);
158
+ return { ok: true, width: w, height: h };
159
+ } catch { }
160
+ }
161
+ if (PLATFORM === 'darwin') {
162
+ try {
163
+ const out = execSync(`system_profiler SPDisplaysDataType | grep Resolution`).toString();
164
+ const m = out.match(/(\d+)\s*x\s*(\d+)/);
165
+ if (m) return { ok: true, width: parseInt(m[1]), height: parseInt(m[2]) };
166
+ } catch { }
167
+ }
168
+ if (TOOLS.xdotool) {
169
+ try {
170
+ const wh = execSync(`xdotool getdisplaygeometry`).toString().trim().split(' ');
171
+ return { ok: true, width: parseInt(wh[0]), height: parseInt(wh[1]) };
172
+ } catch { }
173
+ }
174
+ if (PLATFORM === 'linux') {
175
+ try {
176
+ const out = execSync(`xrandr --current 2>/dev/null | grep '\\*' | head -1`).toString();
177
+ const m = out.match(/(\d+)\s*x\s*(\d+)/);
178
+ if (m) return { ok: true, width: parseInt(m[1]), height: parseInt(m[2]) };
179
+ } catch { }
180
+ }
181
+ return { ok: false, error: 'Não foi possível detectar resolução', width: 1920, height: 1080 };
182
+ }
183
+
184
+ module.exports = {
185
+ click, move, drag, scroll,
186
+ type, key,
187
+ screenshot, getScreenSize,
188
+ TOOLS,
189
+ };
package/lib/tools.js CHANGED
@@ -11,6 +11,8 @@ const fs = require('fs');
11
11
  const path = require('path');
12
12
  const os = require('os');
13
13
  const { exec } = require('child_process');
14
+ const browser = require('./browser');
15
+ const desktop = require('./desktop');
14
16
 
15
17
  const IGNORAR = new Set(['node_modules', '.git', 'dist', 'build', '.next', 'coverage', '.cache', 'vendor']);
16
18
  const MAX_ARQUIVOS_VARREDURA = 5000;
@@ -26,6 +28,11 @@ function fmtBytes(n) {
26
28
  // "../../.." viraria escrita fora da área do projeto.
27
29
  function resolveInProject(rawPath, projectDir) {
28
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
+ }
29
36
  const target = path.resolve(base, rawPath || '.');
30
37
  const rel = path.relative(base, target);
31
38
  if (rel.startsWith('..') || path.isAbsolute(rel)) {
@@ -259,6 +266,57 @@ function readFile({ path: rawPath, offset, limit }, projectDir) {
259
266
  return { ok: true, content: recorte, linhas: todas.length, de: inicio + 1, ate: fim, bytes: bruto.length };
260
267
  }
261
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
+
262
320
  async function execute(name, args, ctx) {
263
321
  const projectDir = (ctx && ctx.projectDir) || process.cwd();
264
322
  args = args || {};
@@ -295,6 +353,60 @@ async function execute(name, args, ctx) {
295
353
  : resolveInProject(args.target, projectDir);
296
354
  return openInBrowser(alvo);
297
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
+
298
410
  default:
299
411
  return { ok: false, error: `Tool não implementada: ${name}` };
300
412
  }
@@ -340,4 +452,5 @@ async function uninstall() {
340
452
  module.exports = {
341
453
  fmtBytes, treeDir, applyCodeBlocks, runShell, openInBrowser, runInNewTerminal,
342
454
  execute, uninstall, resolveInProject, globToRegex, globFiles, grepFiles, editFile,
455
+ multiEdit, writeTodos, getTodos,
343
456
  };