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/README.md +65 -8
- package/bin/primocode.js +54 -5
- package/lib/act.js +48 -6
- package/lib/browser.js +507 -492
- package/lib/config.js +5 -2
- package/lib/desktop.js +189 -142
- package/lib/tools.js +113 -0
- package/lib/ui.js +236 -165
- package/package.json +1 -1
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
|
-
//
|
|
33
|
-
function
|
|
34
|
-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
if (TOOLS.
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
if (TOOLS.xdotool) return run(`xdotool
|
|
92
|
-
if (
|
|
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
|
-
screenshot,
|
|
141
|
-
|
|
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
|
};
|