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.
- package/.firebase/hosting.d2Vi.cache +8 -7
- package/AUTONOMOUS_FEATURES.md +192 -0
- package/README.md +67 -35
- package/bin/src/commands/agent.d.ts.map +1 -1
- package/bin/src/commands/agent.js +21 -8
- package/bin/src/commands/agent.js.map +1 -1
- package/bin/src/commands/login.d.ts.map +1 -1
- package/bin/src/commands/login.js +3 -1
- package/bin/src/commands/login.js.map +1 -1
- package/bin/src/engine/actionVerifier.d.ts.map +1 -1
- package/bin/src/engine/actionVerifier.js +9 -1
- package/bin/src/engine/actionVerifier.js.map +1 -1
- package/bin/src/engine/agentEngine.d.ts.map +1 -1
- package/bin/src/engine/agentEngine.js +319 -13
- package/bin/src/engine/agentEngine.js.map +1 -1
- package/bin/src/engine/browser/browserController.d.ts.map +1 -1
- package/bin/src/engine/browser/browserController.js +53 -13
- package/bin/src/engine/browser/browserController.js.map +1 -1
- package/bin/src/engine/computer/computerController.d.ts.map +1 -1
- package/bin/src/engine/computer/computerController.js +564 -296
- package/bin/src/engine/computer/computerController.js.map +1 -1
- package/bin/src/engine/computer/computerDiagnostic.d.ts.map +1 -0
- package/bin/src/engine/computer/computerDiagnostic.js +71 -0
- package/bin/src/engine/computer/computerDiagnostic.js.map +1 -0
- package/bin/src/engine/computer/index.d.ts.map +1 -1
- package/bin/src/engine/computer/index.js +1 -0
- package/bin/src/engine/computer/index.js.map +1 -1
- package/bin/src/engine/computer/mockComputer.d.ts.map +1 -1
- package/bin/src/engine/computer/mockComputer.js +104 -8
- package/bin/src/engine/computer/mockComputer.js.map +1 -1
- package/bin/src/engine/computer/scoutDriver.cs +1161 -0
- package/bin/src/engine/computer/scout_driver.exe +0 -0
- package/bin/src/engine/runtime/agentRuntime.d.ts.map +1 -1
- package/bin/src/engine/runtime/agentRuntime.js +375 -20
- package/bin/src/engine/runtime/agentRuntime.js.map +1 -1
- package/bin/src/engine/safety.d.ts.map +1 -1
- package/bin/src/engine/safety.js +17 -6
- package/bin/src/engine/safety.js.map +1 -1
- package/bin/src/engine/toolRegistry.d.ts.map +1 -1
- package/bin/src/engine/toolRegistry.js +209 -0
- package/bin/src/engine/toolRegistry.js.map +1 -1
- package/bin/src/engine/workspace/agentWorkspace.d.ts.map +1 -1
- package/bin/src/engine/workspace/agentWorkspace.js +21 -0
- package/bin/src/engine/workspace/agentWorkspace.js.map +1 -1
- package/bin/src/index.js +5 -4
- package/bin/src/index.js.map +1 -1
- package/bin/src/utils/auth.d.ts.map +1 -1
- package/bin/src/utils/auth.js +14 -1
- package/bin/src/utils/auth.js.map +1 -1
- package/bin/src/utils/branding.js +1 -1
- package/bin/src/utils/holoseneAuth.d.ts.map +1 -1
- package/bin/src/utils/holoseneAuth.js +169 -41
- package/bin/src/utils/holoseneAuth.js.map +1 -1
- package/firebase.json +8 -0
- package/package.json +1 -1
- package/web/app.js +3 -3
- 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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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:
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
|
128
|
-
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
|
175
|
-
action: '
|
|
176
|
-
|
|
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
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
|
|
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
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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 {
|
|
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
|
|
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
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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 {
|
|
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
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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 {
|
|
482
|
+
return { x: 0, y: 0 };
|
|
298
483
|
}
|
|
299
|
-
async
|
|
484
|
+
async runDiagnostic() {
|
|
300
485
|
if (process.platform === 'win32') {
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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
|
-
|
|
310
|
-
|
|
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
|
|
552
|
+
return { success: true, output: `Simulated key press: ${key}`, action: 'key_press' };
|
|
314
553
|
}
|
|
315
|
-
async
|
|
316
|
-
|
|
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
|
-
|
|
319
|
-
|
|
320
|
-
|
|
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
|
-
|
|
339
|
-
|
|
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
|
|
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
|
-
|
|
353
|
-
|
|
354
|
-
|
|
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
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
await
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
449
|
-
const
|
|
450
|
-
|
|
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
|
|
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())
|