ft-scout 9.0.4 → 9.0.6

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.
Files changed (57) hide show
  1. package/.firebase/hosting.d2Vi.cache +8 -7
  2. package/AUTONOMOUS_FEATURES.md +192 -0
  3. package/README.md +67 -35
  4. package/bin/src/commands/agent.d.ts.map +1 -1
  5. package/bin/src/commands/agent.js +21 -8
  6. package/bin/src/commands/agent.js.map +1 -1
  7. package/bin/src/commands/login.d.ts.map +1 -1
  8. package/bin/src/commands/login.js +3 -1
  9. package/bin/src/commands/login.js.map +1 -1
  10. package/bin/src/engine/actionVerifier.d.ts.map +1 -1
  11. package/bin/src/engine/actionVerifier.js +9 -1
  12. package/bin/src/engine/actionVerifier.js.map +1 -1
  13. package/bin/src/engine/agentEngine.d.ts.map +1 -1
  14. package/bin/src/engine/agentEngine.js +319 -13
  15. package/bin/src/engine/agentEngine.js.map +1 -1
  16. package/bin/src/engine/browser/browserController.d.ts.map +1 -1
  17. package/bin/src/engine/browser/browserController.js +53 -13
  18. package/bin/src/engine/browser/browserController.js.map +1 -1
  19. package/bin/src/engine/computer/computerController.d.ts.map +1 -1
  20. package/bin/src/engine/computer/computerController.js +564 -296
  21. package/bin/src/engine/computer/computerController.js.map +1 -1
  22. package/bin/src/engine/computer/computerDiagnostic.d.ts.map +1 -0
  23. package/bin/src/engine/computer/computerDiagnostic.js +71 -0
  24. package/bin/src/engine/computer/computerDiagnostic.js.map +1 -0
  25. package/bin/src/engine/computer/index.d.ts.map +1 -1
  26. package/bin/src/engine/computer/index.js +1 -0
  27. package/bin/src/engine/computer/index.js.map +1 -1
  28. package/bin/src/engine/computer/mockComputer.d.ts.map +1 -1
  29. package/bin/src/engine/computer/mockComputer.js +104 -8
  30. package/bin/src/engine/computer/mockComputer.js.map +1 -1
  31. package/bin/src/engine/computer/scoutDriver.cs +1161 -0
  32. package/bin/src/engine/computer/scout_driver.exe +0 -0
  33. package/bin/src/engine/runtime/agentRuntime.d.ts.map +1 -1
  34. package/bin/src/engine/runtime/agentRuntime.js +375 -20
  35. package/bin/src/engine/runtime/agentRuntime.js.map +1 -1
  36. package/bin/src/engine/safety.d.ts.map +1 -1
  37. package/bin/src/engine/safety.js +17 -6
  38. package/bin/src/engine/safety.js.map +1 -1
  39. package/bin/src/engine/toolRegistry.d.ts.map +1 -1
  40. package/bin/src/engine/toolRegistry.js +209 -0
  41. package/bin/src/engine/toolRegistry.js.map +1 -1
  42. package/bin/src/engine/workspace/agentWorkspace.d.ts.map +1 -1
  43. package/bin/src/engine/workspace/agentWorkspace.js +21 -0
  44. package/bin/src/engine/workspace/agentWorkspace.js.map +1 -1
  45. package/bin/src/index.js +5 -4
  46. package/bin/src/index.js.map +1 -1
  47. package/bin/src/utils/auth.d.ts.map +1 -1
  48. package/bin/src/utils/auth.js +14 -1
  49. package/bin/src/utils/auth.js.map +1 -1
  50. package/bin/src/utils/branding.js +1 -1
  51. package/bin/src/utils/holoseneAuth.d.ts.map +1 -1
  52. package/bin/src/utils/holoseneAuth.js +169 -41
  53. package/bin/src/utils/holoseneAuth.js.map +1 -1
  54. package/firebase.json +8 -0
  55. package/package.json +1 -1
  56. package/web/app.js +3 -3
  57. package/web/styles.css +2 -2
@@ -1,13 +1,19 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
- import { exec } from 'child_process';
3
+ import { exec, execFile, spawn } from 'child_process';
4
4
  import { promisify } from 'util';
5
+ import { fileURLToPath } from 'url';
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = path.dirname(__filename);
5
8
  const execAsync = promisify(exec);
9
+ const execFileAsync = promisify(execFile);
6
10
  export class SystemComputerController {
7
11
  workspaceDir;
8
12
  screenshotsDir;
9
13
  expectedApplication = null;
10
14
  activeWindow = null;
15
+ driverExePath = null;
16
+ driverInitAttempted = false;
11
17
  constructor(workspaceDir = process.cwd()) {
12
18
  this.workspaceDir = workspaceDir;
13
19
  this.screenshotsDir = path.join(this.workspaceDir, '.ft', 'screenshots');
@@ -24,47 +30,246 @@ export class SystemComputerController {
24
30
  getExpectedApplication() {
25
31
  return this.expectedApplication;
26
32
  }
27
- async runPowerShell(script) {
28
- const encoded = Buffer.from(script, 'utf16le').toString('base64');
29
- const { stdout } = await execAsync(`powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -EncodedCommand ${encoded}`);
30
- return (stdout || '').trim();
33
+ /**
34
+ * Resolves or builds the native scout_driver.exe for Windows.
35
+ */
36
+ async ensureDriver() {
37
+ if (process.platform !== 'win32') {
38
+ return null;
39
+ }
40
+ const csSourceCandidates = [
41
+ path.join(__dirname, 'scoutDriver.cs'),
42
+ path.join(this.workspaceDir, 'bin', 'src', 'engine', 'computer', 'scoutDriver.cs'),
43
+ ];
44
+ const csSource = csSourceCandidates.find((p) => fs.existsSync(p));
45
+ const cscExe = 'C:\\Windows\\Microsoft.NET\\Framework64\\v4.0.30319\\csc.exe';
46
+ if (this.driverExePath && fs.existsSync(this.driverExePath)) {
47
+ // If C# source exists and is newer than the cached binary, recompile to pick up changes
48
+ if (csSource && fs.existsSync(cscExe)) {
49
+ try {
50
+ const srcMtime = fs.statSync(csSource).mtimeMs;
51
+ const exeMtime = fs.statSync(this.driverExePath).mtimeMs;
52
+ if (srcMtime > exeMtime) {
53
+ await execAsync(`"${cscExe}" /nologo /optimize /t:exe /out:"${this.driverExePath}" /r:System.Windows.Forms.dll,System.Drawing.dll "${csSource}"`);
54
+ }
55
+ }
56
+ catch { }
57
+ }
58
+ return this.driverExePath;
59
+ }
60
+ // Candidate locations
61
+ const candidates = [
62
+ path.join(__dirname, 'scout_driver.exe'),
63
+ path.join(this.workspaceDir, 'bin', 'src', 'engine', 'computer', 'scout_driver.exe'),
64
+ path.join(this.workspaceDir, '.ft', 'bin', 'scout_driver.exe'),
65
+ ];
66
+ for (const c of candidates) {
67
+ if (fs.existsSync(c)) {
68
+ // Check if source is newer than candidate exe
69
+ if (csSource && fs.existsSync(cscExe)) {
70
+ try {
71
+ const srcMtime = fs.statSync(csSource).mtimeMs;
72
+ const exeMtime = fs.statSync(c).mtimeMs;
73
+ if (srcMtime > exeMtime) {
74
+ await execAsync(`"${cscExe}" /nologo /optimize /t:exe /out:"${c}" /r:System.Windows.Forms.dll,System.Drawing.dll "${csSource}"`);
75
+ }
76
+ }
77
+ catch { }
78
+ }
79
+ this.driverExePath = c;
80
+ return c;
81
+ }
82
+ }
83
+ if (this.driverInitAttempted) {
84
+ return null;
85
+ }
86
+ this.driverInitAttempted = true;
87
+ // Try compiling using csc.exe if no candidate binary found
88
+ if (csSource && fs.existsSync(cscExe)) {
89
+ const outDir = path.join(this.workspaceDir, '.ft', 'bin');
90
+ try {
91
+ if (!fs.existsSync(outDir))
92
+ fs.mkdirSync(outDir, { recursive: true });
93
+ const targetExe = path.join(outDir, 'scout_driver.exe');
94
+ await execAsync(`"${cscExe}" /nologo /optimize /t:exe /out:"${targetExe}" /r:System.Windows.Forms.dll,System.Drawing.dll "${csSource}"`);
95
+ if (fs.existsSync(targetExe)) {
96
+ this.driverExePath = targetExe;
97
+ return targetExe;
98
+ }
99
+ }
100
+ catch { }
101
+ }
102
+ return null;
103
+ }
104
+ /**
105
+ * Runs an action using native driver or fallback.
106
+ */
107
+ async runDriverAction(action, args = []) {
108
+ const driver = await this.ensureDriver();
109
+ const strArgs = args.map((a) => String(a));
110
+ if (driver) {
111
+ try {
112
+ const { stdout } = await execFileAsync(driver, [action, ...strArgs], {
113
+ windowsHide: true,
114
+ timeout: 10000,
115
+ });
116
+ const trimmed = (stdout || '').trim();
117
+ if (trimmed.startsWith('{') && trimmed.endsWith('}')) {
118
+ try {
119
+ return JSON.parse(trimmed);
120
+ }
121
+ catch { }
122
+ }
123
+ return { success: true, output: trimmed };
124
+ }
125
+ catch (err) {
126
+ return {
127
+ success: false,
128
+ output: `Driver execution error: ${err?.message || String(err)}`,
129
+ error: err?.message || String(err),
130
+ recoverable: true,
131
+ };
132
+ }
133
+ }
134
+ // PowerShell fallback with input desktop attachment
135
+ return await this.runPowerShellFallback(action, strArgs);
136
+ }
137
+ /**
138
+ * Reliable PowerShell fallback with thread input desktop attachment
139
+ */
140
+ async runPowerShellFallback(action, args) {
141
+ const script = `
142
+ Add-Type -AssemblyName System.Windows.Forms
143
+ Add-Type -AssemblyName System.Drawing
144
+
145
+ $src = @"
146
+ using System;
147
+ using System.Drawing;
148
+ using System.Drawing.Imaging;
149
+ using System.Threading;
150
+ using System.Runtime.InteropServices;
151
+ public class WinDriverFallback {
152
+ [DllImport("user32.dll", SetLastError = true)]
153
+ public static extern IntPtr OpenWindowStation(string lpszWinSta, bool fInherit, uint dwDesiredAccess);
154
+ [DllImport("user32.dll", SetLastError = true)]
155
+ public static extern bool SetProcessWindowStation(IntPtr hWinSta);
156
+ [DllImport("user32.dll", SetLastError = true)]
157
+ public static extern IntPtr OpenInputDesktop(uint dwFlags, bool fInherit, uint dwDesiredAccess);
158
+ [DllImport("user32.dll", SetLastError = true)]
159
+ public static extern IntPtr OpenDesktop(string lpszDesktop, uint dwFlags, bool fInherit, uint dwDesiredAccess);
160
+ [DllImport("user32.dll", SetLastError = true)]
161
+ public static extern bool SetThreadDesktop(IntPtr hDesktop);
162
+ [DllImport("user32.dll", SetLastError = true)]
163
+ public static extern bool GetCursorPos(out POINT lpPoint);
164
+ [DllImport("user32.dll", SetLastError = true)]
165
+ public static extern bool SetCursorPos(int X, int Y);
166
+ [DllImport("user32.dll", SetLastError = true)]
167
+ public static extern void mouse_event(uint dwFlags, uint dx, uint dy, uint cButtons, uint dwExtraInfo);
168
+ [DllImport("user32.dll")]
169
+ public static extern int GetSystemMetrics(int nIndex);
170
+ [DllImport("user32.dll")]
171
+ public static extern bool SetProcessDPIAware();
172
+
173
+ [StructLayout(LayoutKind.Sequential)]
174
+ public struct POINT { public int X; public int Y; }
175
+
176
+ public static void Attach() {
177
+ try {
178
+ SetProcessDPIAware();
179
+ IntPtr hw = OpenWindowStation("WinSta0", false, 0x10000000);
180
+ if (hw != IntPtr.Zero) SetProcessWindowStation(hw);
181
+ IntPtr hd = OpenInputDesktop(0, false, 0x10000000);
182
+ if (hd == IntPtr.Zero) hd = OpenDesktop("Default", 0, false, 0x10000000);
183
+ if (hd != IntPtr.Zero) SetThreadDesktop(hd);
184
+ } catch {}
185
+ }
186
+ }
187
+ "@
188
+ Add-Type -TypeDefinition $src -ReferencedAssemblies System.Drawing
189
+
190
+ [WinDriverFallback]::Attach()
191
+ $act = "${action.replace(/"/g, '`"')}".ToLower()
192
+
193
+ if ($act -eq "move") {
194
+ $x = [int]${Number(args[0]) || 0}
195
+ $y = [int]${Number(args[1]) || 0}
196
+ [WinDriverFallback]::SetCursorPos($x, $y)
197
+ Write-Output "{\`"success\`":true,\`"action\`":\`"move\`",\`"coordinates\`":{\`"x\`":$x,\`"y\`":$y}}"
198
+ } elseif ($act -eq "click" -or $act -eq "left_click") {
199
+ $x = [int]${Number(args[0]) || 0}
200
+ $y = [int]${Number(args[1]) || 0}
201
+ [WinDriverFallback]::SetCursorPos($x, $y)
202
+ Start-Sleep -Milliseconds 30
203
+ [WinDriverFallback]::mouse_event(0x0002, 0, 0, 0, 0)
204
+ Start-Sleep -Milliseconds 40
205
+ [WinDriverFallback]::mouse_event(0x0004, 0, 0, 0, 0)
206
+ Write-Output "{\`"success\`":true,\`"action\`":\`"click\`",\`"button\`":\`"left\`",\`"coordinates\`":{\`"x\`":$x,\`"y\`":$y}}"
207
+ } elseif ($act -eq "right_click") {
208
+ $x = [int]${Number(args[0]) || 0}
209
+ $y = [int]${Number(args[1]) || 0}
210
+ [WinDriverFallback]::SetCursorPos($x, $y)
211
+ Start-Sleep -Milliseconds 30
212
+ [WinDriverFallback]::mouse_event(0x0008, 0, 0, 0, 0)
213
+ Start-Sleep -Milliseconds 40
214
+ [WinDriverFallback]::mouse_event(0x0010, 0, 0, 0, 0)
215
+ Write-Output "{\`"success\`":true,\`"action\`":\`"right_click\`",\`"button\`":\`"right\`",\`"coordinates\`":{\`"x\`":$x,\`"y\`":$y}}"
216
+ } elseif ($act -eq "double_click") {
217
+ $x = [int]${Number(args[0]) || 0}
218
+ $y = [int]${Number(args[1]) || 0}
219
+ [WinDriverFallback]::SetCursorPos($x, $y)
220
+ Start-Sleep -Milliseconds 30
221
+ [WinDriverFallback]::mouse_event(0x0002, 0, 0, 0, 0)
222
+ Start-Sleep -Milliseconds 40
223
+ [WinDriverFallback]::mouse_event(0x0004, 0, 0, 0, 0)
224
+ Start-Sleep -Milliseconds 80
225
+ [WinDriverFallback]::mouse_event(0x0002, 0, 0, 0, 0)
226
+ Start-Sleep -Milliseconds 40
227
+ [WinDriverFallback]::mouse_event(0x0004, 0, 0, 0, 0)
228
+ Write-Output "{\`"success\`":true,\`"action\`":\`"double_click\`",\`"coordinates\`":{\`"x\`":$x,\`"y\`":$y}}"
229
+ } elseif ($act -eq "screenshot") {
230
+ $w = [WinDriverFallback]::GetSystemMetrics(0)
231
+ $h = [WinDriverFallback]::GetSystemMetrics(1)
232
+ if ($w -le 0) { $w = 1920 }
233
+ if ($h -le 0) { $h = 1080 }
234
+ $p = "${(args[0] || '').replace(/\\/g, '\\\\').replace(/"/g, '`"')}"
235
+ $bmp = New-Object System.Drawing.Bitmap $w, $h
236
+ $g = [System.Drawing.Graphics]::FromImage($bmp)
237
+ $g.CopyFromScreen(0, 0, 0, 0, (New-Object System.Drawing.Size($w, $h)), [System.Drawing.CopyPixelOperation]::SourceCopy)
238
+ $bmp.Save($p, [System.Drawing.Imaging.ImageFormat]::Png)
239
+ $g.Dispose()
240
+ $bmp.Dispose()
241
+ Write-Output "{\`"success\`":true,\`"action\`":\`"screenshot\`",\`"width\`":$w,\`"height\`":$h,\`"path\`":\`"$p\`"}"
242
+ } else {
243
+ Write-Output "{\`"success\`":true,\`"action\`":\`"$act\`"}"
244
+ }
245
+ `;
246
+ try {
247
+ const encoded = Buffer.from(script, 'utf16le').toString('base64');
248
+ const { stdout } = await execAsync(`powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -EncodedCommand ${encoded}`);
249
+ const trimmed = (stdout || '').trim();
250
+ if (trimmed.startsWith('{') && trimmed.endsWith('}')) {
251
+ return JSON.parse(trimmed);
252
+ }
253
+ return { success: true, output: trimmed };
254
+ }
255
+ catch (err) {
256
+ return { success: false, output: `PowerShell fallback failed: ${err?.message || String(err)}`, error: err?.message, recoverable: true };
257
+ }
31
258
  }
32
259
  async screenshot() {
33
260
  const timestamp = new Date().toISOString();
34
261
  const filename = `screen_${Date.now()}.png`;
35
262
  const filePath = path.join(this.screenshotsDir, filename);
36
263
  if (process.platform === 'win32') {
37
- try {
38
- const psScript = `
39
- Add-Type -AssemblyName System.Windows.Forms
40
- Add-Type -AssemblyName System.Drawing
41
- $bounds = [System.Windows.Forms.Screen]::PrimaryScreen.Bounds
42
- $bmp = New-Object System.Drawing.Bitmap $bounds.Width, $bounds.Height
43
- $graphics = [System.Drawing.Graphics]::FromImage($bmp)
44
- $graphics.CopyFromScreen($bounds.Location, [System.Drawing.Point]::Empty, $bounds.Size)
45
- $bmp.Save('${filePath.replace(/'/g, "''")}', [System.Drawing.Imaging.ImageFormat]::Png)
46
- $graphics.Dispose()
47
- $bmp.Dispose()
48
- Write-Output "$($bounds.Width)x$($bounds.Height)"
49
- `.trim();
50
- const { stdout } = await execAsync(`powershell -NoProfile -NonInteractive -Command "${psScript.replace(/\n/g, '; ')}"`);
51
- const dims = stdout.trim().split('x');
52
- const width = parseInt(dims[0] || '1920', 10);
53
- const height = parseInt(dims[1] || '1080', 10);
54
- const ui = await this.inspectUI();
264
+ const res = await this.runDriverAction('screenshot', [filePath]);
265
+ if (res && res.success) {
266
+ const activeWin = res.activeWindow || (await this.inspectUI()).activeWindow;
267
+ this.activeWindow = activeWin;
55
268
  return {
56
269
  path: filePath,
57
- width,
58
- height,
59
- activeWindow: ui.activeWindow,
60
- timestamp,
61
- };
62
- }
63
- catch (err) {
64
- return {
65
- width: 1920,
66
- height: 1080,
67
- activeWindow: 'Desktop',
270
+ width: Number(res.width) || 1920,
271
+ height: Number(res.height) || 1080,
272
+ activeWindow: activeWin,
68
273
  timestamp,
69
274
  };
70
275
  }
@@ -79,7 +284,6 @@ Write-Output "$($bounds.Width)x$($bounds.Height)"
79
284
  }
80
285
  }
81
286
  else {
82
- // Linux fallback (scrot or import)
83
287
  try {
84
288
  await execAsync(`scrot "${filePath}"`);
85
289
  return { path: filePath, width: 1920, height: 1080, activeWindow: 'Desktop', timestamp };
@@ -88,258 +292,293 @@ Write-Output "$($bounds.Width)x$($bounds.Height)"
88
292
  return { width: 1920, height: 1080, timestamp };
89
293
  }
90
294
  }
295
+ return { width: 1920, height: 1080, activeWindow: 'Desktop', timestamp };
91
296
  }
92
297
  async click(x, y, options) {
93
298
  const btn = options?.button || 'left';
94
299
  const count = options?.clicks || 1;
95
300
  if (process.platform === 'win32') {
96
- try {
97
- const psScript = `
98
- Add-Type -AssemblyName System.Windows.Forms
99
- [System.Windows.Forms.Cursor]::Position = New-Object System.Drawing.Point(${Math.round(x)}, ${Math.round(y)})
100
- $src = @"
101
- using System;
102
- using System.Runtime.InteropServices;
103
- public class WinMouse {
104
- [DllImport("user32.dll")]
105
- public static extern void mouse_event(uint dwFlags, uint dx, uint dy, uint cButtons, uint dwExtraInfo);
106
- }
107
- "@
108
- Add-Type -TypeDefinition $src -ErrorAction SilentlyContinue
109
- # MOUSEEVENTF_LEFTDOWN = 0x0002, MOUSEEVENTF_LEFTUP = 0x0004
110
- # MOUSEEVENTF_RIGHTDOWN = 0x0008, MOUSEEVENTF_RIGHTUP = 0x0010
111
- ${btn === 'right'
112
- ? `[WinMouse]::mouse_event(0x0008, 0, 0, 0, 0); Start-Sleep -Milliseconds 50; [WinMouse]::mouse_event(0x0010, 0, 0, 0, 0)`
113
- : `for ($i=0; $i -lt ${count}; $i++) { [WinMouse]::mouse_event(0x0002, 0, 0, 0, 0); Start-Sleep -Milliseconds 40; [WinMouse]::mouse_event(0x0004, 0, 0, 0, 0); Start-Sleep -Milliseconds 50 }`}
114
- `.trim();
115
- await execAsync(`powershell -NoProfile -NonInteractive -Command "${psScript.replace(/\n/g, '; ')}"`);
116
- return { success: true, output: `Clicked ${btn} button at (${x}, ${y})` };
117
- }
118
- catch (err) {
119
- return { success: false, output: `Click failed at (${x}, ${y}): ${err?.message || String(err)}` };
120
- }
301
+ const act = count === 2 ? 'double_click' : btn === 'right' ? 'right_click' : 'click';
302
+ const res = await this.runDriverAction(act, [x, y, btn]);
303
+ const success = Boolean(res?.success);
304
+ const coords = res?.coordinates || { x, y };
305
+ const activeWin = res?.activeWindow || this.activeWindow || undefined;
306
+ return {
307
+ success,
308
+ output: success
309
+ ? `Clicked ${btn} button at (${coords.x}, ${coords.y})`
310
+ : `Click failed at (${x}, ${y}): ${res?.error || 'Unknown error'}`,
311
+ action: 'click',
312
+ coordinates: coords,
313
+ button: btn,
314
+ error: res?.error,
315
+ recoverable: res?.recoverable ?? true,
316
+ activeWindow: activeWin,
317
+ };
121
318
  }
122
- return { success: true, output: `Simulated click at (${x}, ${y}) on platform ${process.platform}` };
319
+ return {
320
+ success: true,
321
+ output: `Simulated click ${btn} at (${x}, ${y})`,
322
+ action: 'click',
323
+ coordinates: { x, y },
324
+ button: btn,
325
+ };
326
+ }
327
+ async leftClick(x, y) {
328
+ return this.click(x, y, { button: 'left', clicks: 1 });
329
+ }
330
+ async rightClick(x, y) {
331
+ return this.click(x, y, { button: 'right', clicks: 1 });
123
332
  }
124
333
  async doubleClick(x, y) {
125
- return this.click(x, y, { button: 'left', clicks: 2 });
334
+ if (process.platform === 'win32') {
335
+ const res = await this.runDriverAction('double_click', [x, y]);
336
+ const success = Boolean(res?.success);
337
+ const coords = res?.coordinates || { x, y };
338
+ return {
339
+ success,
340
+ output: success
341
+ ? `Double-clicked at (${coords.x}, ${coords.y})`
342
+ : `Double-click failed at (${x}, ${y}): ${res?.error || 'Unknown error'}`,
343
+ action: 'double_click',
344
+ coordinates: coords,
345
+ button: 'left',
346
+ error: res?.error,
347
+ recoverable: res?.recoverable ?? true,
348
+ activeWindow: res?.activeWindow || this.activeWindow || undefined,
349
+ };
350
+ }
351
+ return {
352
+ success: true,
353
+ output: `Simulated double-click at (${x}, ${y})`,
354
+ action: 'double_click',
355
+ coordinates: { x, y },
356
+ button: 'left',
357
+ };
126
358
  }
127
- async type(text) {
128
- if (!text)
129
- return { success: true, output: 'Typed empty string', action: 'type' };
359
+ async mouseDown(x, y, button) {
360
+ const btn = button || 'left';
130
361
  if (process.platform === 'win32') {
131
- try {
132
- const targetApp = this.expectedApplication || 'Notepad';
133
- const escaped = text
134
- .replace(/\{/g, '{{}')
135
- .replace(/\}/g, '{}}')
136
- .replace(/\+/g, '{+}')
137
- .replace(/\^/g, '{^}')
138
- .replace(/%/g, '{%}')
139
- .replace(/~/g, '{~}')
140
- .replace(/\(/g, '{(}')
141
- .replace(/\)/g, '{)}')
142
- .replace(/\[/g, '{[}')
143
- .replace(/\]/g, '{]}');
144
- const psScript = `
145
- $wshell = New-Object -ComObject Wscript.Shell
146
- $null = $wshell.AppActivate('${targetApp}')
147
- Start-Sleep -Milliseconds 250
148
- Add-Type -AssemblyName System.Windows.Forms
149
- [System.Windows.Forms.SendKeys]::SendWait("${escaped.replace(/"/g, '`"')}")
150
- `.trim();
151
- await this.runPowerShell(psScript);
152
- this.activeWindow = targetApp;
153
- return {
154
- success: true,
155
- output: `Typed text (${text.length} chars) into active application "${targetApp}"`,
156
- action: 'type',
157
- activeWindow: targetApp,
158
- expectedWindow: targetApp,
159
- };
160
- }
161
- catch (err) {
162
- return {
163
- success: false,
164
- output: `Type error on active window "${this.expectedApplication || 'foreground'}": ${err?.message || String(err)}`,
165
- action: 'type',
166
- error: err?.message || String(err),
167
- recoverable: true,
168
- expectedWindow: this.expectedApplication || undefined,
169
- };
170
- }
362
+ const res = await this.runDriverAction('mouse_down', [x ?? -1, y ?? -1, btn]);
363
+ const coords = res?.coordinates || (x !== undefined && y !== undefined ? { x, y } : undefined);
364
+ return {
365
+ success: Boolean(res?.success),
366
+ output: `Mouse down (${btn}) at (${coords?.x ?? 'current'}, ${coords?.y ?? 'current'})`,
367
+ action: 'mouse_down',
368
+ button: btn,
369
+ coordinates: coords,
370
+ error: res?.error,
371
+ recoverable: true,
372
+ };
171
373
  }
172
374
  return {
173
375
  success: true,
174
- output: `Simulated type (${text.length} chars)`,
175
- action: 'type',
176
- activeWindow: this.expectedApplication || 'Simulated Window',
376
+ output: `Simulated mouse down (${btn})`,
377
+ action: 'mouse_down',
378
+ button: btn,
379
+ coordinates: x !== undefined && y !== undefined ? { x, y } : undefined,
177
380
  };
178
381
  }
179
- async keyPress(key) {
180
- const keyMap = {
181
- enter: '{ENTER}',
182
- return: '{ENTER}',
183
- tab: '{TAB}',
184
- escape: '{ESC}',
185
- esc: '{ESC}',
186
- backspace: '{BACKSPACE}',
187
- delete: '{DEL}',
188
- up: '{UP}',
189
- down: '{DOWN}',
190
- left: '{LEFT}',
191
- right: '{RIGHT}',
192
- space: ' ',
193
- home: '{HOME}',
194
- end: '{END}',
195
- pageup: '{PGUP}',
196
- pagedown: '{PGDN}',
382
+ async mouseUp(x, y, button) {
383
+ const btn = button || 'left';
384
+ if (process.platform === 'win32') {
385
+ const res = await this.runDriverAction('mouse_up', [x ?? -1, y ?? -1, btn]);
386
+ const coords = res?.coordinates || (x !== undefined && y !== undefined ? { x, y } : undefined);
387
+ return {
388
+ success: Boolean(res?.success),
389
+ output: `Mouse up (${btn}) at (${coords?.x ?? 'current'}, ${coords?.y ?? 'current'})`,
390
+ action: 'mouse_up',
391
+ button: btn,
392
+ coordinates: coords,
393
+ error: res?.error,
394
+ recoverable: true,
395
+ };
396
+ }
397
+ return {
398
+ success: true,
399
+ output: `Simulated mouse up (${btn})`,
400
+ action: 'mouse_up',
401
+ button: btn,
402
+ coordinates: x !== undefined && y !== undefined ? { x, y } : undefined,
197
403
  };
198
- const mapped = keyMap[key.toLowerCase()] || key;
404
+ }
405
+ async mouseMove(x, y) {
406
+ return this.move(x, y);
407
+ }
408
+ async move(x, y) {
199
409
  if (process.platform === 'win32') {
200
- try {
201
- const targetApp = this.expectedApplication || '';
202
- const psScript = `
203
- $wshell = New-Object -ComObject Wscript.Shell
204
- ${targetApp ? `$null = $wshell.AppActivate('${targetApp}')\nStart-Sleep -Milliseconds 150` : ''}
205
- Add-Type -AssemblyName System.Windows.Forms
206
- [System.Windows.Forms.SendKeys]::SendWait("${mapped}")
207
- `.trim();
208
- await this.runPowerShell(psScript);
209
- return {
210
- success: true,
211
- output: `Pressed key: ${key}`,
212
- action: 'key_press',
213
- activeWindow: targetApp || undefined,
214
- };
215
- }
216
- catch (err) {
217
- return {
218
- success: false,
219
- output: `KeyPress error: ${err?.message || String(err)}`,
220
- action: 'key_press',
221
- error: err?.message || String(err),
222
- recoverable: true,
223
- };
224
- }
410
+ const res = await this.runDriverAction('move', [x, y]);
411
+ const success = Boolean(res?.success);
412
+ const coords = res?.coordinates || { x, y };
413
+ return {
414
+ success,
415
+ output: success
416
+ ? `Moved mouse cursor to (${coords.x}, ${coords.y})`
417
+ : `MouseMove failed to (${x}, ${y}): ${res?.error || 'Unknown error'}`,
418
+ action: 'move',
419
+ coordinates: coords,
420
+ error: res?.error,
421
+ recoverable: res?.recoverable ?? true,
422
+ activeWindow: res?.activeWindow || this.activeWindow || undefined,
423
+ };
225
424
  }
226
- return { success: true, output: `Simulated key press: ${key}`, action: 'key_press' };
425
+ return {
426
+ success: true,
427
+ output: `Simulated mouse move to (${x}, ${y})`,
428
+ action: 'move',
429
+ coordinates: { x, y },
430
+ };
227
431
  }
228
- async hotkey(keys) {
229
- if (!keys || keys.length === 0)
230
- return { success: false, output: 'No keys provided for hotkey', action: 'hotkey' };
432
+ async drag(startX, startY, endX, endY) {
231
433
  if (process.platform === 'win32') {
232
- try {
233
- const targetApp = this.expectedApplication || '';
234
- let prefix = '';
235
- let mainKey = '';
236
- for (const k of keys) {
237
- const lk = k.toLowerCase();
238
- if (lk === 'ctrl' || lk === 'control')
239
- prefix += '^';
240
- else if (lk === 'alt')
241
- prefix += '%';
242
- else if (lk === 'shift')
243
- prefix += '+';
244
- else
245
- mainKey = k;
246
- }
247
- const sendPattern = `${prefix}${mainKey.toLowerCase()}`;
248
- const psScript = `
249
- $wshell = New-Object -ComObject Wscript.Shell
250
- ${targetApp ? `$null = $wshell.AppActivate('${targetApp}')\nStart-Sleep -Milliseconds 150` : ''}
251
- Add-Type -AssemblyName System.Windows.Forms
252
- [System.Windows.Forms.SendKeys]::SendWait("${sendPattern}")
253
- `.trim();
254
- await this.runPowerShell(psScript);
255
- return {
256
- success: true,
257
- output: `Executed hotkey: ${keys.join('+')} on "${targetApp || 'active window'}"`,
258
- action: 'hotkey',
259
- activeWindow: targetApp || undefined,
260
- };
261
- }
262
- catch (err) {
263
- return {
264
- success: false,
265
- output: `Hotkey error: ${err?.message || String(err)}`,
266
- action: 'hotkey',
267
- error: err?.message || String(err),
268
- recoverable: true,
269
- };
270
- }
434
+ const res = await this.runDriverAction('drag', [startX, startY, endX, endY]);
435
+ const success = Boolean(res?.success);
436
+ return {
437
+ success,
438
+ output: success
439
+ ? `Dragged from (${startX}, ${startY}) to (${endX}, ${endY})`
440
+ : `Drag failed: ${res?.error || 'Unknown error'}`,
441
+ action: 'drag',
442
+ from: { x: startX, y: startY },
443
+ to: { x: endX, y: endY },
444
+ coordinates: res?.coordinates || { x: endX, y: endY },
445
+ error: res?.error,
446
+ recoverable: res?.recoverable ?? true,
447
+ };
271
448
  }
272
- return { success: true, output: `Simulated hotkey: ${keys.join('+')}`, action: 'hotkey' };
449
+ return {
450
+ success: true,
451
+ output: `Simulated drag from (${startX}, ${startY}) to (${endX}, ${endY})`,
452
+ action: 'drag',
453
+ from: { x: startX, y: startY },
454
+ to: { x: endX, y: endY },
455
+ coordinates: { x: endX, y: endY },
456
+ };
273
457
  }
274
458
  async scroll(deltaX, deltaY) {
275
459
  if (process.platform === 'win32') {
276
- try {
277
- const psScript = `
278
- $src = @"
279
- using System;
280
- using System.Runtime.InteropServices;
281
- public class WinMouseScroll {
282
- [DllImport("user32.dll")]
283
- public static extern void mouse_event(uint dwFlags, uint dx, uint dy, uint cButtons, uint dwExtraInfo);
284
- }
285
- "@
286
- Add-Type -TypeDefinition $src -ErrorAction SilentlyContinue
287
- # MOUSEEVENTF_WHEEL = 0x0800
288
- [WinMouseScroll]::mouse_event(0x0800, 0, 0, [uint32]${Math.round(-deltaY * 120)}, 0)
289
- `.trim();
290
- await execAsync(`powershell -NoProfile -NonInteractive -Command "${psScript.replace(/\n/g, '; ')}"`);
291
- return { success: true, output: `Scrolled deltaY=${deltaY}, deltaX=${deltaX}` };
292
- }
293
- catch (err) {
294
- return { success: false, output: `Scroll error: ${err?.message || String(err)}` };
460
+ const res = await this.runDriverAction('scroll', [deltaX, deltaY]);
461
+ return {
462
+ success: Boolean(res?.success),
463
+ output: `Scrolled deltaY=${deltaY}, deltaX=${deltaX}`,
464
+ action: 'scroll',
465
+ details: { deltaX, deltaY },
466
+ };
467
+ }
468
+ return {
469
+ success: true,
470
+ output: `Simulated scroll deltaY=${deltaY}, deltaX=${deltaX}`,
471
+ action: 'scroll',
472
+ details: { deltaX, deltaY },
473
+ };
474
+ }
475
+ async getMousePosition() {
476
+ if (process.platform === 'win32') {
477
+ const res = await this.runDriverAction('position', []);
478
+ if (res && res.coordinates) {
479
+ return res.coordinates;
295
480
  }
296
481
  }
297
- return { success: true, output: `Simulated scroll deltaY=${deltaY}` };
482
+ return { x: 0, y: 0 };
298
483
  }
299
- async mouseMove(x, y) {
484
+ async runDiagnostic() {
300
485
  if (process.platform === 'win32') {
301
- try {
302
- const psScript = `
303
- Add-Type -AssemblyName System.Windows.Forms
304
- [System.Windows.Forms.Cursor]::Position = New-Object System.Drawing.Point(${Math.round(x)}, ${Math.round(y)})
305
- `.trim();
306
- await execAsync(`powershell -NoProfile -NonInteractive -Command "${psScript.replace(/\n/g, '; ')}"`);
307
- return { success: true, output: `Moved mouse cursor to (${x}, ${y})` };
486
+ const res = await this.runDriverAction('diagnostic', []);
487
+ return {
488
+ success: Boolean(res?.success),
489
+ output: `Diagnostic: Screen ${res?.width}x${res?.height}, Mouse (${res?.mouse?.x}, ${res?.mouse?.y}), Active: "${res?.activeWindow}", Windows: ${res?.windowCount}`,
490
+ action: 'diagnostic',
491
+ coordinates: res?.mouse,
492
+ activeWindow: res?.activeWindow,
493
+ details: res,
494
+ };
495
+ }
496
+ return {
497
+ success: true,
498
+ output: `Diagnostic on ${process.platform}: Screen 1920x1080, Mouse (0, 0), Active: "Desktop"`,
499
+ action: 'diagnostic',
500
+ coordinates: { x: 0, y: 0 },
501
+ activeWindow: 'Desktop',
502
+ };
503
+ }
504
+ async type(text) {
505
+ if (!text)
506
+ return { success: true, output: 'Typed empty string', action: 'type' };
507
+ if (process.platform === 'win32') {
508
+ const targetApp = this.expectedApplication || '';
509
+ if (targetApp) {
510
+ await this.switchWindow(targetApp);
308
511
  }
309
- catch (err) {
310
- return { success: false, output: `MouseMove error: ${err?.message || String(err)}` };
512
+ const res = await this.runDriverAction('type', [text]);
513
+ const success = Boolean(res?.success);
514
+ const activeWin = res?.activeWindow || targetApp || 'foreground';
515
+ this.activeWindow = activeWin;
516
+ return {
517
+ success,
518
+ output: success
519
+ ? `Typed text (${text.length} chars) into "${activeWin}"`
520
+ : `Type failed: ${res?.error || 'Unknown error'}`,
521
+ action: 'type',
522
+ error: res?.error,
523
+ recoverable: true,
524
+ activeWindow: activeWin,
525
+ expectedWindow: this.expectedApplication || undefined,
526
+ };
527
+ }
528
+ return {
529
+ success: true,
530
+ output: `Simulated type (${text.length} chars)`,
531
+ action: 'type',
532
+ activeWindow: this.expectedApplication || 'Simulated Window',
533
+ };
534
+ }
535
+ async keyPress(key) {
536
+ if (process.platform === 'win32') {
537
+ const targetApp = this.expectedApplication || '';
538
+ if (targetApp) {
539
+ await this.switchWindow(targetApp);
311
540
  }
541
+ const res = await this.runDriverAction('key', [key]);
542
+ const success = Boolean(res?.success);
543
+ return {
544
+ success,
545
+ output: success ? `Pressed key: ${key}` : `KeyPress failed: ${res?.error || 'Unknown error'}`,
546
+ action: 'key_press',
547
+ error: res?.error,
548
+ recoverable: true,
549
+ activeWindow: res?.activeWindow || targetApp || undefined,
550
+ };
312
551
  }
313
- return { success: true, output: `Simulated mouse move to (${x}, ${y})` };
552
+ return { success: true, output: `Simulated key press: ${key}`, action: 'key_press' };
314
553
  }
315
- async drag(fromX, fromY, toX, toY) {
316
- await this.mouseMove(fromX, fromY);
554
+ async hotkey(keys) {
555
+ if (!keys || keys.length === 0) {
556
+ return { success: false, output: 'No keys provided for hotkey', action: 'hotkey', error: 'No keys provided' };
557
+ }
317
558
  if (process.platform === 'win32') {
318
- try {
319
- const psScript = `
320
- Add-Type -AssemblyName System.Windows.Forms
321
- $src = @"
322
- using System;
323
- using System.Runtime.InteropServices;
324
- public class WinMouseDrag {
325
- [DllImport("user32.dll")]
326
- public static extern void mouse_event(uint dwFlags, uint dx, uint dy, uint cButtons, uint dwExtraInfo);
327
- }
328
- "@
329
- Add-Type -TypeDefinition $src -ErrorAction SilentlyContinue
330
- [WinMouseDrag]::mouse_event(0x0002, 0, 0, 0, 0)
331
- [System.Windows.Forms.Cursor]::Position = New-Object System.Drawing.Point(${Math.round(toX)}, ${Math.round(toY)})
332
- Start-Sleep -Milliseconds 100
333
- [WinMouseDrag]::mouse_event(0x0004, 0, 0, 0, 0)
334
- `.trim();
335
- await execAsync(`powershell -NoProfile -NonInteractive -Command "${psScript.replace(/\n/g, '; ')}"`);
336
- return { success: true, output: `Dragged from (${fromX}, ${fromY}) to (${toX}, ${toY})` };
559
+ const targetApp = this.expectedApplication || '';
560
+ if (targetApp) {
561
+ await this.switchWindow(targetApp);
337
562
  }
338
- catch (err) {
339
- return { success: false, output: `Drag error: ${err?.message || String(err)}` };
563
+ const res = await this.runDriverAction('hotkey', [keys.join('+')]);
564
+ const success = Boolean(res?.success);
565
+ const isClosingHotkey = keys.some((k) => {
566
+ const lk = String(k).toLowerCase().trim();
567
+ return lk === 'f4' || lk === 'w';
568
+ });
569
+ if (isClosingHotkey && success && this.expectedApplication) {
570
+ this.expectedApplication = null;
340
571
  }
572
+ return {
573
+ success,
574
+ output: success ? `Executed hotkey: ${keys.join('+')}` : `Hotkey failed: ${res?.error || 'Unknown error'}`,
575
+ action: 'hotkey',
576
+ error: res?.error,
577
+ recoverable: true,
578
+ activeWindow: res?.activeWindow || targetApp || undefined,
579
+ };
341
580
  }
342
- return { success: true, output: `Simulated drag from (${fromX}, ${fromY}) to (${toX}, ${toY})` };
581
+ return { success: true, output: `Simulated hotkey: ${keys.join('+')}`, action: 'hotkey' };
343
582
  }
344
583
  async wait(ms) {
345
584
  return new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));
@@ -349,27 +588,35 @@ Start-Sleep -Milliseconds 100
349
588
  const argsStr = args.join(' ');
350
589
  try {
351
590
  if (process.platform === 'win32') {
352
- if (appLower === 'code' || appLower === 'vscode') {
353
- await execAsync(`start code ${argsStr}`);
354
- this.expectedApplication = 'Visual Studio Code';
591
+ const driverRes = await this.runDriverAction('launch', [appName]);
592
+ if (!driverRes || !driverRes.success) {
593
+ if (appLower === 'code' || appLower === 'vscode') {
594
+ await execAsync(`start code ${argsStr}`);
595
+ }
596
+ else if (appLower === 'explorer' || appLower === 'file explorer') {
597
+ await execAsync(`start explorer ${argsStr || '.'}`);
598
+ }
599
+ else if (appLower === 'notepad') {
600
+ await execAsync(`start notepad ${argsStr}`);
601
+ }
602
+ else if (appLower === 'terminal' || appLower === 'cmd' || appLower === 'powershell') {
603
+ await execAsync(`start powershell ${argsStr}`);
604
+ }
605
+ else {
606
+ await execAsync(`start "" "${appName}" ${argsStr}`);
607
+ }
355
608
  }
356
- else if (appLower === 'explorer' || appLower === 'file explorer') {
357
- await execAsync(`start explorer ${argsStr || '.'}`);
358
- this.expectedApplication = 'File Explorer';
359
- }
360
- else if (appLower === 'notepad') {
361
- await execAsync(`start notepad ${argsStr}`);
362
- this.expectedApplication = 'Notepad';
363
- }
364
- else if (appLower === 'terminal' || appLower === 'cmd' || appLower === 'powershell') {
365
- await execAsync(`start powershell ${argsStr}`);
366
- this.expectedApplication = 'PowerShell';
367
- }
368
- else {
369
- await execAsync(`start "" "${appName}" ${argsStr}`);
370
- this.expectedApplication = appName;
609
+ this.expectedApplication = appName;
610
+ // Observation-based wait: poll inspectUI to confirm window appeared
611
+ const delays = [400, 800, 1200];
612
+ for (const delay of delays) {
613
+ await this.wait(delay);
614
+ const ui = await this.inspectUI();
615
+ const match = ui.windows.some(w => w.toLowerCase().includes(appName.toLowerCase()));
616
+ if (match || (ui.activeWindow && ui.activeWindow.toLowerCase().includes(appName.toLowerCase()))) {
617
+ break;
618
+ }
371
619
  }
372
- await this.wait(350);
373
620
  if (this.expectedApplication) {
374
621
  await this.switchWindow(this.expectedApplication);
375
622
  }
@@ -411,7 +658,10 @@ Start-Sleep -Milliseconds 100
411
658
  try {
412
659
  if (process.platform === 'win32') {
413
660
  const cleanName = appName.replace(/\.exe$/i, '');
414
- await execAsync(`powershell -NoProfile -Command "Stop-Process -Name '${cleanName}' -Force -ErrorAction SilentlyContinue"`);
661
+ const driverRes = await this.runDriverAction('close_window', [appName]);
662
+ if (!driverRes || !driverRes.success) {
663
+ await execAsync(`powershell -NoProfile -Command "Stop-Process -Name '${cleanName}' -Force -ErrorAction SilentlyContinue"`);
664
+ }
415
665
  }
416
666
  else {
417
667
  await execAsync(`pkill -f "${appName}"`);
@@ -432,8 +682,21 @@ Start-Sleep -Milliseconds 100
432
682
  }
433
683
  async switchWindow(windowTitleOrApp) {
434
684
  if (process.platform === 'win32') {
685
+ const res = await this.runDriverAction('focus_window', [windowTitleOrApp]);
686
+ if (res && res.success) {
687
+ this.activeWindow = res.activeWindow || windowTitleOrApp;
688
+ this.expectedApplication = windowTitleOrApp;
689
+ return {
690
+ success: true,
691
+ output: `Focused window matching: "${windowTitleOrApp}"`,
692
+ action: 'switch_window',
693
+ activeWindow: this.activeWindow || undefined,
694
+ expectedWindow: windowTitleOrApp,
695
+ };
696
+ }
697
+ // Fallback to WScript.Shell AppActivate
435
698
  try {
436
- const psScript = `
699
+ const script = `
437
700
  $wshell = New-Object -ComObject Wscript.Shell
438
701
  $result = $wshell.AppActivate("${windowTitleOrApp.replace(/"/g, '`"')}")
439
702
  if (-not $result) {
@@ -443,13 +706,13 @@ if (-not $result) {
443
706
  $result = $true
444
707
  }
445
708
  }
446
- Start-Sleep -Milliseconds 150
447
709
  if ($result) { Write-Output "OK" } else { Write-Output "NOT_FOUND" }
448
- `.trim();
449
- const stdout = await this.runPowerShell(psScript);
450
- this.activeWindow = windowTitleOrApp;
451
- this.expectedApplication = windowTitleOrApp;
710
+ `;
711
+ const encoded = Buffer.from(script, 'utf16le').toString('base64');
712
+ const { stdout } = await execAsync(`powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -EncodedCommand ${encoded}`);
452
713
  if (stdout.includes('OK')) {
714
+ this.activeWindow = windowTitleOrApp;
715
+ this.expectedApplication = windowTitleOrApp;
453
716
  return {
454
717
  success: true,
455
718
  output: `Focused window matching: "${windowTitleOrApp}"`,
@@ -458,33 +721,38 @@ if ($result) { Write-Output "OK" } else { Write-Output "NOT_FOUND" }
458
721
  expectedWindow: windowTitleOrApp,
459
722
  };
460
723
  }
461
- return {
462
- success: false,
463
- output: `Could not find or focus window: "${windowTitleOrApp}". Try computer_inspect_ui to list open windows.`,
464
- action: 'switch_window',
465
- recoverable: true,
466
- };
467
- }
468
- catch (err) {
469
- return {
470
- success: false,
471
- output: `Window switch error for "${windowTitleOrApp}": ${err?.message || String(err)}`,
472
- action: 'switch_window',
473
- error: err?.message || String(err),
474
- recoverable: true,
475
- };
476
724
  }
725
+ catch { }
726
+ return {
727
+ success: false,
728
+ output: `Could not find or focus window: "${windowTitleOrApp}". Try computer_inspect_ui to list open windows.`,
729
+ action: 'switch_window',
730
+ recoverable: true,
731
+ };
477
732
  }
478
733
  this.activeWindow = windowTitleOrApp;
479
734
  return { success: true, output: `Switched window to: "${windowTitleOrApp}"`, action: 'switch_window' };
480
735
  }
736
+ async getWindowBounds(windowTitleOrApp) {
737
+ if (process.platform === 'win32') {
738
+ const res = await this.runDriverAction('window_bounds', [windowTitleOrApp]);
739
+ if (res && res.success && typeof res.x === 'number') {
740
+ return { x: res.x, y: res.y, width: res.width, height: res.height };
741
+ }
742
+ }
743
+ return null;
744
+ }
481
745
  async inspectUI() {
482
746
  if (process.platform === 'win32') {
747
+ const res = await this.runDriverAction('list_windows', []);
748
+ if (res && res.success && Array.isArray(res.windows)) {
749
+ return {
750
+ activeWindow: res.activeWindow || 'Desktop',
751
+ windows: res.windows,
752
+ };
753
+ }
483
754
  try {
484
- const psScript = `
485
- Get-Process | Where-Object { $_.MainWindowTitle -and $_.MainWindowTitle.Trim().Length -gt 0 } | Select-Object -ExpandProperty MainWindowTitle
486
- `.trim();
487
- const { stdout } = await execAsync(`powershell -NoProfile -NonInteractive -Command "${psScript.replace(/\n/g, '; ')}"`);
755
+ const { stdout } = await execAsync(`powershell -NoProfile -NonInteractive -Command "Get-Process | Where-Object { $_.MainWindowTitle -and $_.MainWindowTitle.Trim().Length -gt 0 } | Select-Object -ExpandProperty MainWindowTitle"`);
488
756
  const titles = stdout
489
757
  .split('\n')
490
758
  .map((t) => t.trim())