primocode 7.10.1 → 7.11.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/bin/primocode.js CHANGED
@@ -22,6 +22,7 @@ const cfg = require('../lib/config');
22
22
  const { runAct } = require('../lib/act');
23
23
  const pasta = require('../lib/pasta');
24
24
  const catalogo = require('../lib/catalogo');
25
+ const streaming = require('../lib/streaming');
25
26
 
26
27
  // A pasta de onde o comando foi dado. Não é mais onde ele trabalha (o padrão é
27
28
  // ~/primocode), mas continua servindo de ponto de partida para achar pasta que
@@ -55,6 +56,7 @@ ${c.bold('Comandos')}
55
56
  ${c.brand('/read')} <arquivo> mostra um arquivo
56
57
  ${c.brand('/run')} <comando> executa no shell do projeto
57
58
  ${c.brand('/open')} <arquivo|url> abre no navegador
59
+ ${c.brand('/streaming')} painel ao vivo — veja o agente mexendo no computador
58
60
  ${c.brand('/status')} checa o servidor
59
61
  ${c.brand('/name')} <nome> como o Primo te chama
60
62
  ${c.brand('/mascot')} <estilo> mascote: primo|fox|cat
@@ -465,7 +467,7 @@ async function handleSlash(text) {
465
467
  for (const g of catalogo.GRUPOS) {
466
468
  console.log(' ' + c.bold(c.white(g.nome)) + ' ' + ui.truncate(c.dim('— ' + g.resumo), cols - 4 - ui.visibleWidth(g.nome)));
467
469
  for (const i of g.itens) {
468
- console.log(' ' + c.brand(ui.padRight(i.tool, 21)) + ui.truncate(c.muted(i.faz), cols - 26));
470
+ console.log(' ' + c.brand(ui.padRight(i.tool, 23)) + ui.truncate(c.muted(i.faz), cols - 28));
469
471
  }
470
472
  console.log('');
471
473
  }
@@ -503,6 +505,33 @@ async function handleSlash(text) {
503
505
  break;
504
506
  }
505
507
 
508
+ // Painel ao vivo: um servidorzinho local mostra cada ação do agente na
509
+ // tela + a foto tirada logo depois dela. Liga/desliga; /streaming on|off
510
+ // também funciona.
511
+ case 'streaming': case 'stream': case 'live': {
512
+ const pedido = rest.toLowerCase();
513
+ const ligar = pedido === 'on' ? true : pedido === 'off' ? false : !streaming.isOn();
514
+
515
+ if (!ligar) {
516
+ if (streaming.isOn()) { streaming.stop(); console.log(c.ok('Streaming desligado.')); }
517
+ else console.log(c.muted('O streaming já estava desligado.'));
518
+ break;
519
+ }
520
+ if (streaming.isOn()) {
521
+ console.log(c.muted(`Já está ligado: ${streaming.url()}`));
522
+ break;
523
+ }
524
+ try {
525
+ const url = await streaming.start();
526
+ console.log(c.ok('Streaming ligado: ') + c.white(url));
527
+ console.log(c.muted(' Dê uma instrução e veja cada passo dele aí, ao vivo. /streaming de novo desliga.'));
528
+ await tools.openInBrowser(url);
529
+ } catch (e) {
530
+ console.log(c.err(`Não consegui ligar o streaming: ${e.message}`));
531
+ }
532
+ break;
533
+ }
534
+
506
535
  case 'name': {
507
536
  if (!rest) { console.log(c.muted(` Nome atual: ${state.config.userName || '(nenhum)'}`)); break; }
508
537
  state.config.userName = rest;
package/lib/act.js CHANGED
@@ -15,6 +15,7 @@ const tools = require('./tools');
15
15
  const api = require('./api');
16
16
  const cfg = require('./config');
17
17
  const catalogo = require('./catalogo');
18
+ const streaming = require('./streaming');
18
19
 
19
20
  const MAX_ITERATIONS = 15;
20
21
  const MAX_PROFUNDIDADE = 2; // principal + 1 nível de subagente
@@ -93,6 +94,27 @@ function logResult(kind, result, profundidade) {
93
94
  case 'browser_read':
94
95
  console.log(p + c.muted(`${(result.text || '').length} caracteres lidos da página`));
95
96
  return;
97
+ case 'desktop_windows': {
98
+ console.log(p + c.muted(`${result.total} janela${result.total === 1 ? '' : 's'}`));
99
+ for (const j of (result.janelas || []).slice(0, 5)) console.log(p + c.dim(`${j.app ? j.app + ' · ' : ''}${j.titulo}`.slice(0, 120)));
100
+ if ((result.janelas || []).length > 5) console.log(p + c.dim(`… +${result.janelas.length - 5}`));
101
+ return;
102
+ }
103
+ case 'desktop_elements': {
104
+ console.log(p + c.muted(`${result.total} elemento${result.total === 1 ? '' : 's'} em “${result.janela || 'janela em foco'}”`));
105
+ for (const e of (result.elementos || []).slice(0, 5)) console.log(p + c.dim(`${e.nome} (${e.x},${e.y})`.slice(0, 120)));
106
+ if ((result.elementos || []).length > 5) console.log(p + c.dim(`… +${result.elementos.length - 5}`));
107
+ return;
108
+ }
109
+ case 'desktop_click_element':
110
+ console.log(p + c.ok('ok') + c.muted(` · ${result.duplo ? 'clique duplo' : 'clique'} em “${result.clicado}” (${result.x},${result.y})`));
111
+ return;
112
+ case 'desktop_open_app':
113
+ console.log(p + c.ok('ok') + c.muted(` · ${result.app}`));
114
+ return;
115
+ case 'desktop_focus':
116
+ console.log(p + c.ok('ok') + c.muted(` · ${result.janela || 'em foco'}`));
117
+ return;
96
118
  case 'browser_screenshot':
97
119
  case 'desktop_screenshot':
98
120
  console.log(p + c.muted(`${result.path} · ${tools.fmtBytes(result.bytes || 0)}`));
@@ -104,6 +126,7 @@ function logResult(kind, result, profundidade) {
104
126
  case 'browser_wait':
105
127
  case 'browser_eval':
106
128
  case 'desktop_click':
129
+ case 'desktop_double_click':
107
130
  case 'desktop_move':
108
131
  case 'desktop_drag':
109
132
  case 'desktop_type':
@@ -257,6 +280,10 @@ async function runAct(userPrompt, opts) {
257
280
  const result = await tools.execute(tc.name, tc.arguments, { projectDir: pastaAtual });
258
281
  logResult(tc.name, result, profundidade);
259
282
 
283
+ // Painel ao vivo (/streaming): registra o passo e fotografa a tela.
284
+ // Nunca pode derrubar o loop — falha de foto é problema do painel.
285
+ try { streaming.step(tc.name, tc.arguments, result); } catch {}
286
+
260
287
  // Uma escrita bem-sucedida limpa a dívida: o agente se corrigiu.
261
288
  if (result && result.ok === false) falhasPendentes.push(`${tc.name}(${tc.arguments?.path || ''}): ${result.error}`);
262
289
  else if (['write_file', 'edit_file'].includes(tc.name)) falhasPendentes.length = 0;
package/lib/catalogo.js CHANGED
@@ -55,16 +55,22 @@ const GRUPOS = [
55
55
  },
56
56
  {
57
57
  nome: 'Computador',
58
- resumo: 'mouse, teclado e tela — age em qualquer programa, não só no navegador',
58
+ resumo: 'janelas, botões, mouse e teclado — age em qualquer programa e na área de trabalho',
59
59
  itens: [
60
- { tool: 'desktop_screenshot', escreve: true, faz: 'tira foto da sua tela' },
61
- { tool: 'desktop_click', escreve: false, faz: 'clica na tela' },
62
- { tool: 'desktop_move', escreve: false, faz: 'move o mouse' },
63
- { tool: 'desktop_drag', escreve: false, faz: 'arrasta de um ponto a outro' },
64
- { tool: 'desktop_type', escreve: false, faz: 'digita no programa que está na frente' },
65
- { tool: 'desktop_key', escreve: false, faz: 'aperta tecla ou atalho' },
66
- { tool: 'desktop_scroll', escreve: false, faz: 'rola a tela' },
67
- { tool: 'desktop_size', escreve: false, faz: 'mede a tela, para se orientar antes de clicar' },
60
+ { tool: 'desktop_open_app', escreve: false, faz: 'abre um programa pelo nome (Chrome, Bloco de Notas…)' },
61
+ { tool: 'desktop_windows', escreve: false, faz: 'lista as janelas abertas, com título' },
62
+ { tool: 'desktop_focus', escreve: false, faz: 'traz uma janela para a frente, pelo título' },
63
+ { tool: 'desktop_elements', escreve: false, faz: 'lê os botões, campos e ícones da janela, com posição' },
64
+ { tool: 'desktop_click_element', escreve: false, faz: 'clica num botão ou ícone pelo NOME, sem coordenada' },
65
+ { tool: 'desktop_click', escreve: false, faz: 'clica na tela por coordenada' },
66
+ { tool: 'desktop_double_click', escreve: false, faz: 'clique duplo — abre ícone da área de trabalho' },
67
+ { tool: 'desktop_move', escreve: false, faz: 'move o mouse' },
68
+ { tool: 'desktop_drag', escreve: false, faz: 'arrasta de um ponto a outro' },
69
+ { tool: 'desktop_type', escreve: false, faz: 'digita no programa que está na frente' },
70
+ { tool: 'desktop_key', escreve: false, faz: 'aperta tecla ou atalho (Enter, ctrl+c, alt+tab)' },
71
+ { tool: 'desktop_scroll', escreve: false, faz: 'rola a tela' },
72
+ { tool: 'desktop_size', escreve: false, faz: 'mede a tela, para se orientar antes de clicar' },
73
+ { tool: 'desktop_screenshot', escreve: true, faz: 'tira foto da sua tela' },
68
74
  ],
69
75
  },
70
76
  {
package/lib/desktop.js CHANGED
@@ -1,9 +1,14 @@
1
1
  /**
2
- * desktop.js — Controle nativo do mouse/teclado do sistema operacional.
2
+ * desktop.js — Controle nativo do computador: mouse, teclado, janelas e tela.
3
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
4
+ * Linux: xdotool (mouse/teclado/janelas) + scrot/maim/… (tela)
5
+ * macOS: cliclick (brew install cliclick) e osascript/System Events
6
+ * Windows: powershell (user32 + UI Automation) — nada a instalar
7
+ *
8
+ * A regra do módulo: o agente NÃO enxerga pixels (os modelos são só-texto),
9
+ * então ele enxerga por NOMES — as janelas abertas (listWindows), os botões e
10
+ * ícones de cada janela (uiElements) e o clique pelo nome (clickElement).
11
+ * Coordenada continua existindo, mas é o último recurso, não o primeiro.
7
12
  */
8
13
 
9
14
  'use strict';
@@ -24,19 +29,52 @@ const TOOLS = {
24
29
  nircmd: PLATFORM === 'win32' && (() => { try { execSync('where nircmd', {stdio:'ignore'}); return true; } catch { return false; } })(),
25
30
  };
26
31
 
27
- function run(cmd) {
28
- try { execSync(cmd, { stdio: 'ignore', timeout: 10000 }); return { ok: true }; }
29
- catch (e) { return { ok: false, error: e.message }; }
32
+ function run(cmd, opts = {}) {
33
+ try {
34
+ execSync(cmd, {
35
+ stdio: 'ignore',
36
+ timeout: opts.timeout || 15000,
37
+ env: opts.env ? { ...process.env, ...opts.env } : undefined,
38
+ });
39
+ return { ok: true };
40
+ } catch (e) { return { ok: false, error: e.message }; }
30
41
  }
31
42
 
32
- // PowerShell sem perfil; escapa aspas duplas para caber em -Command "...".
33
- function pwsh(script) {
34
- return run(`powershell -NoProfile -Command "${script.replace(/"/g, '\\"')}"`);
43
+ // Igual ao run(), mas devolve o stdout — para os comandos que PERGUNTAM algo
44
+ // (listar janelas, ler elementos) em vez de só agir.
45
+ function runOut(cmd, opts = {}) {
46
+ try {
47
+ const out = execSync(cmd, {
48
+ encoding: 'utf-8',
49
+ stdio: ['ignore', 'pipe', 'ignore'],
50
+ timeout: opts.timeout || 15000,
51
+ env: opts.env ? { ...process.env, ...opts.env } : undefined,
52
+ });
53
+ return { ok: true, out: String(out || '') };
54
+ } catch (e) { return { ok: false, error: e.message, out: String((e && e.stdout) || '') }; }
35
55
  }
36
56
 
57
+ // Argumento de shell entre aspas simples, à prova de apóstrofo. O truque
58
+ // POSIX é fechar a aspa, escapar o apóstrofo e reabrir: 'don'\''t'.
59
+ function sh1(s) {
60
+ return `'${String(s).replace(/'/g, `'\\''`)}'`;
61
+ }
62
+
63
+ // PowerShell via -EncodedCommand: o script vai em base64 (UTF-16LE), então
64
+ // aspas, cifrão e acento do texto do usuário NUNCA passam pelo cmd.exe cru.
65
+ // A versão antiga escapava aspas à mão dentro de -Command "..." e quebrava
66
+ // com qualquer texto que tivesse caractere especial — era um dos motivos de
67
+ // "clica mas não digita" no Windows.
68
+ function pwshCmd(script) {
69
+ const b64 = Buffer.from(script, 'utf16le').toString('base64');
70
+ return `powershell -NoProfile -NonInteractive -EncodedCommand ${b64}`;
71
+ }
72
+ function pwshRun(script, opts = {}) { return run(pwshCmd(script), opts); }
73
+ function pwshOut(script, opts = {}) { return runOut(pwshCmd(script), opts); }
74
+
37
75
  // Clique/scroll de verdade no Windows precisam de mouse_event (user32).
38
76
  // 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);';";
77
+ 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
78
 
41
79
  // Primeira ferramenta cujo comando existe no PATH.
42
80
  function firstCmd(list) {
@@ -44,86 +82,574 @@ function firstCmd(list) {
44
82
  return null;
45
83
  }
46
84
 
85
+ // Coordenada tem de ser número. Validar aqui vale para as três plataformas —
86
+ // sem isso um x="abc" virava comando de shell quebrado, não erro legível.
87
+ function coord(v) {
88
+ const n = Math.round(Number(v));
89
+ return Number.isFinite(n) ? n : null;
90
+ }
91
+
92
+ // ── Teclas: um dialeto por SO, uma tradução só ───────────────────────────
93
+ // O modelo fala "ctrl+shift+t", "Enter", "alt+tab" — e cada SO tem a própria
94
+ // gramática. Estas funções são puras (testáveis sem SO nenhum).
95
+
96
+ /** "ctrl+shift+t" → { mods: ['ctrl','shift'], key: 't' } */
97
+ function parseCombo(name) {
98
+ const parts = String(name || '').trim().split('+').map((s) => s.trim()).filter(Boolean);
99
+ const key = parts.pop() || '';
100
+ const mods = [];
101
+ for (const p of parts) {
102
+ const m = p.toLowerCase();
103
+ if (m === 'ctrl' || m === 'control') mods.push('ctrl');
104
+ else if (m === 'shift') mods.push('shift');
105
+ else if (m === 'alt' || m === 'option' || m === 'opt') mods.push('alt');
106
+ else if (m === 'cmd' || m === 'command' || m === 'win' || m === 'windows' || m === 'super' || m === 'meta') mods.push('super');
107
+ }
108
+ return { mods, key };
109
+ }
110
+
111
+ const F_RE = /^f([1-9]|1[0-2])$/i;
112
+
113
+ // xdotool: nomes de keysym.
114
+ const XKEYS = {
115
+ enter: 'Return', return: 'Return', tab: 'Tab', escape: 'Escape', esc: 'Escape',
116
+ backspace: 'BackSpace', delete: 'Delete', del: 'Delete', space: 'space',
117
+ arrowup: 'Up', up: 'Up', arrowdown: 'Down', down: 'Down',
118
+ arrowleft: 'Left', left: 'Left', arrowright: 'Right', right: 'Right',
119
+ home: 'Home', end: 'End', pageup: 'Prior', pagedown: 'Next',
120
+ insert: 'Insert', printscreen: 'Print',
121
+ win: 'Super_L', windows: 'Super_L', super: 'Super_L', cmd: 'Super_L', meta: 'Super_L',
122
+ };
123
+
124
+ /** "ctrl+shift+ArrowLeft" → "ctrl+shift+Left" (gramática do xdotool). */
125
+ function xdoKey(name) {
126
+ const { mods, key } = parseCombo(name);
127
+ const k = key.toLowerCase();
128
+ const traduzida = XKEYS[k] || (F_RE.test(k) ? k.toUpperCase() : key);
129
+ return [...mods, traduzida].join('+');
130
+ }
131
+
132
+ // Windows SendKeys: ^=ctrl +=shift %=alt, especiais entre chaves.
133
+ const WINKEYS = {
134
+ enter: '{ENTER}', return: '{ENTER}', tab: '{TAB}', escape: '{ESC}', esc: '{ESC}',
135
+ backspace: '{BACKSPACE}', delete: '{DEL}', del: '{DEL}', space: ' ',
136
+ arrowup: '{UP}', up: '{UP}', arrowdown: '{DOWN}', down: '{DOWN}',
137
+ arrowleft: '{LEFT}', left: '{LEFT}', arrowright: '{RIGHT}', right: '{RIGHT}',
138
+ home: '{HOME}', end: '{END}', pageup: '{PGUP}', pagedown: '{PGDN}',
139
+ insert: '{INS}', printscreen: '{PRTSC}',
140
+ };
141
+
142
+ /** Texto literal para SendKeys: +^%~(){}[] têm significado — vão entre chaves. */
143
+ function escapeSendKeys(text) {
144
+ return String(text).replace(/[+^%~(){}[\]]/g, '{$&}');
145
+ }
146
+
147
+ /**
148
+ * "ctrl+shift+t" → "^+t" (SendKeys). Devolve null quando o atalho usa a tecla
149
+ * Windows, que o SendKeys não alcança — o chamador explica a alternativa.
150
+ * "win" sozinho vira ^{ESC}, que abre o menu Iniciar desde sempre.
151
+ */
152
+ function winSendKeys(name) {
153
+ const { mods, key } = parseCombo(name);
154
+ const k = key.toLowerCase();
155
+ if (!mods.length && (k === 'win' || k === 'windows' || k === 'super' || k === 'meta' || k === 'cmd')) return '^{ESC}';
156
+ if (mods.includes('super')) return null;
157
+ const prefix = mods.map((m) => (m === 'ctrl' ? '^' : m === 'shift' ? '+' : '%')).join('');
158
+ let token = WINKEYS[k] || (F_RE.test(k) ? `{${k.toUpperCase()}}` : escapeSendKeys(key.length === 1 ? key.toLowerCase() : key));
159
+ // Modificador se aplica à PRÓXIMA tecla; um token de várias teclas precisa
160
+ // de parênteses para o modificador cobrir todas.
161
+ if (prefix && token.length > 1 && !/^\{[^}]+\}$/.test(token)) token = `(${token})`;
162
+ return prefix + token;
163
+ }
164
+
165
+ // macOS: tecla especial é key code NUMÉRICO; letra é keystroke.
166
+ const MACCODES = {
167
+ enter: 36, return: 36, tab: 48, escape: 53, esc: 53, backspace: 51, delete: 117, del: 117,
168
+ space: 49, arrowleft: 123, left: 123, arrowright: 124, right: 124,
169
+ arrowdown: 125, down: 125, arrowup: 126, up: 126,
170
+ home: 115, end: 119, pageup: 116, pagedown: 121,
171
+ f1: 122, f2: 120, f3: 99, f4: 118, f5: 96, f6: 97, f7: 98, f8: 100, f9: 101, f10: 109, f11: 103, f12: 111,
172
+ };
173
+
174
+ /** A linha de System Events para uma tecla/atalho: `key code 36 using {...}`. */
175
+ function macKeyScript(name) {
176
+ const { mods, key } = parseCombo(name);
177
+ const using = mods.map((m) => (m === 'ctrl' ? 'control down' : m === 'shift' ? 'shift down' : m === 'alt' ? 'option down' : 'command down')).join(', ');
178
+ const sufixo = using ? ` using {${using}}` : '';
179
+ const k = key.toLowerCase();
180
+ if (MACCODES[k] != null) return `key code ${MACCODES[k]}${sufixo}`;
181
+ const safe = String(key).replace(/\\/g, '\\\\').replace(/"/g, '\\"');
182
+ return `keystroke "${safe}"${sufixo}`;
183
+ }
184
+
185
+ function osascript(linhas, opts = {}) {
186
+ const script = Array.isArray(linhas) ? linhas.join('\n') : String(linhas);
187
+ return run(`osascript -e ${sh1(script)}`, opts);
188
+ }
189
+ function osascriptOut(linhas, opts = {}) {
190
+ const script = Array.isArray(linhas) ? linhas.join('\n') : String(linhas);
191
+ return runOut(`osascript -e ${sh1(script)}`, opts);
192
+ }
193
+
47
194
  // ── Mouse ─────────────────────────────────────────────
48
195
  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}`);
196
+ const [xi, yi] = [coord(x), coord(y)];
197
+ if (xi == null || yi == null) return { ok: false, error: `Coordenada inválida: x=${x}, y=${y}` };
198
+ if (TOOLS.xdotool) return run(`xdotool mousemove ${xi} ${yi} click ${button === 'right' ? 3 : 1}`);
199
+ if (TOOLS.cliclick) return run(`cliclick ${button === 'right' ? 'rc' : 'c'}:${xi},${yi}`);
51
200
  if (PLATFORM === 'win32') {
52
201
  const down = button === 'right' ? '0x0008' : '0x0002';
53
202
  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)`);
203
+ return pwshRun(`${WIN_USER32} [W.U]::SetCursorPos(${xi},${yi}); Start-Sleep -Milliseconds 40; [W.U]::mouse_event(${down},0,0,0,0); [W.U]::mouse_event(${up},0,0,0,0)`);
55
204
  }
56
205
  return { ok: false, error: 'Nenhuma ferramenta de automação encontrada. Instale xdotool (Linux) ou cliclick (macOS), ou use as tools browser_*.' };
57
206
  }
58
207
 
208
+ // Clique duplo é o que ABRE as coisas fora do navegador: ícone da área de
209
+ // trabalho, arquivo no Explorer, célula de planilha. Sem ele o agente "clicava
210
+ // mas nada abria".
211
+ function doubleClick(x, y) {
212
+ const [xi, yi] = [coord(x), coord(y)];
213
+ if (xi == null || yi == null) return { ok: false, error: `Coordenada inválida: x=${x}, y=${y}` };
214
+ if (TOOLS.xdotool) return run(`xdotool mousemove ${xi} ${yi} click --repeat 2 --delay 120 1`);
215
+ if (TOOLS.cliclick) return run(`cliclick dc:${xi},${yi}`);
216
+ if (PLATFORM === 'win32') {
217
+ const clique = '[W.U]::mouse_event(0x0002,0,0,0,0); [W.U]::mouse_event(0x0004,0,0,0,0)';
218
+ return pwshRun(`${WIN_USER32} [W.U]::SetCursorPos(${xi},${yi}); Start-Sleep -Milliseconds 40; ${clique}; Start-Sleep -Milliseconds 90; ${clique}`);
219
+ }
220
+ return { ok: false, error: 'Clique duplo não disponível: instale xdotool (Linux) ou cliclick (macOS).' };
221
+ }
222
+
59
223
  function move(x, y) {
60
- if (TOOLS.xdotool) return run(`xdotool mousemove ${x} ${y}`);
61
- if (TOOLS.cliclick) return run(`cliclick m:${x},${y}`);
224
+ const [xi, yi] = [coord(x), coord(y)];
225
+ if (xi == null || yi == null) return { ok: false, error: `Coordenada inválida: x=${x}, y=${y}` };
226
+ if (TOOLS.xdotool) return run(`xdotool mousemove ${xi} ${yi}`);
227
+ if (TOOLS.cliclick) return run(`cliclick m:${xi},${yi}`);
62
228
  if (PLATFORM === 'win32') {
63
- return pwsh(`${WIN_USER32} [W.U]::SetCursorPos(${x},${y})`);
229
+ return pwshRun(`${WIN_USER32} [W.U]::SetCursorPos(${xi},${yi})`);
64
230
  }
65
231
  return { ok: false, error: 'Automação não disponível' };
66
232
  }
67
233
 
68
234
  function drag(fromX, fromY, toX, toY) {
235
+ const [fx, fy, tx, ty] = [coord(fromX), coord(fromY), coord(toX), coord(toY)];
236
+ if ([fx, fy, tx, ty].some((v) => v == null)) return { ok: false, error: 'Coordenada inválida no drag' };
69
237
  if (TOOLS.xdotool) {
70
- return run(`xdotool mousemove ${fromX} ${fromY} mousedown 1 mousemove ${toX} ${toY} mouseup 1`);
238
+ return run(`xdotool mousemove ${fx} ${fy} mousedown 1 mousemove ${tx} ${ty} mouseup 1`);
71
239
  }
72
240
  if (TOOLS.cliclick) {
73
- return run(`cliclick dd:${fromX},${fromY} du:${toX},${toY}`);
241
+ return run(`cliclick dd:${fx},${fy} du:${tx},${ty}`);
242
+ }
243
+ if (PLATFORM === 'win32') {
244
+ // Arrastar de verdade exige passos intermediários: soltar o botão no
245
+ // destino sem passar pelo caminho não conta como drag para o Windows.
246
+ const passos = [];
247
+ const N = 8;
248
+ for (let i = 1; i <= N; i++) {
249
+ const px = Math.round(fx + ((tx - fx) * i) / N);
250
+ const py = Math.round(fy + ((ty - fy) * i) / N);
251
+ passos.push(`[W.U]::SetCursorPos(${px},${py}); Start-Sleep -Milliseconds 25`);
252
+ }
253
+ return pwshRun(`${WIN_USER32} [W.U]::SetCursorPos(${fx},${fy}); Start-Sleep -Milliseconds 60; [W.U]::mouse_event(0x0002,0,0,0,0); Start-Sleep -Milliseconds 120; ${passos.join('; ')}; [W.U]::mouse_event(0x0004,0,0,0,0)`);
74
254
  }
75
255
  return { ok: false, error: 'Drag não suportado nesta plataforma sem xdotool/cliclick' };
76
256
  }
77
257
 
78
258
  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)}`);
259
+ const d = Number(dy);
260
+ const passo = Number.isFinite(d) ? d : 300;
261
+ if (TOOLS.xdotool) return run(`xdotool click --repeat ${Math.max(1, Math.abs(Math.round(passo / 50)))} ${passo > 0 ? 5 : 4}`);
262
+ if (TOOLS.cliclick) return run(`cliclick scroll:${Math.round(passo / 50)}`);
81
263
  if (PLATFORM === 'win32') {
82
264
  // 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)`);
265
+ const delta = -Math.round(passo / 50) * 120;
266
+ return pwshRun(`${WIN_USER32} [W.U]::mouse_event(0x0800,0,0,${delta},0)`);
85
267
  }
86
268
  return { ok: false, error: 'Scroll não suportado nesta plataforma' };
87
269
  }
88
270
 
89
271
  // ── Teclado ───────────────────────────────────────────
90
272
  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}"'`);
273
+ const t = String(text ?? '');
274
+ if (!t) return { ok: true, digitado: 0 };
275
+ if (TOOLS.xdotool) return run(`xdotool type --delay 12 -- ${sh1(t)}`);
276
+ if (PLATFORM === 'darwin') {
277
+ // Uma keystroke por linha; entre elas, Return de verdade (key code 36) —
278
+ // keystroke com "\n" no meio não quebra linha em app nenhum.
279
+ const linhas = t.split(/\r?\n/);
280
+ const corpo = [];
281
+ linhas.forEach((l, i) => {
282
+ if (i > 0) corpo.push('key code 36');
283
+ if (l) corpo.push(`keystroke "${l.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`);
284
+ });
285
+ return osascript(['tell application "System Events"', ...corpo, 'end tell']);
95
286
  }
96
287
  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}"`);
288
+ // SendKeys trata +^%~(){}[] como comando — escapar TUDO, senão digitar
289
+ // "olá (teste)" dispara atalho em vez de texto. Depois do escape, o Enter
290
+ // literal vira {ENTER}.
291
+ const seq = escapeSendKeys(t).replace(/\r?\n/g, '{ENTER}');
292
+ return pwshRun(`Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.SendKeys]::SendWait($env:PRIMO_KEYS)`, { env: { PRIMO_KEYS: seq } });
99
293
  }
100
294
  return { ok: false, error: 'Automação de teclado não disponível' };
101
295
  }
102
296
 
103
297
  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}`);
298
+ if (!name) return { ok: false, error: 'Diga a tecla: "Enter", "ctrl+c", "alt+tab"…' };
299
+ if (TOOLS.xdotool) return run(`xdotool key --clearmodifiers ${xdoKey(name)}`);
111
300
 
112
301
  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, '\\"')}"'`);
302
+ return osascript(`tell application "System Events" to ${macKeyScript(name)}`);
118
303
  }
119
304
  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, "''")}')`);
305
+ const seq = winSendKeys(name);
306
+ if (seq === null) {
307
+ return { ok: false, error: 'Atalho com a tecla Windows não é alcançável por SendKeys. Alternativas: desktop_key("win") abre o menu Iniciar; desktop_open_app abre programas direto.' };
308
+ }
309
+ return pwshRun(`Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.SendKeys]::SendWait($env:PRIMO_KEYS)`, { env: { PRIMO_KEYS: seq } });
123
310
  }
124
311
  return { ok: false, error: 'Key não suportado nesta plataforma' };
125
312
  }
126
313
 
314
+ // ── Programas e janelas ───────────────────────────────
315
+ // Abrir app o agente já conseguia pelo shell; o que faltava era o resto da
316
+ // conversa: saber QUAIS janelas existem, trazer uma para a frente e enxergar
317
+ // o que há dentro dela. É isso que transforma "abre mas não interage" em
318
+ // interação de verdade.
319
+
320
+ // Nomes em português do dia a dia → executável/app real. Pequena de propósito:
321
+ // só o que tem tradução óbvia. O resto passa direto como o usuário falou.
322
+ const APP_ALIASES = {
323
+ win32: {
324
+ 'bloco de notas': 'notepad', 'notepad': 'notepad', 'calculadora': 'calc',
325
+ 'paint': 'mspaint', 'explorador de arquivos': 'explorer', 'arquivos': 'explorer',
326
+ 'explorer': 'explorer', 'prompt de comando': 'cmd', 'terminal': 'wt',
327
+ 'gerenciador de tarefas': 'taskmgr', 'configuracoes': 'ms-settings:',
328
+ 'painel de controle': 'control', 'edge': 'msedge', 'word': 'winword',
329
+ 'excel': 'excel', 'powerpoint': 'powerpnt',
330
+ },
331
+ darwin: {
332
+ 'calculadora': 'Calculator', 'bloco de notas': 'TextEdit', 'notas': 'Notes',
333
+ 'arquivos': 'Finder', 'finder': 'Finder', 'terminal': 'Terminal',
334
+ 'chrome': 'Google Chrome', 'safari': 'Safari', 'musica': 'Music',
335
+ 'fotos': 'Photos', 'configuracoes': 'System Settings', 'ajustes': 'System Settings',
336
+ },
337
+ linux: {
338
+ 'calculadora': 'gnome-calculator', 'arquivos': 'nautilus',
339
+ 'bloco de notas': 'gedit', 'editor de texto': 'gedit',
340
+ 'terminal': 'gnome-terminal', 'chrome': 'google-chrome',
341
+ },
342
+ };
343
+
344
+ function normaliza(s) {
345
+ return String(s || '').normalize('NFD').replace(/[̀-ͯ]/g, '').toLowerCase().trim();
346
+ }
347
+
348
+ function aliasDoApp(nome, platform = PLATFORM) {
349
+ const mapa = APP_ALIASES[platform] || {};
350
+ return mapa[normaliza(nome)] || String(nome || '').trim();
351
+ }
352
+
353
+ function openApp(nome) {
354
+ const pedido = String(nome || '').trim();
355
+ if (!pedido) return { ok: false, error: 'Diga o nome do programa (ex: "chrome", "bloco de notas").' };
356
+ const app = aliasDoApp(pedido);
357
+
358
+ let r;
359
+ if (PLATFORM === 'win32') {
360
+ // Start-Process resolve PATH e App Paths (chrome, winword…); o start do
361
+ // cmd cobre o resto (URIs tipo ms-settings:, associações).
362
+ r = pwshOut(`$ErrorActionPreference='Stop'; $a=$env:PRIMO_APP; try { Start-Process $a; Write-Output 'OK' } catch { try { Start-Process -FilePath 'cmd.exe' -ArgumentList @('/c','start','',$a) -WindowStyle Hidden; Write-Output 'OK' } catch { Write-Output ('ERRO ' + $_.Exception.Message) } }`, { env: { PRIMO_APP: app } });
363
+ if (r.ok && !/^ERRO/m.test(r.out)) r = { ok: true };
364
+ else r = { ok: false, error: (r.out || r.error || '').trim() || `não consegui abrir "${app}"` };
365
+ } else if (PLATFORM === 'darwin') {
366
+ r = run(`open -a ${sh1(app)}`);
367
+ if (!r.ok && app !== pedido) r = run(`open -a ${sh1(pedido)}`);
368
+ if (!r.ok) r = { ok: false, error: `Não achei o app "${pedido}". Use o nome exato como aparece em /Applications.` };
369
+ } else {
370
+ const bin = app.split(/\s+/)[0];
371
+ if (hasCmd('gtk-launch') && run(`gtk-launch ${sh1(bin)}`).ok) r = { ok: true };
372
+ else if (hasCmd(bin)) r = run(`bash -c ${sh1(`setsid ${app} >/dev/null 2>&1 < /dev/null &`)}`);
373
+ else r = { ok: false, error: `Não achei o programa "${pedido}" no PATH.` };
374
+ }
375
+
376
+ if (!r.ok) return r;
377
+ return { ok: true, app, dica: 'A janela pode levar 1–2s para abrir. Confirme com desktop_windows e traga para a frente com desktop_focus.' };
378
+ }
379
+
380
+ function listWindows() {
381
+ let linhas = [];
382
+ if (PLATFORM === 'win32') {
383
+ const r = pwshOut(`Get-Process | Where-Object { $_.MainWindowTitle } | ForEach-Object { $t = $_.MainWindowTitle -replace "[\\r\\n|]", " "; Write-Output ("$($_.ProcessName)|$t") }`, { timeout: 20000 });
384
+ if (!r.ok && !r.out) return { ok: false, error: r.error || 'não consegui listar as janelas' };
385
+ linhas = r.out.split('\n');
386
+ } else if (PLATFORM === 'darwin') {
387
+ const r = osascriptOut([
388
+ 'set saida to ""',
389
+ 'tell application "System Events"',
390
+ ' repeat with p in (application processes whose visible is true)',
391
+ ' repeat with w in windows of p',
392
+ ' try',
393
+ ' set saida to saida & (name of p) & "|" & (name of w) & linefeed',
394
+ ' end try',
395
+ ' end repeat',
396
+ ' end repeat',
397
+ 'end tell',
398
+ 'return saida',
399
+ ], { timeout: 20000 });
400
+ if (!r.ok) return { ok: false, error: 'Não consegui listar as janelas. Conceda permissão de Acessibilidade ao Terminal em Ajustes → Privacidade e Segurança.' };
401
+ linhas = r.out.split('\n');
402
+ } else {
403
+ if (hasCmd('wmctrl')) {
404
+ const r = runOut('wmctrl -l');
405
+ if (r.ok) linhas = r.out.split('\n').map((l) => {
406
+ const m = l.match(/^\S+\s+-?\d+\s+\S+\s+(.*)$/);
407
+ return m ? `|${m[1]}` : '';
408
+ });
409
+ } else if (TOOLS.xdotool) {
410
+ const r = runOut(`xdotool search --onlyvisible --name '.*' getwindowname %@ 2>/dev/null`);
411
+ if (r.ok) linhas = r.out.split('\n').map((t) => (t.trim() ? `|${t.trim()}` : ''));
412
+ } else {
413
+ return { ok: false, error: 'Instale wmctrl ou xdotool para listar janelas no Linux.' };
414
+ }
415
+ }
416
+
417
+ const janelas = [];
418
+ for (const l of linhas) {
419
+ const linha = l.replace(/\r/g, '').trim();
420
+ if (!linha) continue;
421
+ const sep = linha.indexOf('|');
422
+ const app = sep >= 0 ? linha.slice(0, sep).trim() : '';
423
+ const titulo = (sep >= 0 ? linha.slice(sep + 1) : linha).trim();
424
+ if (titulo) janelas.push({ app, titulo });
425
+ }
426
+ return { ok: true, total: janelas.length, janelas: janelas.slice(0, 60) };
427
+ }
428
+
429
+ function focusWindow(titulo) {
430
+ const t = String(titulo || '').trim();
431
+ if (!t) return { ok: false, error: 'Diga o título (ou parte dele) da janela.' };
432
+
433
+ if (PLATFORM === 'win32') {
434
+ const r = pwshOut(
435
+ `$ErrorActionPreference='SilentlyContinue'
436
+ Add-Type -Name W -Namespace P -MemberDefinition '[DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr h); [DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr h, int n); [DllImport("user32.dll")] public static extern bool IsIconic(IntPtr h);'
437
+ $t = $env:PRIMO_TITLE
438
+ $p = Get-Process | Where-Object { $_.MainWindowTitle -and ($_.MainWindowTitle -match [regex]::Escape($t) -or $_.ProcessName -match [regex]::Escape($t)) } | Select-Object -First 1
439
+ if (-not $p) { Write-Output 'NOTFOUND'; exit 0 }
440
+ if ([P.W]::IsIconic($p.MainWindowHandle)) { [P.W]::ShowWindow($p.MainWindowHandle, 9) | Out-Null }
441
+ [P.W]::SetForegroundWindow($p.MainWindowHandle) | Out-Null
442
+ Write-Output $p.MainWindowTitle`,
443
+ { env: { PRIMO_TITLE: t }, timeout: 20000 },
444
+ );
445
+ const saida = (r.out || '').trim();
446
+ if (!r.ok && !saida) return { ok: false, error: r.error || 'falhou ao focar' };
447
+ if (/^NOTFOUND/m.test(saida)) return naoAchouJanela(t);
448
+ return { ok: true, janela: saida.split('\n')[0] };
449
+ }
450
+
451
+ if (PLATFORM === 'darwin') {
452
+ const safe = t.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
453
+ const r = osascript([
454
+ 'tell application "System Events"',
455
+ ` set alvo to first application process whose visible is true and (name contains "${safe}" or displayed name contains "${safe}")`,
456
+ ' set frontmost of alvo to true',
457
+ 'end tell',
458
+ ]);
459
+ if (!r.ok) return naoAchouJanela(t);
460
+ return { ok: true, janela: t };
461
+ }
462
+
463
+ if (TOOLS.xdotool) {
464
+ const busca = runOut(`xdotool search --onlyvisible --name ${sh1(t)} 2>/dev/null`);
465
+ const id = (busca.out || '').trim().split('\n')[0];
466
+ if (!id) return naoAchouJanela(t);
467
+ const r = run(`xdotool windowactivate --sync ${id}`);
468
+ return r.ok ? { ok: true, janela: t } : r;
469
+ }
470
+ return { ok: false, error: 'Focar janela não disponível: instale xdotool (Linux).' };
471
+ }
472
+
473
+ // Erro que sabe o que EXISTE leva a informação junto — mesma ideia dos
474
+ // "trechosParecidos" do edit_file. Erro seco faz o modelo chutar de novo.
475
+ function naoAchouJanela(titulo) {
476
+ const w = listWindows();
477
+ return {
478
+ ok: false,
479
+ error: `Nenhuma janela combinou com "${titulo}".`,
480
+ janelasAbertas: w.ok ? w.janelas.map((j) => j.titulo).slice(0, 30) : undefined,
481
+ comoCorrigir: 'Use um título de "janelasAbertas", copiado literalmente (ou um pedaço único dele). Se o programa nem abriu, chame desktop_open_app antes.',
482
+ };
483
+ }
484
+
485
+ // ── Elementos da janela — os "olhos" do agente ────────────────────────────
486
+ // Os modelos do servidor são só-texto: mandar um PNG não adianta nada. O que
487
+ // funciona é ler a árvore de acessibilidade — a mesma que o leitor de tela usa
488
+ // — e devolver NOME + POSIÇÃO de cada botão, campo e ícone. Aí "clique em
489
+ // Salvar" vira coordenada exata, sem chute.
490
+
491
+ const MAX_ELEMENTOS = 120;
492
+
493
+ const PS_ELEMENTOS = `$ErrorActionPreference='SilentlyContinue'
494
+ Add-Type -AssemblyName UIAutomationClient
495
+ Add-Type -AssemblyName UIAutomationTypes
496
+ Add-Type -Name W -Namespace P -MemberDefinition '[DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow();'
497
+ $titulo = $env:PRIMO_TITLE
498
+ $root = [System.Windows.Automation.AutomationElement]::RootElement
499
+ $win = $null
500
+ if ($titulo) {
501
+ foreach ($w in $root.FindAll([System.Windows.Automation.TreeScope]::Children, [System.Windows.Automation.Condition]::TrueCondition)) {
502
+ if ($w.Current.Name -like ('*' + $titulo + '*')) { $win = $w; break }
503
+ }
504
+ }
505
+ if (-not $win) {
506
+ $h = [P.W]::GetForegroundWindow()
507
+ if ($h -ne [IntPtr]::Zero) { $win = [System.Windows.Automation.AutomationElement]::FromHandle($h) }
508
+ }
509
+ if (-not $win) { Write-Output 'SEMJANELA'; exit 0 }
510
+ Write-Output ('JANELA|' + ($win.Current.Name -replace "[\\r\\n|]", ' '))
511
+ $todos = $win.FindAll([System.Windows.Automation.TreeScope]::Descendants, [System.Windows.Automation.Condition]::TrueCondition)
512
+ $n = 0
513
+ foreach ($e in $todos) {
514
+ if ($n -ge ${MAX_ELEMENTOS}) { break }
515
+ $c = $e.Current
516
+ $nome = $c.Name
517
+ if (-not $nome) { continue }
518
+ if ($c.IsOffscreen) { continue }
519
+ $r = $c.BoundingRectangle
520
+ if ($r.Width -le 0 -or $r.Height -le 0) { continue }
521
+ $tipo = $c.ControlType.ProgrammaticName -replace 'ControlType\\.', ''
522
+ $cx = [int]($r.X + $r.Width / 2); $cy = [int]($r.Y + $r.Height / 2)
523
+ $nome = $nome -replace "[\\r\\n|]", ' '
524
+ Write-Output ("$tipo|$cx|$cy|$nome")
525
+ $n++
526
+ }`;
527
+
528
+ function uiElements(janela) {
529
+ const titulo = String(janela || '').trim();
530
+
531
+ if (PLATFORM === 'win32') {
532
+ const r = pwshOut(PS_ELEMENTOS, { env: { PRIMO_TITLE: titulo }, timeout: 45000 });
533
+ if (!r.ok && !r.out) return { ok: false, error: r.error || 'não consegui ler os elementos' };
534
+ if (/^SEMJANELA/m.test(r.out)) return { ok: false, error: 'Nenhuma janela em foco. Abra o programa (desktop_open_app) ou foque a janela (desktop_focus) primeiro.' };
535
+ return parseElementos(r.out);
536
+ }
537
+
538
+ if (PLATFORM === 'darwin') {
539
+ // `entire contents` lê a janela toda de uma vez; elemento sem nome ou
540
+ // sem posição só é pulado (o try engole).
541
+ const r = osascriptOut([
542
+ 'set saida to ""',
543
+ 'set n to 0',
544
+ 'tell application "System Events"',
545
+ ' set p to first application process whose frontmost is true',
546
+ ' set saida to "JANELA|" & (name of p) & linefeed',
547
+ ' try',
548
+ ' set els to entire contents of front window of p',
549
+ ' repeat with e in els',
550
+ ` if n ≥ ${MAX_ELEMENTOS} then exit repeat`,
551
+ ' try',
552
+ ' set nm to name of e',
553
+ ' if nm is not missing value and nm is not "" then',
554
+ ' set {ex, ey} to position of e',
555
+ ' set {ew, eh} to size of e',
556
+ ' set saida to saida & (role of e) & "|" & (ex + ew div 2) & "|" & (ey + eh div 2) & "|" & nm & linefeed',
557
+ ' set n to n + 1',
558
+ ' end if',
559
+ ' end try',
560
+ ' end repeat',
561
+ ' end try',
562
+ 'end tell',
563
+ 'return saida',
564
+ ], { timeout: 45000 });
565
+ if (!r.ok) return { ok: false, error: 'Não consegui ler a janela. Conceda permissão de Acessibilidade ao Terminal em Ajustes → Privacidade e Segurança → Acessibilidade.' };
566
+ return parseElementos(r.out);
567
+ }
568
+
569
+ return { ok: false, error: 'Leitura de elementos não disponível no Linux. Caminho alternativo: desktop_screenshot para o usuário ver, e desktop_click com coordenadas (xdotool).' };
570
+ }
571
+
572
+ function parseElementos(saida) {
573
+ const elementos = [];
574
+ let janela = '';
575
+ const vistos = new Set();
576
+ for (const linha of String(saida || '').split('\n')) {
577
+ const l = linha.replace(/\r/g, '').trim();
578
+ if (!l) continue;
579
+ if (l.startsWith('JANELA|')) { janela = l.slice(7).trim(); continue; }
580
+ const partes = l.split('|');
581
+ if (partes.length < 4) continue;
582
+ const [tipo, xs, ys] = partes;
583
+ const nome = partes.slice(3).join('|').trim();
584
+ const x = Math.round(Number(xs)), y = Math.round(Number(ys));
585
+ if (!nome || !Number.isFinite(x) || !Number.isFinite(y)) continue;
586
+ const chave = `${tipo}|${x}|${y}|${nome}`;
587
+ if (vistos.has(chave)) continue;
588
+ vistos.add(chave);
589
+ elementos.push({ tipo: tipo.trim(), x, y, nome: nome.slice(0, 120) });
590
+ if (elementos.length >= MAX_ELEMENTOS) break;
591
+ }
592
+ if (!elementos.length) {
593
+ return { ok: false, error: 'A janela não expôs elementos nomeados (alguns apps escondem a árvore de acessibilidade). Use desktop_screenshot + desktop_click por coordenada.' , janela };
594
+ }
595
+ return { ok: true, janela, total: elementos.length, elementos };
596
+ }
597
+
598
+ /**
599
+ * Melhor elemento para um nome pedido — sem acento, sem caixa, aceitando
600
+ * pedaço ("salvar" acha "Salvar como…"). Pura, para o teste bater sem SO.
601
+ */
602
+ function escolherElemento(elementos, consulta) {
603
+ const q = normaliza(consulta);
604
+ if (!q) return null;
605
+ let melhor = null;
606
+ let nota = 0;
607
+ for (const e of elementos || []) {
608
+ const n = normaliza(e.nome);
609
+ if (!n) continue;
610
+ let s = 0;
611
+ // O nome que ESTENDE a consulta ("salvar como" → "Salvar como…") vale
612
+ // mais que a consulta estender o nome: quem pediu a frase maior quis o
613
+ // elemento maior, não o irmão de nome curto.
614
+ if (n === q) s = 100;
615
+ else if (n.startsWith(q)) s = 85;
616
+ else if (q.startsWith(n)) s = 75;
617
+ else if (n.includes(q)) s = 65;
618
+ else if (q.includes(n)) s = 55;
619
+ else {
620
+ const termos = q.split(/\s+/).filter((t) => t.length > 1);
621
+ const acertos = termos.filter((t) => n.includes(t)).length;
622
+ if (acertos) s = Math.round((40 * acertos) / termos.length);
623
+ }
624
+ if (!s) continue;
625
+ // Empate: nome mais curto costuma ser o botão em si, não a frase que o contém.
626
+ if (s > nota || (s === nota && melhor && e.nome.length < melhor.nome.length)) { nota = s; melhor = e; }
627
+ }
628
+ return nota >= 40 ? melhor : null;
629
+ }
630
+
631
+ function clickElement(nome, opts = {}) {
632
+ const pedido = String(nome || '').trim();
633
+ if (!pedido) return { ok: false, error: 'Diga o nome do elemento (o texto do botão, ícone ou campo).' };
634
+
635
+ const lidos = uiElements(opts.window);
636
+ if (!lidos.ok) return lidos;
637
+
638
+ const alvo = escolherElemento(lidos.elementos, pedido);
639
+ if (!alvo) {
640
+ return {
641
+ ok: false,
642
+ error: `Nenhum elemento parecido com "${pedido}" na janela "${lidos.janela}".`,
643
+ elementosDisponiveis: lidos.elementos.slice(0, 40).map((e) => e.nome),
644
+ comoCorrigir: 'Chame de novo com um nome de "elementosDisponiveis", copiado literalmente.',
645
+ };
646
+ }
647
+
648
+ const r = opts.double ? doubleClick(alvo.x, alvo.y) : click(alvo.x, alvo.y, opts.button || 'left');
649
+ if (!r.ok) return r;
650
+ return { ok: true, clicado: alvo.nome, tipo: alvo.tipo, x: alvo.x, y: alvo.y, duplo: !!opts.double };
651
+ }
652
+
127
653
  // ── Screenshot ────────────────────────────────────────
128
654
  function screenshot(saveTo) {
129
655
  const filename = saveTo || `desktop-${Date.now()}.png`;
@@ -133,8 +659,8 @@ function screenshot(saveTo) {
133
659
  return done(run(`screencapture -x "${filename}"`));
134
660
  }
135
661
  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));
662
+ 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.replace(/'/g, "''")}'); $g.Dispose(); $bmp.Dispose()`;
663
+ return done(pwshRun(ps));
138
664
  }
139
665
  // Linux: tenta as ferramentas comuns, na ordem.
140
666
  const scrot = firstCmd(['scrot', 'gnome-screenshot', 'spectacle', 'import', 'maim']);
@@ -182,8 +708,13 @@ function getScreenSize() {
182
708
  }
183
709
 
184
710
  module.exports = {
185
- click, move, drag, scroll,
711
+ click, doubleClick, move, drag, scroll,
186
712
  type, key,
713
+ openApp, listWindows, focusWindow, uiElements, clickElement,
187
714
  screenshot, getScreenSize,
188
715
  TOOLS,
716
+ // Puras, exportadas para o test/desktop.test.js provar a tradução de teclas
717
+ // e a escolha de elemento sem depender do SO da máquina de teste.
718
+ parseCombo, xdoKey, winSendKeys, macKeyScript, escapeSendKeys,
719
+ escolherElemento, aliasDoApp,
189
720
  };
package/lib/modo.js CHANGED
@@ -30,12 +30,16 @@ const VERBOS = [
30
30
  'implement[ae]r?', 'l[eê]i?a?r?', 'procur[ae]r?', 'busc[ae]r?', 'encontr[ae]r?',
31
31
  'renome[ai]r?', 'copi[ae]r?', 'commit[ae]r?', 'deploy',
32
32
  'sobe', 'suba', 'subir', 'empurr[ae]r?', 'mergea?r?',
33
+ // Interação com o computador: "clica no botão", "digita a senha",
34
+ // "pesquisa no google", "aperta enter", "arrasta o arquivo".
35
+ 'clic[ao]r?', 'clique', 'digit[ae]r?', 'pesquis[ae]r?', 'apert[ae]r?',
36
+ 'arrast[ae]r?', 'rol[ae]r?', 'minimiz[ae]r?', 'maximiz[ae]r?', 'fech[ae]r?',
33
37
  ];
34
38
  const RE_VERBO = new RegExp(`(^|\\s)(${VERBOS.join('|')})\\b`, 'i');
35
39
 
36
40
  // Coisas que só existem quando há trabalho de arquivo/máquina envolvido.
37
41
  const RE_ARTEFATO = /\.(js|ts|tsx|jsx|html|css|json|md|py|sh|yml|yaml|txt|csv|env)\b|[\w-]+\/[\w-]/i;
38
- const RE_FERRAMENTA = /\b(navegador|browser|desktop|terminal|shell|git|npm|node|deploy|servidor|localhost|porta \d+|screenshot|print da tela)\b/i;
42
+ const RE_FERRAMENTA = /\b(navegador|browser|desktop|terminal|shell|git|npm|node|deploy|servidor|localhost|porta \d+|screenshot|print da tela|mouse|teclado|janela|[áa]rea de trabalho|aplicativo|[íi]cone)\b/i;
39
43
  const RE_PASTA = /\b(pasta|diret[óo]rio|arquivo|arquivos|reposit[óo]rio|repo|projeto)\b/i;
40
44
 
41
45
  // Conversa: cumprimento, agradecimento, pergunta conceitual.
@@ -0,0 +1,241 @@
1
+ /**
2
+ * streaming.js — o painel "ao vivo": ver o agente mexendo no computador.
3
+ *
4
+ * O agente age no desktop do usuário (clica, digita, abre janela), mas rápido
5
+ * demais para acompanhar a olho nu — e às vezes numa janela que não está na
6
+ * frente. O /streaming liga um servidorzinho HTTP local que mostra, no
7
+ * navegador, cada ação em ordem + uma foto da tela tirada logo depois dela.
8
+ * Você dá a instrução no terminal e VÊ o agente fazendo, passo a passo.
9
+ *
10
+ * Zero dependências: http nativo, uma página HTML embutida que consulta
11
+ * /state a cada 700ms. Só escuta em 127.0.0.1 — nada disso sai da máquina.
12
+ */
13
+
14
+ 'use strict';
15
+
16
+ const http = require('http');
17
+ const fs = require('fs');
18
+ const path = require('path');
19
+ const os = require('os');
20
+ const desktop = require('./desktop');
21
+
22
+ const DIR = path.join(os.homedir(), '.primocode', 'streaming');
23
+ const FRAME = path.join(DIR, 'frame.png');
24
+ const MAX_LOG = 200;
25
+ // Uma foto por ação no máximo a cada 600ms: digitar letra por letra não pode
26
+ // virar uma rajada de screenshots.
27
+ const INTERVALO_FOTO_MS = 600;
28
+
29
+ const state = {
30
+ on: false,
31
+ server: null,
32
+ url: '',
33
+ frame: 0, // contador: a página troca a imagem quando ele muda
34
+ log: [],
35
+ erroFoto: null,
36
+ ultimaFoto: 0,
37
+ passo: 0,
38
+ };
39
+
40
+ // Ações cujo efeito aparece NA TELA — são as que merecem uma foto nova.
41
+ const ACOES_NA_TELA = new Set([
42
+ 'desktop_click', 'desktop_double_click', 'desktop_click_element',
43
+ 'desktop_move', 'desktop_drag', 'desktop_type', 'desktop_key',
44
+ 'desktop_scroll', 'desktop_open_app', 'desktop_focus', 'desktop_screenshot',
45
+ 'browser_open', 'browser_click', 'browser_type', 'browser_key',
46
+ 'browser_scroll',
47
+ ]);
48
+
49
+ function isOn() { return state.on; }
50
+ function url() { return state.url; }
51
+
52
+ const corta = (s, n) => { const t = String(s ?? ''); return t.length > n ? t.slice(0, n - 1) + '…' : t; };
53
+
54
+ // A frase humana de cada ação — é o que aparece na linha do tempo.
55
+ function descrever(name, args = {}) {
56
+ switch (name) {
57
+ case 'desktop_click': return `clique em (${args.x}, ${args.y})`;
58
+ case 'desktop_double_click': return `clique duplo em (${args.x}, ${args.y})`;
59
+ case 'desktop_click_element': return `clique em “${corta(args.name, 40)}”${args.double ? ' (duplo)' : ''}`;
60
+ case 'desktop_move': return `mouse para (${args.x}, ${args.y})`;
61
+ case 'desktop_drag': return `arrastou (${args.from_x}, ${args.from_y}) → (${args.to_x}, ${args.to_y})`;
62
+ case 'desktop_type': return `digitou “${corta(args.text, 40)}”`;
63
+ case 'desktop_key': return `tecla ${args.key}`;
64
+ case 'desktop_scroll': return `rolou a tela (${args.dy ?? 300})`;
65
+ case 'desktop_open_app': return `abriu ${corta(args.app || args.name, 40)}`;
66
+ case 'desktop_focus': return `focou a janela “${corta(args.window || args.title, 40)}”`;
67
+ case 'desktop_windows': return 'listou as janelas abertas';
68
+ case 'desktop_elements': return `leu os elementos${args.window ? ` de “${corta(args.window, 30)}”` : ' da janela em foco'}`;
69
+ case 'desktop_screenshot': return 'tirou um print da tela';
70
+ case 'desktop_size': return 'mediu a tela';
71
+ case 'browser_open': return `abriu ${corta(args.url, 60)} no navegador`;
72
+ case 'browser_click': return `clicou em “${corta(args.text || args.selector || `${args.x},${args.y}`, 40)}” na página`;
73
+ case 'browser_type': return `digitou “${corta(args.text, 40)}” na página`;
74
+ case 'browser_key': return `tecla ${args.key} na página`;
75
+ case 'browser_scroll': return 'rolou a página';
76
+ case 'browser_read': return 'leu a página';
77
+ case 'browser_screenshot': return 'print da página';
78
+ case 'browser_wait': return `esperou ${corta(args.selector, 40)}`;
79
+ case 'browser_eval': return 'rodou JavaScript na página';
80
+ case 'write_file': return `criou/reescreveu ${corta(args.path, 50)}`;
81
+ case 'edit_file': return `editou ${corta(args.path, 50)}`;
82
+ case 'multi_edit': return `editou ${corta(args.path, 50)} (várias trocas)`;
83
+ case 'read_file': return `leu ${corta(args.path, 50)}`;
84
+ case 'run_shell': return `rodou: ${corta(args.command, 60)}`;
85
+ case 'run_in_new_terminal': return `terminal novo: ${corta(args.command, 50)}`;
86
+ case 'write_todos': return 'atualizou o plano';
87
+ default: return name.replace(/_/g, ' ');
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Chamada pelo loop do agente depois de CADA tool. Quando o streaming está
93
+ * desligado é um no-op — o caminho normal não paga nada por isto.
94
+ */
95
+ function step(name, args, result) {
96
+ if (!state.on) return;
97
+
98
+ state.passo++;
99
+ state.log.push({
100
+ n: state.passo,
101
+ hora: new Date().toTimeString().slice(0, 8),
102
+ acao: descrever(name, args || {}),
103
+ ok: !(result && result.ok === false),
104
+ erro: result && result.ok === false ? corta(result.error, 120) : undefined,
105
+ });
106
+ if (state.log.length > MAX_LOG) state.log.splice(0, state.log.length - MAX_LOG);
107
+
108
+ if (ACOES_NA_TELA.has(name)) capturar();
109
+ }
110
+
111
+ function capturar() {
112
+ const agora = Date.now();
113
+ if (agora - state.ultimaFoto < INTERVALO_FOTO_MS) return;
114
+ state.ultimaFoto = agora;
115
+ const r = desktop.screenshot(FRAME);
116
+ if (r.ok) { state.frame++; state.erroFoto = null; }
117
+ else if (!state.erroFoto) state.erroFoto = r.error;
118
+ }
119
+
120
+ // ── Página ────────────────────────────────────────────────────────────────
121
+ const PAGINA = `<!doctype html>
122
+ <html lang="pt-BR"><head><meta charset="utf-8">
123
+ <meta name="viewport" content="width=device-width, initial-scale=1">
124
+ <title>PrimoCode · Ao vivo</title>
125
+ <style>
126
+ * { box-sizing: border-box; margin: 0; }
127
+ body { background: #0b1220; color: #e2e8f0; font: 14px/1.5 ui-monospace, 'Cascadia Code', Consolas, monospace; height: 100vh; display: flex; flex-direction: column; }
128
+ header { display: flex; align-items: center; gap: 10px; padding: 12px 18px; border-bottom: 1px solid #1e293b; }
129
+ .dot { width: 9px; height: 9px; border-radius: 50%; background: #22d3ee; animation: pulsa 1.4s infinite; }
130
+ @keyframes pulsa { 50% { opacity: .25 } }
131
+ header b { color: #22d3ee; font-weight: 600; }
132
+ header span { color: #64748b; font-size: 12px; margin-left: auto; }
133
+ main { flex: 1; display: flex; min-height: 0; }
134
+ #tela { flex: 1; display: flex; align-items: center; justify-content: center; padding: 14px; min-width: 0; background: #0f172a; }
135
+ #tela img { max-width: 100%; max-height: 100%; border: 1px solid #1e293b; border-radius: 8px; display: none; }
136
+ #aviso { color: #64748b; text-align: center; max-width: 46ch; }
137
+ aside { width: 360px; max-width: 45%; border-left: 1px solid #1e293b; overflow-y: auto; padding: 10px 0; }
138
+ .item { padding: 6px 14px; display: flex; gap: 8px; border-left: 2px solid transparent; }
139
+ .item.novo { border-left-color: #22d3ee; background: #0f172a; }
140
+ .item .n { color: #475569; min-width: 3ch; text-align: right; }
141
+ .item .ok { color: #22d3ee; }
142
+ .item .falha { color: #fb7185; }
143
+ .item .txt { flex: 1; word-break: break-word; }
144
+ .item .hora { color: #475569; font-size: 11px; }
145
+ .item .erro { color: #fb7185; font-size: 12px; display: block; }
146
+ </style></head>
147
+ <body>
148
+ <header><span class="dot"></span><b>PrimoCode</b> · Ao vivo <span id="st">conectando…</span></header>
149
+ <main>
150
+ <section id="tela">
151
+ <img id="frame" alt="tela do computador">
152
+ <div id="aviso">Aguardando a primeira ação na tela…<br><br>Dê uma instrução no terminal do PrimoCode e acompanhe cada passo aqui.</div>
153
+ </section>
154
+ <aside id="log"></aside>
155
+ </main>
156
+ <script>
157
+ let ultimoFrame = -1, ultimoPasso = -1;
158
+ async function tick() {
159
+ try {
160
+ const s = await (await fetch('/state')).json();
161
+ document.getElementById('st').textContent = 'ao vivo · porta ' + location.port;
162
+ if (s.frame !== ultimoFrame && s.frame > 0) {
163
+ ultimoFrame = s.frame;
164
+ const img = document.getElementById('frame');
165
+ img.src = '/frame?' + s.frame;
166
+ img.style.display = 'block';
167
+ document.getElementById('aviso').style.display = 'none';
168
+ }
169
+ if (s.erroFoto) {
170
+ document.getElementById('aviso').textContent = 'Sem foto da tela: ' + s.erroFoto;
171
+ }
172
+ const fim = s.log.length ? s.log[s.log.length - 1].n : -1;
173
+ if (fim !== ultimoPasso) {
174
+ ultimoPasso = fim;
175
+ const el = document.getElementById('log');
176
+ el.innerHTML = s.log.slice().reverse().map((e, i) =>
177
+ '<div class="item' + (i === 0 ? ' novo' : '') + '">'
178
+ + '<span class="n">' + e.n + '</span>'
179
+ + '<span class="' + (e.ok ? 'ok' : 'falha') + '">' + (e.ok ? '✔' : '✗') + '</span>'
180
+ + '<span class="txt">' + esc(e.acao)
181
+ + (e.erro ? '<span class="erro">' + esc(e.erro) + '</span>' : '')
182
+ + '</span><span class="hora">' + e.hora + '</span></div>'
183
+ ).join('');
184
+ }
185
+ } catch (e) {
186
+ document.getElementById('st').textContent = 'reconectando…';
187
+ }
188
+ setTimeout(tick, 700);
189
+ }
190
+ function esc(s) { return String(s ?? '').replace(/[&<>"]/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c])); }
191
+ tick();
192
+ </script>
193
+ </body></html>`;
194
+
195
+ function start() {
196
+ if (state.on) return Promise.resolve(state.url);
197
+ try { fs.mkdirSync(DIR, { recursive: true }); } catch {}
198
+
199
+ return new Promise((resolve, reject) => {
200
+ const server = http.createServer((req, res) => {
201
+ const rota = (req.url || '/').split('?')[0];
202
+ if (rota === '/state') {
203
+ res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8', 'Cache-Control': 'no-store' });
204
+ res.end(JSON.stringify({ frame: state.frame, erroFoto: state.erroFoto, log: state.log.slice(-60) }));
205
+ return;
206
+ }
207
+ if (rota === '/frame') {
208
+ fs.readFile(FRAME, (err, buf) => {
209
+ if (err) { res.writeHead(404); res.end(); return; }
210
+ res.writeHead(200, { 'Content-Type': 'image/png', 'Cache-Control': 'no-store' });
211
+ res.end(buf);
212
+ });
213
+ return;
214
+ }
215
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
216
+ res.end(PAGINA);
217
+ });
218
+
219
+ server.on('error', reject);
220
+ // unref: o painel não pode segurar o processo vivo — /exit tem de sair
221
+ // mesmo com o streaming ligado.
222
+ server.on('connection', (s) => s.unref());
223
+ // Só na própria máquina: isto é a tela do usuário, não um serviço.
224
+ server.listen(0, '127.0.0.1', () => {
225
+ server.unref();
226
+ state.server = server;
227
+ state.on = true;
228
+ state.url = `http://127.0.0.1:${server.address().port}`;
229
+ resolve(state.url);
230
+ });
231
+ });
232
+ }
233
+
234
+ function stop() {
235
+ if (state.server) { try { state.server.close(); } catch {} }
236
+ state.server = null;
237
+ state.on = false;
238
+ state.url = '';
239
+ }
240
+
241
+ module.exports = { start, stop, step, isOn, url, descrever };
package/lib/tools.js CHANGED
@@ -436,6 +436,22 @@ async function despachar(name, args, ctx) {
436
436
  return desktop.scroll(args.dy != null ? args.dy : 300);
437
437
  case 'desktop_size':
438
438
  return desktop.getScreenSize();
439
+ case 'desktop_double_click':
440
+ return desktop.doubleClick(args.x, args.y);
441
+ case 'desktop_open_app':
442
+ return desktop.openApp(args.app || args.name || '');
443
+ case 'desktop_windows':
444
+ return desktop.listWindows();
445
+ case 'desktop_focus':
446
+ return desktop.focusWindow(args.window || args.title || '');
447
+ case 'desktop_elements':
448
+ return desktop.uiElements(args.window);
449
+ case 'desktop_click_element':
450
+ return desktop.clickElement(args.name || args.text || '', {
451
+ window: args.window,
452
+ double: !!args.double,
453
+ button: args.button,
454
+ });
439
455
 
440
456
  default:
441
457
  return { ok: false, error: `Tool não implementada: ${name}` };
package/lib/ui.js CHANGED
@@ -161,9 +161,9 @@ const TIPS = [
161
161
  ];
162
162
 
163
163
  const NEWS = [
164
+ 'Agora ele INTERAGE com o computador: janelas, botões e ícones pelo nome',
165
+ '/streaming abre um painel ao vivo — veja cada passo dele na tela',
164
166
  'Dois provedores em corrente: acabou a cota de um, o outro atende',
165
- 'Ele trabalha em ~/primocode. Fale o nome de uma pasta e ele muda sozinho',
166
- 'Falha de navegador agora diz quais seletores EXISTEM na página',
167
167
  ];
168
168
 
169
169
  // ── Cabeçalho ────────────────────────────────────────────────────────────
@@ -309,9 +309,12 @@ function actionHeader(name, args) {
309
309
  desktop_screenshot:'Desktop·shot', desktop_click:'Desktop·click', desktop_move:'Desktop·move',
310
310
  desktop_drag:'Desktop·drag', desktop_type:'Desktop·type', desktop_key:'Desktop·key',
311
311
  desktop_scroll:'Desktop·scroll', desktop_size:'Desktop·size',
312
+ desktop_double_click:'Desktop·2click', desktop_open_app:'Desktop·app',
313
+ desktop_windows:'Desktop·janelas', desktop_focus:'Desktop·foco',
314
+ desktop_elements:'Desktop·elementos', desktop_click_element:'Desktop·click',
312
315
  };
313
316
  const label = nice[name] || name;
314
- const argStr = args ? (args.path || args.pattern || args.command || args.target || args.url || args.selector || args.text || args.description || (args.todos ? `${args.todos.length} tarefas` : '') || (args.x != null ? `${args.x},${args.y}` : '') || args.key || JSON.stringify(args).slice(0,60)) : '';
317
+ const argStr = args ? (args.path || args.pattern || args.command || args.target || args.url || args.selector || args.text || args.app || args.window || args.name || args.description || (args.todos ? `${args.todos.length} tarefas` : '') || (args.x != null ? `${args.x},${args.y}` : '') || args.key || JSON.stringify(args).slice(0,60)) : '';
315
318
  return ` ${c.dim('⎿')} ${c.brand(label)}${argStr ? ' '+c.muted(argStr) : ''}`;
316
319
  }
317
320
 
package/package.json CHANGED
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "name": "primocode",
3
- "version": "7.10.1",
4
- "description": "PrimoCode — agente de engenharia com IA e cursor próprio. Modelos grátis. Cria arquivos, roda comandos, controla navegador e desktop.",
3
+ "version": "7.11.0",
4
+ "description": "PrimoCode — agente de engenharia com IA e cursor próprio. Modelos grátis. Cria arquivos, roda comandos, controla navegador e desktop: abre apps, clica em botões e ícones pelo nome, digita e usa atalhos.",
5
5
  "main": "bin/primocode.js",
6
6
  "bin": {
7
7
  "primocode": "bin/primocode.js"
8
8
  },
9
9
  "scripts": {
10
10
  "start": "node bin/primocode.js",
11
- "test": "node test/tools.test.js && node test/ui.test.js && node test/modo.test.js && node test/pasta.test.js && node test/catalogo.test.js"
11
+ "test": "node test/tools.test.js && node test/ui.test.js && node test/modo.test.js && node test/pasta.test.js && node test/catalogo.test.js && node test/desktop.test.js"
12
12
  },
13
13
  "engines": {
14
14
  "node": ">=18.17.0"