ft-scout 9.0.3 → 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 (84) hide show
  1. package/.firebase/hosting.d2Vi.cache +7 -7
  2. package/README.md +76 -83
  3. package/bin/src/commands/agent.d.ts.map +1 -1
  4. package/bin/src/commands/agent.js +31 -228
  5. package/bin/src/commands/agent.js.map +1 -1
  6. package/bin/src/commands/plan.d.ts.map +1 -1
  7. package/bin/src/commands/plan.js +45 -18
  8. package/bin/src/commands/plan.js.map +1 -1
  9. package/bin/src/engine/actionVerifier.d.ts.map +1 -1
  10. package/bin/src/engine/actionVerifier.js +61 -9
  11. package/bin/src/engine/actionVerifier.js.map +1 -1
  12. package/bin/src/engine/agentEngine.d.ts.map +1 -1
  13. package/bin/src/engine/agentEngine.js +701 -553
  14. package/bin/src/engine/agentEngine.js.map +1 -1
  15. package/bin/src/engine/agentState.d.ts.map +1 -1
  16. package/bin/src/engine/agentState.js +0 -64
  17. package/bin/src/engine/agentState.js.map +1 -1
  18. package/bin/src/engine/browser/browserController.d.ts.map +1 -1
  19. package/bin/src/engine/browser/browserController.js +8 -0
  20. package/bin/src/engine/browser/browserController.js.map +1 -1
  21. package/bin/src/engine/computer/computerController.d.ts.map +1 -1
  22. package/bin/src/engine/computer/computerController.js +686 -240
  23. package/bin/src/engine/computer/computerController.js.map +1 -1
  24. package/bin/src/engine/computer/computerDiagnostic.d.ts.map +1 -0
  25. package/bin/src/engine/computer/computerDiagnostic.js +71 -0
  26. package/bin/src/engine/computer/computerDiagnostic.js.map +1 -0
  27. package/bin/src/engine/computer/index.d.ts.map +1 -1
  28. package/bin/src/engine/computer/index.js +2 -0
  29. package/bin/src/engine/computer/index.js.map +1 -1
  30. package/bin/src/engine/computer/mockComputer.d.ts.map +1 -0
  31. package/bin/src/engine/computer/mockComputer.js +260 -0
  32. package/bin/src/engine/computer/mockComputer.js.map +1 -0
  33. package/bin/src/engine/computer/scoutDriver.cs +873 -0
  34. package/bin/src/engine/computer/scout_driver.exe +0 -0
  35. package/bin/src/engine/runtime/agentRuntime.d.ts.map +1 -0
  36. package/bin/src/engine/runtime/agentRuntime.js +1160 -0
  37. package/bin/src/engine/runtime/agentRuntime.js.map +1 -0
  38. package/bin/src/engine/runtime/index.d.ts.map +1 -0
  39. package/bin/src/engine/runtime/index.js +2 -0
  40. package/bin/src/engine/runtime/index.js.map +1 -0
  41. package/bin/src/engine/safety.d.ts.map +1 -1
  42. package/bin/src/engine/safety.js +59 -25
  43. package/bin/src/engine/safety.js.map +1 -1
  44. package/bin/src/engine/toolRegistry.d.ts.map +1 -1
  45. package/bin/src/engine/toolRegistry.js +293 -68
  46. package/bin/src/engine/toolRegistry.js.map +1 -1
  47. package/bin/src/engine/voiceEngine.d.ts.map +1 -1
  48. package/bin/src/engine/voiceEngine.js +0 -1
  49. package/bin/src/engine/voiceEngine.js.map +1 -1
  50. package/bin/src/engine/workspace/agentWorkspace.d.ts.map +1 -0
  51. package/bin/src/engine/workspace/agentWorkspace.js +505 -0
  52. package/bin/src/engine/workspace/agentWorkspace.js.map +1 -0
  53. package/bin/src/engine/workspace/index.d.ts.map +1 -0
  54. package/bin/src/engine/workspace/index.js +2 -0
  55. package/bin/src/engine/workspace/index.js.map +1 -0
  56. package/bin/src/index.js +4 -5
  57. package/bin/src/index.js.map +1 -1
  58. package/bin/src/server/server.d.ts.map +1 -1
  59. package/bin/src/server/server.js +25 -4
  60. package/bin/src/server/server.js.map +1 -1
  61. package/bin/src/utils/auth.d.ts.map +1 -1
  62. package/bin/src/utils/auth.js +81 -50
  63. package/bin/src/utils/auth.js.map +1 -1
  64. package/bin/src/utils/branding.js +1 -1
  65. package/bin/src/utils/payment.d.ts.map +1 -1
  66. package/bin/src/utils/payment.js +15 -1
  67. package/bin/src/utils/payment.js.map +1 -1
  68. package/bin/src/utils/pricing.js +1 -1
  69. package/bin/src/utils/pricing.js.map +1 -1
  70. package/bin/src/utils/razorpay.d.ts.map +1 -1
  71. package/bin/src/utils/razorpay.js +10 -2
  72. package/bin/src/utils/razorpay.js.map +1 -1
  73. package/downloads/invoice-2026-03.pdf +1 -0
  74. package/package.json +1 -1
  75. package/web/app.js +54 -135
  76. package/web/styles.css +2 -2
  77. package/bin/src/engine/appControl.d.ts.map +0 -1
  78. package/bin/src/engine/appControl.js +0 -2413
  79. package/bin/src/engine/appControl.js.map +0 -1
  80. package/bin/src/engine/liveScreenEngine.d.ts.map +0 -1
  81. package/bin/src/engine/liveScreenEngine.js +0 -421
  82. package/bin/src/engine/liveScreenEngine.js.map +0 -1
  83. package/bin/src/engine/scripts/winCapture.ps1 +0 -248
  84. package/downloads/INVOICE_2026_ACME.pdf +0 -1
@@ -1,295 +1,741 @@
1
- import { openApp, executeInApp, takeoverControl, releaseTakeover, captureScreen, analyzeScreen, getScreenInfo, } from '../appControl.js';
2
- /**
3
- * OSComputerController: Production implementation driving host desktop OS
4
- * (Windows, macOS, Linux) through Scout's platform automation engine.
5
- */
6
- export class OSComputerController {
7
- async screenshot() {
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { exec, execFile, spawn } from 'child_process';
4
+ import { promisify } from 'util';
5
+ import { fileURLToPath } from 'url';
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = path.dirname(__filename);
8
+ const execAsync = promisify(exec);
9
+ const execFileAsync = promisify(execFile);
10
+ export class SystemComputerController {
11
+ workspaceDir;
12
+ screenshotsDir;
13
+ expectedApplication = null;
14
+ activeWindow = null;
15
+ driverExePath = null;
16
+ driverInitAttempted = false;
17
+ constructor(workspaceDir = process.cwd()) {
18
+ this.workspaceDir = workspaceDir;
19
+ this.screenshotsDir = path.join(this.workspaceDir, '.ft', 'screenshots');
8
20
  try {
9
- const shotPath = await captureScreen();
10
- return {
11
- success: true,
12
- output: `Captured screen to ${shotPath}`,
13
- details: { path: shotPath },
14
- };
15
- }
16
- catch (err) {
17
- return {
18
- success: false,
19
- output: `Failed to capture screen: ${err.message}`,
20
- };
21
+ if (!fs.existsSync(this.screenshotsDir)) {
22
+ fs.mkdirSync(this.screenshotsDir, { recursive: true });
23
+ }
21
24
  }
25
+ catch { }
22
26
  }
23
- async click(x, y, button = 'left') {
24
- const res = await executeInApp('desktop', 'click_app', { x, y, button });
25
- return {
26
- success: res.success,
27
- output: res.output,
28
- details: res.details,
29
- };
30
- }
31
- async double_click(x, y) {
32
- const res = await executeInApp('desktop', 'click_app', { x, y, double: true });
33
- return {
34
- success: res.success,
35
- output: res.output,
36
- details: res.details,
37
- };
38
- }
39
- async type(text) {
40
- const res = await executeInApp('desktop', 'type_text', { text });
41
- return {
42
- success: res.success,
43
- output: res.output,
44
- details: res.details,
45
- };
46
- }
47
- async key_press(key) {
48
- const res = await executeInApp('desktop', 'send_keys', { key });
49
- return {
50
- success: res.success,
51
- output: res.output,
52
- details: res.details,
53
- };
54
- }
55
- async hotkey(keys) {
56
- const keyCombo = Array.isArray(keys) ? keys.join('+') : keys;
57
- const res = await executeInApp('desktop', 'key_combo', { keyCombo });
58
- return {
59
- success: res.success,
60
- output: res.output,
61
- details: res.details,
62
- };
27
+ setExpectedApplication(app) {
28
+ this.expectedApplication = app;
63
29
  }
64
- async scroll(direction, amount = 4) {
65
- const res = await executeInApp('desktop', 'scroll', { direction, amount });
66
- return {
67
- success: res.success,
68
- output: res.output,
69
- details: res.details,
70
- };
30
+ getExpectedApplication() {
31
+ return this.expectedApplication;
71
32
  }
72
- async move_mouse(x, y) {
73
- const res = await executeInApp('desktop', 'move_mouse', { x, y });
74
- return {
75
- success: res.success,
76
- output: res.output,
77
- details: res.details,
78
- };
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 {}
79
163
  }
80
- async drag(start, end) {
81
- const res = await executeInApp('desktop', 'drag', { startX: start.x, startY: start.y, endX: end.x, endY: end.y });
82
- return {
83
- success: res.success,
84
- output: res.output,
85
- details: res.details,
86
- };
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
+ }
87
236
  }
88
- async wait(durationMs) {
89
- await new Promise((r) => setTimeout(r, Math.min(10000, durationMs)));
237
+ async screenshot() {
238
+ const timestamp = new Date().toISOString();
239
+ const filename = `screen_${Date.now()}.png`;
240
+ const filePath = path.join(this.screenshotsDir, filename);
241
+ if (process.platform === 'win32') {
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;
246
+ return {
247
+ path: filePath,
248
+ width: Number(res.width) || 1920,
249
+ height: Number(res.height) || 1080,
250
+ activeWindow: activeWin,
251
+ timestamp,
252
+ };
253
+ }
254
+ }
255
+ else if (process.platform === 'darwin') {
256
+ try {
257
+ await execAsync(`screencapture -x "${filePath}"`);
258
+ return { path: filePath, width: 1920, height: 1080, activeWindow: 'Desktop', timestamp };
259
+ }
260
+ catch {
261
+ return { width: 1920, height: 1080, timestamp };
262
+ }
263
+ }
264
+ else {
265
+ try {
266
+ await execAsync(`scrot "${filePath}"`);
267
+ return { path: filePath, width: 1920, height: 1080, activeWindow: 'Desktop', timestamp };
268
+ }
269
+ catch {
270
+ return { width: 1920, height: 1080, timestamp };
271
+ }
272
+ }
273
+ return { width: 1920, height: 1080, activeWindow: 'Desktop', timestamp };
274
+ }
275
+ async click(x, y, options) {
276
+ const btn = options?.button || 'left';
277
+ const count = options?.clicks || 1;
278
+ if (process.platform === 'win32') {
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
+ };
296
+ }
90
297
  return {
91
298
  success: true,
92
- output: `Waited for ${durationMs}ms`,
93
- details: { durationMs },
94
- };
95
- }
96
- async open_application(name, target) {
97
- const res = await openApp(name, target);
98
- return {
99
- success: res.success,
100
- output: res.output,
101
- details: res.details,
102
- };
103
- }
104
- async close_application(name) {
105
- const res = await executeInApp(name, 'close_app');
106
- return {
107
- success: res.success,
108
- output: res.output,
109
- details: res.details,
299
+ output: `Simulated click ${btn} at (${x}, ${y})`,
300
+ action: 'click',
301
+ coordinates: { x, y },
302
+ button: btn,
110
303
  };
111
304
  }
112
- async switch_application(name) {
113
- const res = await executeInApp('desktop', 'focus_window', { app: name });
114
- return {
115
- success: res.success,
116
- output: res.output,
117
- details: res.details,
118
- };
305
+ async leftClick(x, y) {
306
+ return this.click(x, y, { button: 'left', clicks: 1 });
119
307
  }
120
- async inspect_page() {
121
- const res = await executeInApp('desktop', 'analyze_screen');
122
- return {
123
- success: res.success,
124
- output: res.output,
125
- details: res.details,
126
- };
308
+ async rightClick(x, y) {
309
+ return this.click(x, y, { button: 'right', clicks: 1 });
127
310
  }
128
- async get_active_window() {
129
- const res = await getScreenInfo();
311
+ async doubleClick(x, y) {
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
+ }
130
329
  return {
131
- success: res.success,
132
- output: res.output,
133
- details: res.details,
330
+ success: true,
331
+ output: `Simulated double-click at (${x}, ${y})`,
332
+ action: 'double_click',
333
+ coordinates: { x, y },
334
+ button: 'left',
134
335
  };
135
336
  }
136
- }
137
- /**
138
- * MockComputerController: Fast, deterministic in-memory computer controller
139
- * for automated unit tests, CI pipelines, and desktop simulations.
140
- */
141
- export class MockComputerController {
142
- cursor = { x: 0, y: 0 };
143
- activeApp = null;
144
- openApps = new Set();
145
- typedHistory = [];
146
- actionLog = [];
147
- async screenshot() {
148
- this.actionLog.push({ action: 'screenshot', params: {} });
337
+ async mouseDown(x, y, button) {
338
+ const btn = button || 'left';
339
+ if (process.platform === 'win32') {
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
+ };
351
+ }
149
352
  return {
150
353
  success: true,
151
- output: 'Mock screenshot captured: 1920x1080 virtual desktop.',
152
- details: { width: 1920, height: 1080, format: 'png' },
354
+ output: `Simulated mouse down (${btn})`,
355
+ action: 'mouse_down',
356
+ button: btn,
357
+ coordinates: x !== undefined && y !== undefined ? { x, y } : undefined,
153
358
  };
154
359
  }
155
- async click(x, y, button) {
156
- const btn = typeof button === 'object' ? (button.button || 'left') : (button || 'left');
157
- this.cursor = { x, y };
158
- this.actionLog.push({ action: 'click', params: { x, y, button: btn } });
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
+ }
159
375
  return {
160
376
  success: true,
161
- output: `Mock clicked ${btn} button at (${x}, ${y}).`,
162
- details: { x, y, button: btn },
377
+ output: `Simulated mouse up (${btn})`,
378
+ action: 'mouse_up',
379
+ button: btn,
380
+ coordinates: x !== undefined && y !== undefined ? { x, y } : undefined,
163
381
  };
164
382
  }
165
- async getWindowList() {
166
- return Array.from(this.openApps).length > 0 ? Array.from(this.openApps) : ['Code', 'Chrome', 'Terminal'];
167
- }
168
- async typeText(text) {
169
- return this.type(text);
170
- }
171
- async takeScreenshot() {
172
- return {
173
- format: 'base64',
174
- width: 1920,
175
- height: 1080,
176
- data: 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==',
177
- };
383
+ async mouseMove(x, y) {
384
+ return this.move(x, y);
178
385
  }
179
- async double_click(x, y) {
180
- this.cursor = { x, y };
181
- this.actionLog.push({ action: 'double_click', params: { x, y } });
386
+ async move(x, y) {
387
+ if (process.platform === 'win32') {
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
+ };
402
+ }
182
403
  return {
183
404
  success: true,
184
- output: `Mock double-clicked at (${x}, ${y}).`,
185
- details: { x, y },
405
+ output: `Simulated mouse move to (${x}, ${y})`,
406
+ action: 'move',
407
+ coordinates: { x, y },
186
408
  };
187
409
  }
188
- async type(text) {
189
- this.typedHistory.push(text);
190
- this.actionLog.push({ action: 'type', params: { text } });
410
+ async drag(startX, startY, endX, endY) {
411
+ if (process.platform === 'win32') {
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
+ };
426
+ }
191
427
  return {
192
428
  success: true,
193
- output: `Mock typed "${text}" into active application ${this.activeApp || 'desktop'}.`,
194
- details: { text, activeApp: this.activeApp },
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 },
195
434
  };
196
435
  }
197
- async key_press(key) {
198
- this.actionLog.push({ action: 'key_press', params: { key } });
436
+ async scroll(deltaX, deltaY) {
437
+ if (process.platform === 'win32') {
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
+ };
445
+ }
199
446
  return {
200
447
  success: true,
201
- output: `Mock pressed key "${key}".`,
202
- details: { key },
448
+ output: `Simulated scroll deltaY=${deltaY}, deltaX=${deltaX}`,
449
+ action: 'scroll',
450
+ details: { deltaX, deltaY },
203
451
  };
204
452
  }
205
- async hotkey(keys) {
206
- const combo = Array.isArray(keys) ? keys.join('+') : keys;
207
- this.actionLog.push({ action: 'hotkey', params: { combo } });
208
- return {
209
- success: true,
210
- output: `Mock pressed hotkey combo "${combo}".`,
211
- details: { combo },
212
- };
453
+ async getMousePosition() {
454
+ if (process.platform === 'win32') {
455
+ const res = await this.runDriverAction('position', []);
456
+ if (res && res.coordinates) {
457
+ return res.coordinates;
458
+ }
459
+ }
460
+ return { x: 0, y: 0 };
213
461
  }
214
- async scroll(direction, amount = 4) {
215
- this.actionLog.push({ action: 'scroll', params: { direction, amount } });
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
+ }
216
474
  return {
217
475
  success: true,
218
- output: `Mock scrolled ${direction} by ${amount} units.`,
219
- details: { direction, amount },
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',
220
480
  };
221
481
  }
222
- async move_mouse(x, y) {
223
- this.cursor = { x, y };
224
- this.actionLog.push({ action: 'move_mouse', params: { x, y } });
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);
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
+ };
505
+ }
225
506
  return {
226
507
  success: true,
227
- output: `Mock moved cursor to (${x}, ${y}).`,
228
- details: { x, y },
508
+ output: `Simulated type (${text.length} chars)`,
509
+ action: 'type',
510
+ activeWindow: this.expectedApplication || 'Simulated Window',
229
511
  };
230
512
  }
231
- async drag(start, end) {
232
- this.cursor = { ...end };
233
- this.actionLog.push({ action: 'drag', params: { start, end } });
234
- return {
235
- success: true,
236
- output: `Mock dragged from (${start.x}, ${start.y}) to (${end.x}, ${end.y}).`,
237
- details: { start, end },
238
- };
513
+ async keyPress(key) {
514
+ if (process.platform === 'win32') {
515
+ const targetApp = this.expectedApplication || '';
516
+ if (targetApp) {
517
+ await this.switchWindow(targetApp);
518
+ }
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' };
239
531
  }
240
- async wait(durationMs) {
241
- this.actionLog.push({ action: 'wait', params: { durationMs } });
242
- return {
243
- success: true,
244
- output: `Mock waited ${durationMs}ms.`,
245
- details: { durationMs },
246
- };
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);
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
+ };
551
+ }
552
+ return { success: true, output: `Simulated hotkey: ${keys.join('+')}`, action: 'hotkey' };
247
553
  }
248
- async open_application(name, target) {
249
- this.openApps.add(name);
250
- this.activeApp = name;
251
- this.actionLog.push({ action: 'open_application', params: { name, target } });
252
- return {
253
- success: true,
254
- output: `Mock opened application "${name}" with target "${target || 'default'}".`,
255
- details: { name, target, active: true },
256
- };
554
+ async wait(ms) {
555
+ return new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));
257
556
  }
258
- async close_application(name) {
259
- this.openApps.delete(name);
260
- if (this.activeApp === name)
261
- this.activeApp = null;
262
- this.actionLog.push({ action: 'close_application', params: { name } });
263
- return {
264
- success: true,
265
- output: `Mock closed application "${name}".`,
266
- details: { name },
267
- };
557
+ async openApplication(appName, args = []) {
558
+ const appLower = appName.toLowerCase().trim();
559
+ const argsStr = args.join(' ');
560
+ try {
561
+ if (process.platform === 'win32') {
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
+ }
579
+ }
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
+ }
590
+ }
591
+ if (this.expectedApplication) {
592
+ await this.switchWindow(this.expectedApplication);
593
+ }
594
+ return {
595
+ success: true,
596
+ output: `Launched and focused application: "${this.expectedApplication || appName}"`,
597
+ action: 'open_app',
598
+ activeWindow: this.expectedApplication || appName,
599
+ expectedWindow: this.expectedApplication || appName,
600
+ };
601
+ }
602
+ else if (process.platform === 'darwin') {
603
+ await execAsync(`open -a "${appName}" ${argsStr}`);
604
+ this.expectedApplication = appName;
605
+ }
606
+ else {
607
+ await execAsync(`xdg-open "${appName}" ${argsStr} &`);
608
+ this.expectedApplication = appName;
609
+ }
610
+ return {
611
+ success: true,
612
+ output: `Launched application: "${appName}"`,
613
+ action: 'open_app',
614
+ activeWindow: appName,
615
+ expectedWindow: appName,
616
+ };
617
+ }
618
+ catch (err) {
619
+ return {
620
+ success: false,
621
+ output: `Failed to launch "${appName}": ${err?.message || String(err)}`,
622
+ action: 'open_app',
623
+ error: err?.message || String(err),
624
+ recoverable: true,
625
+ };
626
+ }
268
627
  }
269
- async switch_application(name) {
270
- this.openApps.add(name);
271
- this.activeApp = name;
272
- this.actionLog.push({ action: 'switch_application', params: { name } });
273
- return {
274
- success: true,
275
- output: `Mock focused application "${name}".`,
276
- details: { name },
277
- };
628
+ async closeApplication(appName) {
629
+ try {
630
+ if (process.platform === 'win32') {
631
+ const cleanName = appName.replace(/\.exe$/i, '');
632
+ await execAsync(`powershell -NoProfile -Command "Stop-Process -Name '${cleanName}' -Force -ErrorAction SilentlyContinue"`);
633
+ }
634
+ else {
635
+ await execAsync(`pkill -f "${appName}"`);
636
+ }
637
+ if (this.expectedApplication && this.expectedApplication.toLowerCase().includes(appName.toLowerCase())) {
638
+ this.expectedApplication = null;
639
+ }
640
+ return { success: true, output: `Closed application: "${appName}"`, action: 'close_app' };
641
+ }
642
+ catch (err) {
643
+ return {
644
+ success: false,
645
+ output: `Failed to close "${appName}": ${err?.message || String(err)}`,
646
+ action: 'close_app',
647
+ error: err?.message || String(err),
648
+ };
649
+ }
278
650
  }
279
- async inspect_page() {
280
- this.actionLog.push({ action: 'inspect_page', params: {} });
281
- return {
282
- success: true,
283
- output: `Mock visual inspection of active app "${this.activeApp || 'desktop'}": 3 windows, 12 controls detected.`,
284
- details: { activeApp: this.activeApp, controlsCount: 12 },
285
- };
651
+ async switchWindow(windowTitleOrApp) {
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
666
+ try {
667
+ const script = `
668
+ $wshell = New-Object -ComObject Wscript.Shell
669
+ $result = $wshell.AppActivate("${windowTitleOrApp.replace(/"/g, '`"')}")
670
+ if (-not $result) {
671
+ $p = Get-Process | Where-Object { $_.MainWindowTitle -like "*${windowTitleOrApp.replace(/"/g, '`"')}*" -or $_.ProcessName -like "*${windowTitleOrApp.replace(/"/g, '`"')}*" } | Select-Object -First 1
672
+ if ($p) {
673
+ $null = $wshell.AppActivate($p.Id)
674
+ $result = $true
286
675
  }
287
- async get_active_window() {
288
- return {
289
- success: true,
290
- output: `Active window: "${this.activeApp || 'Desktop'}"`,
291
- details: { activeApp: this.activeApp },
292
- };
676
+ }
677
+ if ($result) { Write-Output "OK" } else { Write-Output "NOT_FOUND" }
678
+ `;
679
+ const encoded = Buffer.from(script, 'utf16le').toString('base64');
680
+ const { stdout } = await execAsync(`powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -EncodedCommand ${encoded}`);
681
+ if (stdout.includes('OK')) {
682
+ this.activeWindow = windowTitleOrApp;
683
+ this.expectedApplication = windowTitleOrApp;
684
+ return {
685
+ success: true,
686
+ output: `Focused window matching: "${windowTitleOrApp}"`,
687
+ action: 'switch_window',
688
+ activeWindow: windowTitleOrApp,
689
+ expectedWindow: windowTitleOrApp,
690
+ };
691
+ }
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
+ };
700
+ }
701
+ this.activeWindow = windowTitleOrApp;
702
+ return { success: true, output: `Switched window to: "${windowTitleOrApp}"`, action: 'switch_window' };
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
+ }
713
+ async inspectUI() {
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
+ }
722
+ try {
723
+ const { stdout } = await execAsync(`powershell -NoProfile -NonInteractive -Command "Get-Process | Where-Object { $_.MainWindowTitle -and $_.MainWindowTitle.Trim().Length -gt 0 } | Select-Object -ExpandProperty MainWindowTitle"`);
724
+ const titles = stdout
725
+ .split('\n')
726
+ .map((t) => t.trim())
727
+ .filter((t) => t.length > 0);
728
+ const active = titles.length > 0 ? titles[0] : 'Desktop';
729
+ return {
730
+ activeWindow: active,
731
+ windows: titles,
732
+ };
733
+ }
734
+ catch {
735
+ return { activeWindow: 'Desktop', windows: ['Desktop'] };
736
+ }
737
+ }
738
+ return { activeWindow: 'Desktop', windows: ['Desktop', 'Terminal'] };
293
739
  }
294
740
  }
295
741
  //# sourceMappingURL=computerController.js.map