ft-scout 9.0.4 → 9.0.5

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