ft-scout 8.0.1 → 8.0.3
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/README.md +39 -4
- package/bin/src/commands/agent.d.ts.map +1 -1
- package/bin/src/commands/agent.js +117 -16
- package/bin/src/commands/agent.js.map +1 -1
- package/bin/src/engine/agentEngine.d.ts.map +1 -1
- package/bin/src/engine/agentEngine.js +360 -178
- package/bin/src/engine/agentEngine.js.map +1 -1
- package/bin/src/engine/appControl.d.ts.map +1 -1
- package/bin/src/engine/appControl.js +1151 -133
- package/bin/src/engine/appControl.js.map +1 -1
- package/bin/src/engine/llm.d.ts.map +1 -1
- package/bin/src/engine/llm.js +4 -3
- package/bin/src/engine/llm.js.map +1 -1
- package/bin/src/engine/scripts/winCapture.ps1 +248 -0
- package/bin/src/index.js +4 -4
- package/bin/src/utils/branding.js +1 -1
- package/package.json +2 -2
- package/web/app.js +2 -2
- package/web/styles.css +1 -1
|
@@ -4,6 +4,10 @@ import http from 'http';
|
|
|
4
4
|
import https from 'https';
|
|
5
5
|
import { exec } from 'child_process';
|
|
6
6
|
import { promisify } from 'util';
|
|
7
|
+
import OpenAI from 'openai';
|
|
8
|
+
import dotenv from 'dotenv';
|
|
9
|
+
import chalk from 'chalk';
|
|
10
|
+
dotenv.config({ quiet: true });
|
|
7
11
|
const execAsync = promisify(exec);
|
|
8
12
|
/**
|
|
9
13
|
* Standardize app name aliases across platforms.
|
|
@@ -19,6 +23,9 @@ export function normalizeAppName(rawAppName) {
|
|
|
19
23
|
if (['code', 'vscode', 'visual-studio-code', 'cursor', 'zed', 'sublime', 'notepad', 'notepad++', 'vim', 'nvim', 'nano', 'editor'].includes(appLower)) {
|
|
20
24
|
return { category: 'editor', name: appLower || 'editor' };
|
|
21
25
|
}
|
|
26
|
+
if (['game', 'games', 'gaming', 'play', 'launcher', 'steam', 'epic', 'epicgames', 'riot', 'battlenet', 'ea', 'ubisoft', 'gog'].includes(appLower) || appLower.startsWith('game:')) {
|
|
27
|
+
return { category: 'game', name: appLower };
|
|
28
|
+
}
|
|
22
29
|
return { category: 'system', name: appLower };
|
|
23
30
|
}
|
|
24
31
|
export function parsePayload(payload) {
|
|
@@ -80,6 +87,16 @@ export function buildSocialUrl(platformOrAppName, target, text) {
|
|
|
80
87
|
if (isLinkedIn) {
|
|
81
88
|
return `https://www.linkedin.com/messaging/`;
|
|
82
89
|
}
|
|
90
|
+
const isEmail = pLower.includes('gmail') || pLower.includes('mail') || pLower.includes('email') || (cleanTarget.includes('@') && cleanTarget.includes('.')) || cleanTarget.includes('mail.google.com');
|
|
91
|
+
if (isEmail) {
|
|
92
|
+
const to = cleanTarget.includes('@') && !cleanTarget.startsWith('http') ? cleanTarget : (parsedPayload.to || parsedPayload.recipient || '');
|
|
93
|
+
const subject = parsedPayload.subject || parsedPayload.su || 'Message from Scout';
|
|
94
|
+
const body = rawMsgText || parsedPayload.body || '';
|
|
95
|
+
if (to || body) {
|
|
96
|
+
return `https://mail.google.com/mail/?view=cm&fs=1${to ? `&to=${encodeURIComponent(to)}` : ''}${subject ? `&su=${encodeURIComponent(subject)}` : ''}${body ? `&body=${encodeURIComponent(body)}` : ''}`;
|
|
97
|
+
}
|
|
98
|
+
return 'https://mail.google.com';
|
|
99
|
+
}
|
|
83
100
|
if (cleanTarget.startsWith('http://') || cleanTarget.startsWith('https://')) {
|
|
84
101
|
return cleanTarget;
|
|
85
102
|
}
|
|
@@ -88,6 +105,78 @@ export function buildSocialUrl(platformOrAppName, target, text) {
|
|
|
88
105
|
}
|
|
89
106
|
return 'https://www.instagram.com/direct/inbox/';
|
|
90
107
|
}
|
|
108
|
+
/**
|
|
109
|
+
* Universally search and locate any installed app, game, or store package on the host OS without hardcoding.
|
|
110
|
+
* Automatically resolves Win32 executables, Microsoft Store games, Steam games, and desktop shortcuts.
|
|
111
|
+
*/
|
|
112
|
+
export async function findInstalledApp(query) {
|
|
113
|
+
const cleanQuery = (query || '').replace(/[^\w\s-]/g, '').trim().toLowerCase();
|
|
114
|
+
if (!cleanQuery)
|
|
115
|
+
return null;
|
|
116
|
+
const platform = process.platform;
|
|
117
|
+
if (platform === 'win32') {
|
|
118
|
+
try {
|
|
119
|
+
// Query Windows Get-StartApps with wildcard matching
|
|
120
|
+
const safePattern = `*${cleanQuery.replace(/[*_']/g, '')}*`;
|
|
121
|
+
const psCmd = `powershell.exe -NoProfile -NonInteractive -Command "Get-StartApps -Name '${safePattern}' | Select-Object -First 1 Name, AppID | ConvertTo-Json"`;
|
|
122
|
+
const { stdout } = await execAsync(psCmd, { timeout: 3500 });
|
|
123
|
+
if (stdout && stdout.trim()) {
|
|
124
|
+
const parsed = JSON.parse(stdout.trim());
|
|
125
|
+
if (parsed && (parsed.Name || parsed.AppID)) {
|
|
126
|
+
return {
|
|
127
|
+
name: String(parsed.Name || query),
|
|
128
|
+
appId: String(parsed.AppID || ''),
|
|
129
|
+
source: 'start_apps',
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
catch { }
|
|
135
|
+
// Fallback: check registered URI scheme protocol (e.g. steam, roblox, spotify, discord, minecraft, riot)
|
|
136
|
+
if (!cleanQuery.includes(' ') && cleanQuery.length >= 3) {
|
|
137
|
+
try {
|
|
138
|
+
const checkReg = `powershell.exe -NoProfile -NonInteractive -Command "Test-Path 'Registry::HKEY_CLASSES_ROOT\\${cleanQuery}'"`;
|
|
139
|
+
const { stdout } = await execAsync(checkReg, { timeout: 1500 });
|
|
140
|
+
if (stdout && stdout.trim().toLowerCase() === 'true') {
|
|
141
|
+
return {
|
|
142
|
+
name: query,
|
|
143
|
+
appId: `${cleanQuery}:`,
|
|
144
|
+
source: 'protocol',
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
catch { }
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
else if (platform === 'darwin') {
|
|
152
|
+
try {
|
|
153
|
+
const { stdout } = await execAsync(`mdfind "kMDItemKind == 'Application' && kMDItemDisplayName == '*${cleanQuery}*'c" | head -n 1`, { timeout: 3000 });
|
|
154
|
+
if (stdout && stdout.trim()) {
|
|
155
|
+
return {
|
|
156
|
+
name: path.basename(stdout.trim(), '.app'),
|
|
157
|
+
appId: stdout.trim(),
|
|
158
|
+
source: 'path',
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
catch { }
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
// Linux: check which or desktop application entries
|
|
166
|
+
try {
|
|
167
|
+
const { stdout } = await execAsync(`which "${cleanQuery}" || find /usr/share/applications -name "*${cleanQuery}*.desktop" | head -n 1`, { timeout: 2500 });
|
|
168
|
+
if (stdout && stdout.trim()) {
|
|
169
|
+
return {
|
|
170
|
+
name: query,
|
|
171
|
+
appId: stdout.trim(),
|
|
172
|
+
source: 'path',
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
catch { }
|
|
177
|
+
}
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
91
180
|
/**
|
|
92
181
|
* Cross-platform app opener.
|
|
93
182
|
* Launches and opens target application on Windows, macOS, or Linux.
|
|
@@ -198,16 +287,42 @@ export async function openApp(appNameOrPath, target, options) {
|
|
|
198
287
|
}
|
|
199
288
|
}
|
|
200
289
|
else {
|
|
201
|
-
//
|
|
202
|
-
const
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
290
|
+
// Universal Application, Game & Desktop Launcher (Zero-Hardcode OS Discovery)
|
|
291
|
+
const targetQuery = name || appNameOrPath;
|
|
292
|
+
const installedApp = await findInstalledApp(targetQuery);
|
|
293
|
+
if (installedApp) {
|
|
294
|
+
if (platform === 'win32') {
|
|
295
|
+
if (installedApp.source === 'protocol') {
|
|
296
|
+
const extraArg = cleanTarget ? ` "${cleanTarget}"` : '';
|
|
297
|
+
command = `start ${installedApp.appId}${extraArg}`;
|
|
298
|
+
}
|
|
299
|
+
else {
|
|
300
|
+
command = `explorer.exe "shell:AppsFolder\\${installedApp.appId}"`;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
else if (platform === 'darwin') {
|
|
304
|
+
command = `open -a "${installedApp.appId || installedApp.name}"`;
|
|
305
|
+
}
|
|
306
|
+
else {
|
|
307
|
+
command = `"${installedApp.appId || installedApp.name}" &`;
|
|
308
|
+
}
|
|
208
309
|
}
|
|
209
310
|
else {
|
|
210
|
-
|
|
311
|
+
// Protocol scheme check (e.g. steam:, roblox:, spotify:, discord:, minecraft:, etc.)
|
|
312
|
+
const isProtocol = /^[a-zA-Z0-9_\-]+:/.test(appNameOrPath) || (cleanTarget && /^[a-zA-Z0-9_\-]+:/.test(cleanTarget));
|
|
313
|
+
if (isProtocol && platform === 'win32') {
|
|
314
|
+
const proto = /^[a-zA-Z0-9_\-]+:/.test(appNameOrPath) ? appNameOrPath : cleanTarget;
|
|
315
|
+
command = `start ${proto}`;
|
|
316
|
+
}
|
|
317
|
+
else if (platform === 'win32') {
|
|
318
|
+
command = cleanTarget ? `start "" "${appNameOrPath}" ${cleanTarget}` : `start "" "${appNameOrPath}"`;
|
|
319
|
+
}
|
|
320
|
+
else if (platform === 'darwin') {
|
|
321
|
+
command = cleanTarget ? `open -a "${appNameOrPath}" "${cleanTarget}"` : `open -a "${appNameOrPath}"`;
|
|
322
|
+
}
|
|
323
|
+
else {
|
|
324
|
+
command = cleanTarget ? `${appNameOrPath} "${cleanTarget}" &` : `${appNameOrPath} &`;
|
|
325
|
+
}
|
|
211
326
|
}
|
|
212
327
|
}
|
|
213
328
|
try {
|
|
@@ -261,7 +376,7 @@ export async function executeInApp(appName, action, payload) {
|
|
|
261
376
|
output: res.output,
|
|
262
377
|
};
|
|
263
378
|
}
|
|
264
|
-
if (['capture_screen', 'screenshot', '
|
|
379
|
+
if (['capture_screen', 'screenshot', 'screen_shot'].includes(actLower)) {
|
|
265
380
|
const res = await captureScreen(name || appName);
|
|
266
381
|
return {
|
|
267
382
|
success: res.success,
|
|
@@ -271,8 +386,9 @@ export async function executeInApp(appName, action, payload) {
|
|
|
271
386
|
details: res.details,
|
|
272
387
|
};
|
|
273
388
|
}
|
|
274
|
-
if (['analyze_screen', 'inspect_desktop', 'visual_analysis', 'see_and_analyze'].includes(actLower)) {
|
|
275
|
-
const
|
|
389
|
+
if (['see_screen', 'inspect_screen', 'view_screen', 'analyze_screen', 'inspect_desktop', 'visual_analysis', 'see_and_analyze'].includes(actLower)) {
|
|
390
|
+
const query = typeof payload === 'string' ? payload : (payload?.query || payload?.target || payload?.text || undefined);
|
|
391
|
+
const res = await analyzeScreen(name || appName, query);
|
|
276
392
|
return {
|
|
277
393
|
success: res.success,
|
|
278
394
|
app: name || appName,
|
|
@@ -293,9 +409,26 @@ export async function executeInApp(appName, action, payload) {
|
|
|
293
409
|
}
|
|
294
410
|
if (['type_text', 'send_keys', 'type', 'keystrokes', 'key_combo', 'press_hotkey', 'hotkey'].includes(actLower)) {
|
|
295
411
|
const parsed = parsePayload(payload);
|
|
296
|
-
const
|
|
297
|
-
|
|
298
|
-
|
|
412
|
+
const isHotKeyAction = ['key_combo', 'press_hotkey', 'hotkey', 'send_keys'].includes(actLower);
|
|
413
|
+
let keyCombo = undefined;
|
|
414
|
+
let textToType = '';
|
|
415
|
+
if (typeof parsed === 'object') {
|
|
416
|
+
keyCombo = parsed.keyCombo || parsed.combo || parsed.hotkey || parsed.keys || parsed.key;
|
|
417
|
+
textToType = parsed.text || parsed.content || parsed.message || '';
|
|
418
|
+
}
|
|
419
|
+
if (!keyCombo && typeof payload === 'string') {
|
|
420
|
+
const trimmed = payload.trim();
|
|
421
|
+
if (isHotKeyAction || trimmed.includes('+') || ['enter', 'return', 'tab', 'esc', 'escape', 'backspace', 'space', 'up', 'down', 'left', 'right'].includes(trimmed.toLowerCase())) {
|
|
422
|
+
keyCombo = trimmed;
|
|
423
|
+
}
|
|
424
|
+
else {
|
|
425
|
+
textToType = trimmed;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
else if (!textToType && typeof payload === 'string' && !payload.trim().startsWith('{')) {
|
|
429
|
+
textToType = payload;
|
|
430
|
+
}
|
|
431
|
+
const pressEnter = typeof parsed === 'object' && parsed.enter !== undefined ? parsed.enter : !isHotKeyAction;
|
|
299
432
|
const ksRes = await sendKeystrokes(name || appName, textToType, pressEnter, keyCombo);
|
|
300
433
|
return {
|
|
301
434
|
success: ksRes.success,
|
|
@@ -305,14 +438,34 @@ export async function executeInApp(appName, action, payload) {
|
|
|
305
438
|
details: { appName: name || appName, textToType, pressEnter, keyCombo },
|
|
306
439
|
};
|
|
307
440
|
}
|
|
308
|
-
if (['
|
|
441
|
+
if (['click_element', 'click_ui', 'click_button', 'tap_element'].includes(actLower)) {
|
|
442
|
+
const parsed = parsePayload(payload);
|
|
443
|
+
const query = typeof parsed === 'object' ? (parsed.element || parsed.target || parsed.name || parsed.query || '') : String(parsed || payload);
|
|
444
|
+
const button = typeof parsed === 'object' && parsed.button ? parsed.button : 'left';
|
|
445
|
+
const res = await findAndClickElement(query, name || appName, button);
|
|
446
|
+
return {
|
|
447
|
+
success: res.success,
|
|
448
|
+
app: name || appName,
|
|
449
|
+
action: actLower,
|
|
450
|
+
output: res.output,
|
|
451
|
+
details: res.coordinates,
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
if (['click', 'click_app', 'click_at', 'move_mouse', 'drag_mouse'].includes(actLower)) {
|
|
309
455
|
const parsedPayload = parsePayload(payload);
|
|
456
|
+
if (typeof parsedPayload === 'object' && parsedPayload.element && parsedPayload.x === undefined) {
|
|
457
|
+
const res = await findAndClickElement(parsedPayload.element, name || appName, parsedPayload.button || 'left');
|
|
458
|
+
return {
|
|
459
|
+
success: res.success,
|
|
460
|
+
app: name || appName,
|
|
461
|
+
action: actLower,
|
|
462
|
+
output: res.output,
|
|
463
|
+
details: res.coordinates,
|
|
464
|
+
};
|
|
465
|
+
}
|
|
310
466
|
const x = typeof parsedPayload.x === 'number' ? parsedPayload.x : undefined;
|
|
311
467
|
const y = typeof parsedPayload.y === 'number' ? parsedPayload.y : undefined;
|
|
312
468
|
const button = ['right', 'double', 'middle', 'scroll'].includes(parsedPayload.button) ? parsedPayload.button : 'left';
|
|
313
|
-
if (name && name !== 'desktop' && name !== 'system') {
|
|
314
|
-
await openApp(name);
|
|
315
|
-
}
|
|
316
469
|
const clickRes = await moveAndClickMouse(x, y, button, name || appName);
|
|
317
470
|
return {
|
|
318
471
|
success: clickRes.success,
|
|
@@ -322,6 +475,52 @@ export async function executeInApp(appName, action, payload) {
|
|
|
322
475
|
details: { appName: name || appName, x, y, button },
|
|
323
476
|
};
|
|
324
477
|
}
|
|
478
|
+
if (['scroll', 'scroll_window', 'scroll_down', 'scroll_up', 'scroll_page'].includes(actLower)) {
|
|
479
|
+
const parsed = parsePayload(payload);
|
|
480
|
+
let direction = 'down';
|
|
481
|
+
let amount = 3;
|
|
482
|
+
if (typeof parsed === 'object') {
|
|
483
|
+
if (parsed.direction === 'up' || parsed.dir === 'up')
|
|
484
|
+
direction = 'up';
|
|
485
|
+
if (typeof parsed.amount === 'number')
|
|
486
|
+
amount = parsed.amount;
|
|
487
|
+
if (typeof parsed.clicks === 'number')
|
|
488
|
+
amount = parsed.clicks;
|
|
489
|
+
}
|
|
490
|
+
else if (typeof payload === 'string' && payload.toLowerCase().includes('up')) {
|
|
491
|
+
direction = 'up';
|
|
492
|
+
}
|
|
493
|
+
if (actLower === 'scroll_up')
|
|
494
|
+
direction = 'up';
|
|
495
|
+
const res = await scrollWindow(direction, amount);
|
|
496
|
+
return {
|
|
497
|
+
success: res.success,
|
|
498
|
+
app: name || appName,
|
|
499
|
+
action: actLower,
|
|
500
|
+
output: res.output,
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
if (['list_windows', 'get_windows', 'open_windows', 'show_windows'].includes(actLower)) {
|
|
504
|
+
const res = await listWindows();
|
|
505
|
+
return {
|
|
506
|
+
success: res.success,
|
|
507
|
+
app: name || appName,
|
|
508
|
+
action: actLower,
|
|
509
|
+
output: res.output,
|
|
510
|
+
details: res.windows,
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
if (['focus_window', 'switch_window', 'bring_to_front', 'select_window'].includes(actLower)) {
|
|
514
|
+
const parsed = parsePayload(payload);
|
|
515
|
+
const target = typeof parsed === 'object' ? (parsed.title || parsed.app || parsed.name || '') : String(parsed || payload || name || appName);
|
|
516
|
+
const res = await focusWindow(target);
|
|
517
|
+
return {
|
|
518
|
+
success: res.success,
|
|
519
|
+
app: target,
|
|
520
|
+
action: actLower,
|
|
521
|
+
output: res.output,
|
|
522
|
+
};
|
|
523
|
+
}
|
|
325
524
|
if (['lock_app', 'block_interruption', 'lock_input', 'lock'].includes(actLower)) {
|
|
326
525
|
const parsedPayload = parsePayload(payload);
|
|
327
526
|
const durationMs = typeof parsedPayload.duration === 'number' ? parsedPayload.duration : 3000;
|
|
@@ -337,7 +536,19 @@ export async function executeInApp(appName, action, payload) {
|
|
|
337
536
|
if (category === 'browser') {
|
|
338
537
|
if (['navigate', 'goto', 'open_url'].includes(actLower)) {
|
|
339
538
|
const rawUrl = typeof payload === 'string' ? payload : (payload?.url || payload?.target || 'https://google.com');
|
|
340
|
-
|
|
539
|
+
let targetUrl = rawUrl.trim();
|
|
540
|
+
let navigationNote = '';
|
|
541
|
+
// Universal URL vs Search Query Grounding:
|
|
542
|
+
// If the input is not a valid URL (contains spaces or lacks a dot / scheme), convert into a search
|
|
543
|
+
if (!targetUrl.startsWith('http://') && !targetUrl.startsWith('https://')) {
|
|
544
|
+
if (targetUrl.includes(' ') || !targetUrl.includes('.')) {
|
|
545
|
+
navigationNote = ` [Note: Plain search term passed to navigate; converted to web search query]`;
|
|
546
|
+
targetUrl = `https://www.google.com/search?q=${encodeURIComponent(targetUrl)}`;
|
|
547
|
+
}
|
|
548
|
+
else {
|
|
549
|
+
targetUrl = `https://${targetUrl}`;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
341
552
|
await openApp(name || 'browser', targetUrl);
|
|
342
553
|
await new Promise((r) => setTimeout(r, 800));
|
|
343
554
|
const capture = await captureScreen(name || 'browser');
|
|
@@ -345,7 +556,7 @@ export async function executeInApp(appName, action, payload) {
|
|
|
345
556
|
success: true,
|
|
346
557
|
app: name || 'browser',
|
|
347
558
|
action: actLower,
|
|
348
|
-
output: `Browser Navigated Successfully to Target URL: ${targetUrl}\n- Screenshot Saved: ${capture.details?.outPath || 'Active'}`,
|
|
559
|
+
output: `Browser Navigated Successfully to Target URL: ${targetUrl}${navigationNote}\n- Screenshot Saved: ${capture.details?.outPath || 'Active'}`,
|
|
349
560
|
details: { url: targetUrl, capture: capture.details },
|
|
350
561
|
};
|
|
351
562
|
}
|
|
@@ -394,16 +605,27 @@ export async function executeInApp(appName, action, payload) {
|
|
|
394
605
|
details: { query, searchUrl, capture: capture.details },
|
|
395
606
|
};
|
|
396
607
|
}
|
|
397
|
-
if (['send_dm', 'open_dm', 'dm', 'send_message', 'message', 'social_dm'].includes(actLower)) {
|
|
398
|
-
const
|
|
399
|
-
|
|
400
|
-
|
|
608
|
+
if (['send_dm', 'open_dm', 'dm', 'send_message', 'message', 'social_dm', 'send_mail', 'send_email', 'mail', 'email'].includes(actLower)) {
|
|
609
|
+
const parsed = parsePayload(payload);
|
|
610
|
+
const recipient = typeof payload === 'string' && payload.includes('@') && !payload.startsWith('{')
|
|
611
|
+
? payload
|
|
612
|
+
: (parsed.recipient || parsed.to || parsed.user || parsed.target || parsed.handle || parsed.username || '');
|
|
613
|
+
let msgText = typeof parsed === 'object' ? (parsed.text || parsed.message || parsed.content || parsed.body) : undefined;
|
|
614
|
+
if (!msgText && typeof payload === 'string' && payload.length > 0 && !payload.startsWith('@') && !payload.startsWith('http') && !payload.includes('@')) {
|
|
401
615
|
msgText = payload;
|
|
402
616
|
}
|
|
403
|
-
const
|
|
617
|
+
const isEmail = ['send_mail', 'send_email', 'mail', 'email'].includes(actLower) || recipient.includes('@');
|
|
618
|
+
const targetUrl = buildSocialUrl(isEmail ? 'gmail' : name, recipient, msgText);
|
|
404
619
|
const openRes = await openApp('browser', targetUrl);
|
|
405
620
|
let keystrokeOutput = '';
|
|
406
|
-
if (
|
|
621
|
+
if (isEmail) {
|
|
622
|
+
// For Gmail direct compose: wait 2.5s for page & compose dialog to render, then press Ctrl+Enter to send!
|
|
623
|
+
await lockAppInput('browser', 3500);
|
|
624
|
+
await new Promise((r) => setTimeout(r, 2200));
|
|
625
|
+
const ksRes = await sendKeystrokes('browser', '', false, 'ctrl+enter');
|
|
626
|
+
keystrokeOutput = `\nAuto-Send Keystroke Dispatch (Ctrl+Enter): ${ksRes.output}`;
|
|
627
|
+
}
|
|
628
|
+
else if (msgText) {
|
|
407
629
|
// Lock user interruption during scratchpad keystroke dispatch so user clicks don't interrupt
|
|
408
630
|
await lockAppInput(name || 'browser', 3000);
|
|
409
631
|
await new Promise((r) => setTimeout(r, 1200));
|
|
@@ -414,8 +636,8 @@ export async function executeInApp(appName, action, payload) {
|
|
|
414
636
|
success: true,
|
|
415
637
|
app: name,
|
|
416
638
|
action: actLower,
|
|
417
|
-
output: `
|
|
418
|
-
details: { platform: name, recipient, msgText, targetUrl },
|
|
639
|
+
output: `Executed ${isEmail ? 'Gmail / Email Dispatch' : `${name} Direct Message`} for target recipient "${recipient}" at ${targetUrl}.${keystrokeOutput}`,
|
|
640
|
+
details: { platform: isEmail ? 'gmail' : name, recipient, msgText, targetUrl },
|
|
419
641
|
};
|
|
420
642
|
}
|
|
421
643
|
}
|
|
@@ -448,6 +670,10 @@ export async function executeInApp(appName, action, payload) {
|
|
|
448
670
|
return await openApp(name, filePath);
|
|
449
671
|
}
|
|
450
672
|
}
|
|
673
|
+
if (category === 'game' || ['open', 'launch', 'start'].includes(actLower)) {
|
|
674
|
+
const rawTarget = typeof payload === 'string' ? payload : (payload?.target || payload?.game || payload?.url || payload?.query || payload?.id || '');
|
|
675
|
+
return await openApp(name || appName, rawTarget);
|
|
676
|
+
}
|
|
451
677
|
// Generic fallback app action
|
|
452
678
|
return {
|
|
453
679
|
success: true,
|
|
@@ -508,49 +734,291 @@ export function getAppCapabilities() {
|
|
|
508
734
|
defaultEditorLauncher: platform === 'win32' ? 'code / notepad' : platform === 'darwin' ? 'code / open -a' : 'code / xdg-open',
|
|
509
735
|
};
|
|
510
736
|
}
|
|
737
|
+
const WIN_CAPTURE_SCRIPT_CONTENT = `param(
|
|
738
|
+
[string]$OutPath,
|
|
739
|
+
[switch]$InfoOnly
|
|
740
|
+
)
|
|
741
|
+
|
|
742
|
+
$source = @"
|
|
743
|
+
using System;
|
|
744
|
+
using System.Drawing;
|
|
745
|
+
using System.Drawing.Imaging;
|
|
746
|
+
using System.Runtime.InteropServices;
|
|
747
|
+
using System.Text;
|
|
748
|
+
using System.Threading;
|
|
749
|
+
using System.Collections.Generic;
|
|
750
|
+
|
|
751
|
+
public class WinCaptureResult {
|
|
752
|
+
public bool success;
|
|
753
|
+
public string savedPath;
|
|
754
|
+
public int width;
|
|
755
|
+
public int height;
|
|
756
|
+
public int cursorX;
|
|
757
|
+
public int cursorY;
|
|
758
|
+
public string activeWindow;
|
|
759
|
+
public string[] visibleWindows;
|
|
760
|
+
public string error;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
public class WinCaptureHelper {
|
|
764
|
+
[DllImport("user32.dll", SetLastError = true)]
|
|
765
|
+
public static extern IntPtr OpenInputDesktop(uint dwFlags, bool fInherit, uint dwDesiredAccess);
|
|
766
|
+
|
|
767
|
+
[DllImport("user32.dll", SetLastError = true)]
|
|
768
|
+
public static extern IntPtr OpenDesktop(string lpszDesktop, uint dwFlags, bool fInherit, uint dwDesiredAccess);
|
|
769
|
+
|
|
770
|
+
[DllImport("user32.dll", SetLastError = true)]
|
|
771
|
+
public static extern bool SetThreadDesktop(IntPtr hDesktop);
|
|
772
|
+
|
|
773
|
+
[DllImport("user32.dll", SetLastError = true)]
|
|
774
|
+
public static extern bool CloseDesktop(IntPtr hDesktop);
|
|
775
|
+
|
|
776
|
+
[DllImport("user32.dll")]
|
|
777
|
+
public static extern IntPtr GetDC(IntPtr hWnd);
|
|
778
|
+
|
|
779
|
+
[DllImport("user32.dll")]
|
|
780
|
+
public static extern int ReleaseDC(IntPtr hWnd, IntPtr hDC);
|
|
781
|
+
|
|
782
|
+
[DllImport("gdi32.dll")]
|
|
783
|
+
public static extern IntPtr CreateCompatibleDC(IntPtr hdc);
|
|
784
|
+
|
|
785
|
+
[DllImport("gdi32.dll")]
|
|
786
|
+
public static extern IntPtr CreateCompatibleBitmap(IntPtr hdc, int nWidth, int nHeight);
|
|
787
|
+
|
|
788
|
+
[DllImport("gdi32.dll")]
|
|
789
|
+
public static extern IntPtr SelectObject(IntPtr hdc, IntPtr hgdiobj);
|
|
790
|
+
|
|
791
|
+
[DllImport("gdi32.dll")]
|
|
792
|
+
public static extern bool DeleteDC(IntPtr hdc);
|
|
793
|
+
|
|
794
|
+
[DllImport("gdi32.dll")]
|
|
795
|
+
public static extern bool DeleteObject(IntPtr hObject);
|
|
796
|
+
|
|
797
|
+
[DllImport("gdi32.dll")]
|
|
798
|
+
public static extern bool BitBlt(IntPtr hdcDest, int nXDest, int nYDest, int nWidth, int nHeight, IntPtr hdcSrc, int nXSrc, int nYSrc, int dwRop);
|
|
799
|
+
|
|
800
|
+
[DllImport("user32.dll")]
|
|
801
|
+
public static extern int GetSystemMetrics(int nIndex);
|
|
802
|
+
|
|
803
|
+
public delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
|
|
804
|
+
|
|
805
|
+
[DllImport("user32.dll")]
|
|
806
|
+
public static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam);
|
|
807
|
+
|
|
808
|
+
[DllImport("user32.dll", CharSet = CharSet.Auto)]
|
|
809
|
+
public static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount);
|
|
810
|
+
|
|
811
|
+
[DllImport("user32.dll")]
|
|
812
|
+
public static extern bool IsWindowVisible(IntPtr hWnd);
|
|
813
|
+
|
|
814
|
+
[DllImport("user32.dll")]
|
|
815
|
+
public static extern IntPtr GetForegroundWindow();
|
|
816
|
+
|
|
817
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
818
|
+
public struct POINT {
|
|
819
|
+
public int X;
|
|
820
|
+
public int Y;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
[DllImport("user32.dll")]
|
|
824
|
+
public static extern bool GetCursorPos(out POINT lpPoint);
|
|
825
|
+
|
|
826
|
+
const uint DESKTOP_ACCESS = 0x01FF | 0x00020000;
|
|
827
|
+
const int SM_CXSCREEN = 0;
|
|
828
|
+
const int SM_CYSCREEN = 1;
|
|
829
|
+
const int SRCCOPY = 0x00CC0020;
|
|
830
|
+
const int CAPTUREBLT = 0x40000000;
|
|
831
|
+
|
|
832
|
+
public static WinCaptureResult GetInfo() {
|
|
833
|
+
var res = new WinCaptureResult();
|
|
834
|
+
res.success = true;
|
|
835
|
+
res.error = "";
|
|
836
|
+
var visibleWindows = new List<string>();
|
|
837
|
+
|
|
838
|
+
Thread worker = new Thread(() => {
|
|
839
|
+
IntPtr hDesk = OpenInputDesktop(0, false, DESKTOP_ACCESS);
|
|
840
|
+
if (hDesk == IntPtr.Zero) {
|
|
841
|
+
hDesk = OpenDesktop("Default", 0, false, DESKTOP_ACCESS);
|
|
842
|
+
}
|
|
843
|
+
if (hDesk != IntPtr.Zero) {
|
|
844
|
+
SetThreadDesktop(hDesk);
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
res.width = GetSystemMetrics(SM_CXSCREEN);
|
|
848
|
+
res.height = GetSystemMetrics(SM_CYSCREEN);
|
|
849
|
+
if (res.width <= 0) res.width = 1920;
|
|
850
|
+
if (res.height <= 0) res.height = 1080;
|
|
851
|
+
|
|
852
|
+
POINT pt;
|
|
853
|
+
if (GetCursorPos(out pt)) {
|
|
854
|
+
res.cursorX = pt.X;
|
|
855
|
+
res.cursorY = pt.Y;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
IntPtr fg = GetForegroundWindow();
|
|
859
|
+
if (fg != IntPtr.Zero) {
|
|
860
|
+
var sbFg = new StringBuilder(256);
|
|
861
|
+
GetWindowText(fg, sbFg, 256);
|
|
862
|
+
res.activeWindow = sbFg.ToString().Trim();
|
|
863
|
+
} else {
|
|
864
|
+
res.activeWindow = "";
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
EnumWindows((hWnd, lParam) => {
|
|
868
|
+
if (IsWindowVisible(hWnd)) {
|
|
869
|
+
var title = new StringBuilder(256);
|
|
870
|
+
GetWindowText(hWnd, title, 256);
|
|
871
|
+
string t = title.ToString().Trim();
|
|
872
|
+
if (t.Length > 0 && !visibleWindows.Contains(t) && t != "Program Manager" && t != "Windows Input Experience") {
|
|
873
|
+
visibleWindows.Add(t);
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
return true;
|
|
877
|
+
}, IntPtr.Zero);
|
|
878
|
+
|
|
879
|
+
if (hDesk != IntPtr.Zero) {
|
|
880
|
+
CloseDesktop(hDesk);
|
|
881
|
+
}
|
|
882
|
+
});
|
|
883
|
+
|
|
884
|
+
worker.SetApartmentState(ApartmentState.STA);
|
|
885
|
+
worker.Start();
|
|
886
|
+
worker.Join();
|
|
887
|
+
|
|
888
|
+
res.visibleWindows = visibleWindows.ToArray();
|
|
889
|
+
return res;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
public static WinCaptureResult Capture(string filePath) {
|
|
893
|
+
var res = new WinCaptureResult();
|
|
894
|
+
res.savedPath = filePath;
|
|
895
|
+
res.error = "";
|
|
896
|
+
var visibleWindows = new List<string>();
|
|
897
|
+
|
|
898
|
+
Thread worker = new Thread(() => {
|
|
899
|
+
IntPtr hDesk = OpenInputDesktop(0, false, DESKTOP_ACCESS);
|
|
900
|
+
if (hDesk == IntPtr.Zero) {
|
|
901
|
+
hDesk = OpenDesktop("Default", 0, false, DESKTOP_ACCESS);
|
|
902
|
+
}
|
|
903
|
+
if (hDesk != IntPtr.Zero) {
|
|
904
|
+
SetThreadDesktop(hDesk);
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
res.width = GetSystemMetrics(SM_CXSCREEN);
|
|
908
|
+
res.height = GetSystemMetrics(SM_CYSCREEN);
|
|
909
|
+
if (res.width <= 0) res.width = 1920;
|
|
910
|
+
if (res.height <= 0) res.height = 1080;
|
|
911
|
+
|
|
912
|
+
POINT pt;
|
|
913
|
+
if (GetCursorPos(out pt)) {
|
|
914
|
+
res.cursorX = pt.X;
|
|
915
|
+
res.cursorY = pt.Y;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
IntPtr fg = GetForegroundWindow();
|
|
919
|
+
if (fg != IntPtr.Zero) {
|
|
920
|
+
var sbFg = new StringBuilder(256);
|
|
921
|
+
GetWindowText(fg, sbFg, 256);
|
|
922
|
+
res.activeWindow = sbFg.ToString().Trim();
|
|
923
|
+
} else {
|
|
924
|
+
res.activeWindow = "";
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
EnumWindows((hWnd, lParam) => {
|
|
928
|
+
if (IsWindowVisible(hWnd)) {
|
|
929
|
+
var title = new StringBuilder(256);
|
|
930
|
+
GetWindowText(hWnd, title, 256);
|
|
931
|
+
string t = title.ToString().Trim();
|
|
932
|
+
if (t.Length > 0 && !visibleWindows.Contains(t) && t != "Program Manager" && t != "Windows Input Experience") {
|
|
933
|
+
visibleWindows.Add(t);
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
return true;
|
|
937
|
+
}, IntPtr.Zero);
|
|
938
|
+
|
|
939
|
+
try {
|
|
940
|
+
IntPtr hdcSrc = GetDC(IntPtr.Zero);
|
|
941
|
+
IntPtr hdcDest = CreateCompatibleDC(hdcSrc);
|
|
942
|
+
IntPtr hBitmap = CreateCompatibleBitmap(hdcSrc, res.width, res.height);
|
|
943
|
+
IntPtr hOld = SelectObject(hdcDest, hBitmap);
|
|
944
|
+
bool bltOk = BitBlt(hdcDest, 0, 0, res.width, res.height, hdcSrc, 0, 0, SRCCOPY | CAPTUREBLT);
|
|
945
|
+
|
|
946
|
+
SelectObject(hdcDest, hOld);
|
|
947
|
+
DeleteDC(hdcDest);
|
|
948
|
+
ReleaseDC(IntPtr.Zero, hdcSrc);
|
|
949
|
+
|
|
950
|
+
if (bltOk) {
|
|
951
|
+
using (Bitmap bmp = Bitmap.FromHbitmap(hBitmap)) {
|
|
952
|
+
bmp.Save(filePath, ImageFormat.Png);
|
|
953
|
+
}
|
|
954
|
+
res.success = true;
|
|
955
|
+
} else {
|
|
956
|
+
res.error = "BitBlt failed";
|
|
957
|
+
}
|
|
958
|
+
DeleteObject(hBitmap);
|
|
959
|
+
} catch (Exception ex) {
|
|
960
|
+
res.error = ex.Message;
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
if (hDesk != IntPtr.Zero) {
|
|
964
|
+
CloseDesktop(hDesk);
|
|
965
|
+
}
|
|
966
|
+
});
|
|
967
|
+
|
|
968
|
+
worker.SetApartmentState(ApartmentState.STA);
|
|
969
|
+
worker.Start();
|
|
970
|
+
worker.Join();
|
|
971
|
+
|
|
972
|
+
res.visibleWindows = visibleWindows.ToArray();
|
|
973
|
+
return res;
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
"@
|
|
977
|
+
|
|
978
|
+
Add-Type -TypeDefinition $source -ReferencedAssemblies System.Drawing, System.Windows.Forms
|
|
979
|
+
|
|
980
|
+
if ($InfoOnly) {
|
|
981
|
+
[WinCaptureHelper]::GetInfo() | ConvertTo-Json -Compress
|
|
982
|
+
} else {
|
|
983
|
+
[WinCaptureHelper]::Capture($OutPath) | ConvertTo-Json -Compress
|
|
984
|
+
}
|
|
985
|
+
`;
|
|
986
|
+
export function ensureWinCaptureScript() {
|
|
987
|
+
const scriptsDir = path.join(process.cwd(), '.ft', 'scripts');
|
|
988
|
+
if (!fs.existsSync(scriptsDir)) {
|
|
989
|
+
fs.mkdirSync(scriptsDir, { recursive: true });
|
|
990
|
+
}
|
|
991
|
+
const scriptPath = path.join(scriptsDir, 'winCapture.ps1');
|
|
992
|
+
fs.writeFileSync(scriptPath, WIN_CAPTURE_SCRIPT_CONTENT, 'utf8');
|
|
993
|
+
return scriptPath;
|
|
994
|
+
}
|
|
511
995
|
/**
|
|
512
|
-
* Get screen metrics, active window title, and cursor position.
|
|
996
|
+
* Get screen metrics, active window title, visible windows, and cursor position.
|
|
513
997
|
*/
|
|
514
998
|
export async function getScreenInfo(appName) {
|
|
515
999
|
const platform = process.platform;
|
|
516
1000
|
if (platform === 'win32') {
|
|
517
|
-
const psScript = `
|
|
518
|
-
Add-Type -AssemblyName System.Windows.Forms
|
|
519
|
-
Add-Type -AssemblyName System.Drawing
|
|
520
|
-
|
|
521
|
-
$screen = [System.Windows.Forms.Screen]::PrimaryScreen
|
|
522
|
-
$cursor = [System.Windows.Forms.Cursor]::Position
|
|
523
|
-
|
|
524
|
-
$sig = '[DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow(); [DllImport("user32.dll")] public static extern int GetWindowText(IntPtr hWnd, System.Text.StringBuilder text, int count);'
|
|
525
|
-
$w = Add-Type -memberDefinition $sig -name 'Win32Window' -namespace Win32Functions -passThru
|
|
526
|
-
$hwnd = $w::GetForegroundWindow()
|
|
527
|
-
$sb = New-Object System.Text.StringBuilder 256
|
|
528
|
-
[void]$w::GetWindowText($hwnd, $sb, 256)
|
|
529
|
-
$activeTitle = $sb.ToString()
|
|
530
|
-
|
|
531
|
-
@{
|
|
532
|
-
screenWidth = $screen.Bounds.Width
|
|
533
|
-
screenHeight = $screen.Bounds.Height
|
|
534
|
-
cursorX = $cursor.X
|
|
535
|
-
cursorY = $cursor.Y
|
|
536
|
-
activeWindow = $activeTitle
|
|
537
|
-
} | ConvertTo-Json
|
|
538
|
-
`;
|
|
539
1001
|
try {
|
|
540
|
-
const
|
|
541
|
-
const { stdout } = await execAsync(`powershell -NoProfile -
|
|
1002
|
+
const scriptPath = ensureWinCaptureScript();
|
|
1003
|
+
const { stdout } = await execAsync(`powershell -NoProfile -ExecutionPolicy Bypass -File "${scriptPath}" -InfoOnly`);
|
|
542
1004
|
const data = JSON.parse(stdout.trim());
|
|
1005
|
+
const visList = Array.isArray(data.visibleWindows) && data.visibleWindows.length > 0
|
|
1006
|
+
? ` | Visible Windows: ${data.visibleWindows.slice(0, 5).join(', ')}`
|
|
1007
|
+
: '';
|
|
543
1008
|
return {
|
|
544
1009
|
success: true,
|
|
545
|
-
output: `Screen Info: Resolution ${data.screenWidth}x${data.screenHeight} | Cursor at (${data.cursorX}, ${data.cursorY}) | Active Window: "${data.activeWindow || 'Desktop'}"`,
|
|
546
|
-
details:
|
|
1010
|
+
output: `Screen Info: Resolution ${data.screenWidth}x${data.screenHeight} | Cursor at (${data.cursorX}, ${data.cursorY}) | Active Window: "${data.activeWindow || appName || 'Desktop'}"${visList}`,
|
|
1011
|
+
details: {
|
|
1012
|
+
...data,
|
|
1013
|
+
activeWindow: data.activeWindow || appName || 'Desktop',
|
|
1014
|
+
},
|
|
547
1015
|
};
|
|
548
1016
|
}
|
|
549
|
-
catch
|
|
1017
|
+
catch {
|
|
550
1018
|
return {
|
|
551
1019
|
success: true,
|
|
552
1020
|
output: `Screen Info (Default): Resolution 1920x1080 | Active Window: "${appName || 'Desktop'}"`,
|
|
553
|
-
details: { screenWidth: 1920, screenHeight: 1080, activeWindow: appName || 'Desktop' },
|
|
1021
|
+
details: { screenWidth: 1920, screenHeight: 1080, activeWindow: appName || 'Desktop', cursorX: 0, cursorY: 0 },
|
|
554
1022
|
};
|
|
555
1023
|
}
|
|
556
1024
|
}
|
|
@@ -562,8 +1030,219 @@ $activeTitle = $sb.ToString()
|
|
|
562
1030
|
};
|
|
563
1031
|
}
|
|
564
1032
|
}
|
|
1033
|
+
/**
|
|
1034
|
+
* Display a futuristic sky-blue aura animation on screen borders and corners,
|
|
1035
|
+
* show the warning "Scout is on the screen.", and block external inputs
|
|
1036
|
+
* during live screen capture and automation.
|
|
1037
|
+
*/
|
|
1038
|
+
export async function showScreenAuraOverlay(durationMs = 2200, message = "Scout is viewing the screen.", blockInput = false) {
|
|
1039
|
+
const platform = process.platform;
|
|
1040
|
+
if (platform !== 'win32')
|
|
1041
|
+
return;
|
|
1042
|
+
try {
|
|
1043
|
+
const scriptsDir = path.join(process.cwd(), '.ft', 'scripts');
|
|
1044
|
+
if (!fs.existsSync(scriptsDir)) {
|
|
1045
|
+
fs.mkdirSync(scriptsDir, { recursive: true });
|
|
1046
|
+
}
|
|
1047
|
+
const psFile = path.join(scriptsDir, 'scout_aura_overlay.ps1');
|
|
1048
|
+
const blockInputSnippet = blockInput ? `
|
|
1049
|
+
$sig = '[DllImport("user32.dll")] public static extern bool BlockInput(bool fBlock);'
|
|
1050
|
+
$b = Add-Type -memberDefinition $sig -name 'Win32AuraBlock' -namespace Win32Functions -passThru
|
|
1051
|
+
try { [void]$b::BlockInput($true) } catch {}
|
|
1052
|
+
` : '';
|
|
1053
|
+
const unblockSnippet = blockInput ? `
|
|
1054
|
+
try { [void]$b::BlockInput($false) } catch {}
|
|
1055
|
+
` : '';
|
|
1056
|
+
const subtitleText = blockInput ? " • External Input Locked" : " • Perception Active";
|
|
1057
|
+
const overlayPs = `
|
|
1058
|
+
Add-Type -AssemblyName PresentationFramework, PresentationCore, WindowsBase
|
|
1059
|
+
${blockInputSnippet}
|
|
1060
|
+
|
|
1061
|
+
[xml]$xaml = @"
|
|
1062
|
+
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
|
1063
|
+
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
|
1064
|
+
Title="Scout Aura Overlay"
|
|
1065
|
+
WindowStyle="None"
|
|
1066
|
+
AllowsTransparency="True"
|
|
1067
|
+
Background="Transparent"
|
|
1068
|
+
Topmost="True"
|
|
1069
|
+
ShowInTaskbar="False"
|
|
1070
|
+
ShowActivated="False"
|
|
1071
|
+
IsHitTestVisible="False">
|
|
1072
|
+
<Window.Resources>
|
|
1073
|
+
<Storyboard x:Key="AuraPulse" RepeatBehavior="Forever" AutoReverse="True">
|
|
1074
|
+
<DoubleAnimation Storyboard.TargetName="AuraBorder" Storyboard.TargetProperty="Opacity"
|
|
1075
|
+
From="0.4" To="1.0" Duration="0:0:0.6"/>
|
|
1076
|
+
<DoubleAnimation Storyboard.TargetName="CornerTL" Storyboard.TargetProperty="Opacity"
|
|
1077
|
+
From="0.5" To="1.0" Duration="0:0:0.6"/>
|
|
1078
|
+
<DoubleAnimation Storyboard.TargetName="CornerTR" Storyboard.TargetProperty="Opacity"
|
|
1079
|
+
From="0.5" To="1.0" Duration="0:0:0.6"/>
|
|
1080
|
+
<DoubleAnimation Storyboard.TargetName="CornerBL" Storyboard.TargetProperty="Opacity"
|
|
1081
|
+
From="0.5" To="1.0" Duration="0:0:0.6"/>
|
|
1082
|
+
<DoubleAnimation Storyboard.TargetName="CornerBR" Storyboard.TargetProperty="Opacity"
|
|
1083
|
+
From="0.5" To="1.0" Duration="0:0:0.6"/>
|
|
1084
|
+
<DoubleAnimation Storyboard.TargetName="AuraGlow" Storyboard.TargetProperty="Opacity"
|
|
1085
|
+
From="0.25" To="0.9" Duration="0:0:0.6"/>
|
|
1086
|
+
</Storyboard>
|
|
1087
|
+
</Window.Resources>
|
|
1088
|
+
|
|
1089
|
+
<Grid IsHitTestVisible="False">
|
|
1090
|
+
<!-- Outer Glowing Sky Blue Aura Border -->
|
|
1091
|
+
<Border x:Name="AuraGlow" BorderThickness="6" BorderBrush="#00d8ff" Margin="0">
|
|
1092
|
+
<Border.Effect>
|
|
1093
|
+
<DropShadowEffect Color="#00e5ff" BlurRadius="45" ShadowDepth="0" Opacity="0.95"/>
|
|
1094
|
+
</Border.Effect>
|
|
1095
|
+
</Border>
|
|
1096
|
+
<Border x:Name="AuraBorder" BorderThickness="3" BorderBrush="#38bdf8" Margin="0">
|
|
1097
|
+
<Border.Effect>
|
|
1098
|
+
<DropShadowEffect Color="#00e5ff" BlurRadius="25" ShadowDepth="0" Opacity="1"/>
|
|
1099
|
+
</Border.Effect>
|
|
1100
|
+
</Border>
|
|
1101
|
+
|
|
1102
|
+
<!-- Sky Blue Corner Brackets with Aura Glow -->
|
|
1103
|
+
<!-- Top Left Corner -->
|
|
1104
|
+
<Canvas HorizontalAlignment="Left" VerticalAlignment="Top" Width="120" Height="120" Margin="6,6,0,0">
|
|
1105
|
+
<Path x:Name="CornerTL" Data="M 0,90 L 0,0 L 90,0" Stroke="#00e5ff" StrokeThickness="6">
|
|
1106
|
+
<Path.Effect>
|
|
1107
|
+
<DropShadowEffect Color="#00e5ff" BlurRadius="30" ShadowDepth="0" Opacity="1"/>
|
|
1108
|
+
</Path.Effect>
|
|
1109
|
+
</Path>
|
|
1110
|
+
</Canvas>
|
|
1111
|
+
|
|
1112
|
+
<!-- Top Right Corner -->
|
|
1113
|
+
<Canvas HorizontalAlignment="Right" VerticalAlignment="Top" Width="120" Height="120" Margin="0,6,6,0">
|
|
1114
|
+
<Path x:Name="CornerTR" Data="M 30,0 L 120,0 L 120,90" Stroke="#00e5ff" StrokeThickness="6">
|
|
1115
|
+
<Path.Effect>
|
|
1116
|
+
<DropShadowEffect Color="#00e5ff" BlurRadius="30" ShadowDepth="0" Opacity="1"/>
|
|
1117
|
+
</Path.Effect>
|
|
1118
|
+
</Path>
|
|
1119
|
+
</Canvas>
|
|
1120
|
+
|
|
1121
|
+
<!-- Bottom Left Corner -->
|
|
1122
|
+
<Canvas HorizontalAlignment="Left" VerticalAlignment="Bottom" Width="120" Height="120" Margin="6,0,0,6">
|
|
1123
|
+
<Path x:Name="CornerBL" Data="M 0,30 L 0,120 L 90,120" Stroke="#00e5ff" StrokeThickness="6">
|
|
1124
|
+
<Path.Effect>
|
|
1125
|
+
<DropShadowEffect Color="#00e5ff" BlurRadius="30" ShadowDepth="0" Opacity="1"/>
|
|
1126
|
+
</Path.Effect>
|
|
1127
|
+
</Path>
|
|
1128
|
+
</Canvas>
|
|
1129
|
+
|
|
1130
|
+
<!-- Bottom Right Corner -->
|
|
1131
|
+
<Canvas HorizontalAlignment="Right" VerticalAlignment="Bottom" Width="120" Height="120" Margin="0,0,6,6">
|
|
1132
|
+
<Path x:Name="CornerBR" Data="M 30,120 L 120,120 L 120,30" Stroke="#00e5ff" StrokeThickness="6">
|
|
1133
|
+
<Path.Effect>
|
|
1134
|
+
<DropShadowEffect Color="#00e5ff" BlurRadius="30" ShadowDepth="0" Opacity="1"/>
|
|
1135
|
+
</Path.Effect>
|
|
1136
|
+
</Path>
|
|
1137
|
+
</Canvas>
|
|
1138
|
+
|
|
1139
|
+
<!-- Top Floating Status Badge -->
|
|
1140
|
+
<Border HorizontalAlignment="Center" VerticalAlignment="Top" Margin="0,22,0,0"
|
|
1141
|
+
Background="#EE03121F" BorderBrush="#00e5ff" BorderThickness="1.8" CornerRadius="24" Padding="26,10">
|
|
1142
|
+
<Border.Effect>
|
|
1143
|
+
<DropShadowEffect Color="#00e5ff" BlurRadius="25" ShadowDepth="0" Opacity="0.95"/>
|
|
1144
|
+
</Border.Effect>
|
|
1145
|
+
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
|
1146
|
+
<Ellipse Width="12" Height="12" Fill="#00e5ff" Margin="0,0,12,0">
|
|
1147
|
+
<Ellipse.Effect>
|
|
1148
|
+
<DropShadowEffect Color="#00e5ff" BlurRadius="12" ShadowDepth="0"/>
|
|
1149
|
+
</Ellipse.Effect>
|
|
1150
|
+
</Ellipse>
|
|
1151
|
+
<TextBlock Text="${message}" Foreground="#ffffff" FontSize="16" FontWeight="Bold" VerticalAlignment="Center"/>
|
|
1152
|
+
<TextBlock Text="${subtitleText}" Foreground="#38bdf8" FontSize="13.5" FontWeight="SemiBold" Margin="10,0,0,0" VerticalAlignment="Center"/>
|
|
1153
|
+
</StackPanel>
|
|
1154
|
+
</Border>
|
|
1155
|
+
</Grid>
|
|
1156
|
+
</Window>
|
|
1157
|
+
"@
|
|
1158
|
+
|
|
1159
|
+
$reader = (New-Object System.Xml.XmlNodeReader $xaml)
|
|
1160
|
+
$window = [System.Windows.Markup.XamlReader]::Load($reader)
|
|
1161
|
+
|
|
1162
|
+
$window.Left = [System.Windows.SystemParameters]::VirtualScreenLeft
|
|
1163
|
+
$window.Top = [System.Windows.SystemParameters]::VirtualScreenTop
|
|
1164
|
+
$window.Width = [System.Windows.SystemParameters]::VirtualScreenWidth
|
|
1165
|
+
$window.Height = [System.Windows.SystemParameters]::VirtualScreenHeight
|
|
1166
|
+
$window.Topmost = $true
|
|
1167
|
+
$window.ShowActivated = $false
|
|
1168
|
+
|
|
1169
|
+
$storyboard = $window.Resources["AuraPulse"]
|
|
1170
|
+
$window.Add_Loaded({
|
|
1171
|
+
$storyboard.Begin($window)
|
|
1172
|
+
})
|
|
1173
|
+
|
|
1174
|
+
# Safety timer to close overlay and guarantee unblock
|
|
1175
|
+
$timer = New-Object System.Windows.Threading.DispatcherTimer
|
|
1176
|
+
$timer.Interval = [TimeSpan]::FromMilliseconds(${durationMs})
|
|
1177
|
+
$timer.Add_Tick({
|
|
1178
|
+
$timer.Stop()
|
|
1179
|
+
${unblockSnippet}
|
|
1180
|
+
$window.Close()
|
|
1181
|
+
})
|
|
1182
|
+
$timer.Start()
|
|
1183
|
+
|
|
1184
|
+
[void]$window.ShowDialog()
|
|
1185
|
+
${unblockSnippet}
|
|
1186
|
+
`;
|
|
1187
|
+
fs.writeFileSync(psFile, overlayPs, 'utf8');
|
|
1188
|
+
await execAsync(`powershell -WindowStyle Hidden -NoProfile -Sta -ExecutionPolicy Bypass -File "${psFile}"`, { timeout: durationMs + 3000 });
|
|
1189
|
+
}
|
|
1190
|
+
catch {
|
|
1191
|
+
if (blockInput) {
|
|
1192
|
+
try {
|
|
1193
|
+
await execAsync(`powershell -NoProfile -Command "$sig='[DllImport(\\"user32.dll\\")] public static extern bool BlockInput(bool f);';$b=Add-Type -m $sig -n B -p;[void]$b::BlockInput($false)"`);
|
|
1194
|
+
}
|
|
1195
|
+
catch { }
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
const capturedScreenshots = new Set();
|
|
1200
|
+
/**
|
|
1201
|
+
* Register a captured screenshot file path for automatic post-task cleanup.
|
|
1202
|
+
*/
|
|
1203
|
+
export function registerCapturedScreenshot(filePath) {
|
|
1204
|
+
if (filePath) {
|
|
1205
|
+
capturedScreenshots.add(path.resolve(filePath));
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
/**
|
|
1209
|
+
* Clean up all captured screenshots permanently from disk after task completion.
|
|
1210
|
+
*/
|
|
1211
|
+
export function cleanupScreenshots() {
|
|
1212
|
+
let deletedCount = 0;
|
|
1213
|
+
// 1. Delete all tracked screenshots
|
|
1214
|
+
for (const filePath of capturedScreenshots) {
|
|
1215
|
+
try {
|
|
1216
|
+
if (fs.existsSync(filePath)) {
|
|
1217
|
+
fs.unlinkSync(filePath);
|
|
1218
|
+
deletedCount++;
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
catch { }
|
|
1222
|
+
}
|
|
1223
|
+
capturedScreenshots.clear();
|
|
1224
|
+
// 2. Clean any remaining files in .ft/screenshots
|
|
1225
|
+
try {
|
|
1226
|
+
const screenshotsDir = path.join(process.cwd(), '.ft', 'screenshots');
|
|
1227
|
+
if (fs.existsSync(screenshotsDir)) {
|
|
1228
|
+
const files = fs.readdirSync(screenshotsDir);
|
|
1229
|
+
for (const file of files) {
|
|
1230
|
+
if (file.startsWith('screen_') && (file.endsWith('.png') || file.endsWith('.jpg'))) {
|
|
1231
|
+
try {
|
|
1232
|
+
fs.unlinkSync(path.join(screenshotsDir, file));
|
|
1233
|
+
deletedCount++;
|
|
1234
|
+
}
|
|
1235
|
+
catch { }
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
catch { }
|
|
1241
|
+
return { success: true, deletedCount };
|
|
1242
|
+
}
|
|
565
1243
|
/**
|
|
566
1244
|
* Capture full desktop screen or specified window screenshot without running shell commands.
|
|
1245
|
+
* Screenshots are tracked and will be deleted automatically upon task completion.
|
|
567
1246
|
*/
|
|
568
1247
|
export async function captureScreen(appNameOrScreen) {
|
|
569
1248
|
const platform = process.platform;
|
|
@@ -577,56 +1256,65 @@ export async function captureScreen(appNameOrScreen) {
|
|
|
577
1256
|
const filename = `screen_${Date.now()}.png`;
|
|
578
1257
|
const outPath = path.join(screenshotsDir, filename);
|
|
579
1258
|
if (platform === 'win32') {
|
|
580
|
-
|
|
1259
|
+
try {
|
|
1260
|
+
const scriptPath = ensureWinCaptureScript();
|
|
1261
|
+
const { stdout } = await execAsync(`powershell -NoProfile -ExecutionPolicy Bypass -File "${scriptPath}" -OutPath "${outPath}"`);
|
|
1262
|
+
const meta = JSON.parse(stdout.trim());
|
|
1263
|
+
if (meta.success && fs.existsSync(outPath) && fs.statSync(outPath).size > 0) {
|
|
1264
|
+
registerCapturedScreenshot(outPath);
|
|
1265
|
+
showScreenAuraOverlay(1800, "Scout is viewing the screen...", false).catch(() => { });
|
|
1266
|
+
const visibleList = Array.isArray(meta.visibleWindows) && meta.visibleWindows.length > 0
|
|
1267
|
+
? `\n- Visible Windows: ${meta.visibleWindows.slice(0, 6).map((w) => `"${w}"`).join(', ')}`
|
|
1268
|
+
: '';
|
|
1269
|
+
return {
|
|
1270
|
+
success: true,
|
|
1271
|
+
output: `Screen Captured Successfully!\n- Screenshot Saved To: ${outPath}\n- Resolution: ${meta.width}x${meta.height}\n- Active Window: "${meta.activeWindow || 'Desktop'}"${visibleList}\n- Seeing Status: Screen is visible and captured. (NOTE: This temporary screenshot will be automatically deleted upon task completion).`,
|
|
1272
|
+
details: { ...meta, outPath },
|
|
1273
|
+
};
|
|
1274
|
+
}
|
|
1275
|
+
throw new Error(meta.error || 'Desktop capture returned false');
|
|
1276
|
+
}
|
|
1277
|
+
catch (err) {
|
|
1278
|
+
// Fallback to Graphics.CopyFromScreen
|
|
1279
|
+
const psScript = `
|
|
581
1280
|
Add-Type -AssemblyName System.Windows.Forms
|
|
582
1281
|
Add-Type -AssemblyName System.Drawing
|
|
583
|
-
|
|
584
1282
|
$screen = [System.Windows.Forms.Screen]::PrimaryScreen
|
|
585
1283
|
$bounds = $screen.Bounds
|
|
586
1284
|
$bmp = New-Object System.Drawing.Bitmap($bounds.Width, $bounds.Height)
|
|
587
1285
|
$graphics = [System.Drawing.Graphics]::FromImage($bmp)
|
|
588
|
-
$graphics.CopyFromScreen($bounds.Location, [System.Drawing.Point]::Empty, $bounds.Size)
|
|
1286
|
+
try { $graphics.CopyFromScreen($bounds.Location, [System.Drawing.Point]::Empty, $bounds.Size) } catch {}
|
|
589
1287
|
$bmp.Save("${outPath.replace(/\\/g, '\\\\')}", [System.Drawing.Imaging.ImageFormat]::Png)
|
|
590
1288
|
$graphics.Dispose()
|
|
591
1289
|
$bmp.Dispose()
|
|
592
|
-
|
|
593
|
-
$sig = '[DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow(); [DllImport("user32.dll")] public static extern int GetWindowText(IntPtr hWnd, System.Text.StringBuilder text, int count);'
|
|
594
|
-
$w = Add-Type -memberDefinition $sig -name 'Win32Window' -namespace Win32Functions -passThru
|
|
595
|
-
$hwnd = $w::GetForegroundWindow()
|
|
596
|
-
$sb = New-Object System.Text.StringBuilder 256
|
|
597
|
-
[void]$w::GetWindowText($hwnd, $sb, 256)
|
|
598
|
-
$activeTitle = $sb.ToString()
|
|
599
|
-
|
|
600
|
-
@{
|
|
601
|
-
savedPath = "${outPath.replace(/\\/g, '\\\\')}"
|
|
602
|
-
width = $bounds.Width
|
|
603
|
-
height = $bounds.Height
|
|
604
|
-
activeWindow = $activeTitle
|
|
605
|
-
} | ConvertTo-Json
|
|
1290
|
+
@{ savedPath = "${outPath.replace(/\\/g, '\\\\')}"; width = $bounds.Width; height = $bounds.Height; activeWindow = "${appNameOrScreen || 'Desktop'}" } | ConvertTo-Json
|
|
606
1291
|
`;
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
1292
|
+
try {
|
|
1293
|
+
const encodedScript = Buffer.from(psScript, 'utf16le').toString('base64');
|
|
1294
|
+
const { stdout } = await execAsync(`powershell -NoProfile -EncodedCommand ${encodedScript}`);
|
|
1295
|
+
const meta = JSON.parse(stdout.trim());
|
|
1296
|
+
registerCapturedScreenshot(outPath);
|
|
1297
|
+
return {
|
|
1298
|
+
success: true,
|
|
1299
|
+
output: `Screen Captured Successfully!\n- Screenshot Saved To: ${outPath}\n- Resolution: ${meta.width}x${meta.height}\n- Active Window: "${meta.activeWindow || 'Desktop'}"\n- Seeing Status: Screen is visible and captured.`,
|
|
1300
|
+
details: { ...meta, outPath },
|
|
1301
|
+
};
|
|
1302
|
+
}
|
|
1303
|
+
catch (fallbackErr) {
|
|
1304
|
+
return {
|
|
1305
|
+
success: false,
|
|
1306
|
+
output: `Failed to capture screen: ${err?.message || fallbackErr?.message || String(err)}`,
|
|
1307
|
+
};
|
|
1308
|
+
}
|
|
622
1309
|
}
|
|
623
1310
|
}
|
|
624
1311
|
else if (platform === 'darwin') {
|
|
625
1312
|
try {
|
|
626
1313
|
await execAsync(`screencapture -x "${outPath}"`);
|
|
1314
|
+
registerCapturedScreenshot(outPath);
|
|
627
1315
|
return {
|
|
628
1316
|
success: true,
|
|
629
|
-
output: `macOS Screen Captured Successfully! Saved To: ${outPath}
|
|
1317
|
+
output: `macOS Screen Captured Successfully! Saved To: ${outPath}\n(NOTE: This temporary screenshot will be automatically deleted upon task completion).`,
|
|
630
1318
|
details: { outPath, platform: 'darwin' },
|
|
631
1319
|
};
|
|
632
1320
|
}
|
|
@@ -640,9 +1328,10 @@ $activeTitle = $sb.ToString()
|
|
|
640
1328
|
else {
|
|
641
1329
|
try {
|
|
642
1330
|
await execAsync(`import -window root "${outPath}" || scrot "${outPath}"`);
|
|
1331
|
+
registerCapturedScreenshot(outPath);
|
|
643
1332
|
return {
|
|
644
1333
|
success: true,
|
|
645
|
-
output: `Linux Screen Captured Successfully! Saved To: ${outPath}
|
|
1334
|
+
output: `Linux Screen Captured Successfully! Saved To: ${outPath}\n(NOTE: This temporary screenshot will be automatically deleted upon task completion).`,
|
|
646
1335
|
details: { outPath, platform: 'linux' },
|
|
647
1336
|
};
|
|
648
1337
|
}
|
|
@@ -655,20 +1344,105 @@ $activeTitle = $sb.ToString()
|
|
|
655
1344
|
}
|
|
656
1345
|
}
|
|
657
1346
|
/**
|
|
658
|
-
*
|
|
1347
|
+
* Call Vision AI with graceful multi-provider fallback.
|
|
1348
|
+
* Tries NVIDIA Vision (meta/llama-3.2-11b-vision-instruct), Gemini (gemini-2.0-flash), OpenAI (gpt-4o-mini), and BYOK.
|
|
659
1349
|
*/
|
|
660
|
-
export async function
|
|
1350
|
+
export async function callVisionAi(outPath, prompt, maxTokens = 350) {
|
|
1351
|
+
if (!outPath || !fs.existsSync(outPath))
|
|
1352
|
+
return null;
|
|
1353
|
+
let imageBase64;
|
|
1354
|
+
try {
|
|
1355
|
+
imageBase64 = fs.readFileSync(outPath).toString('base64');
|
|
1356
|
+
}
|
|
1357
|
+
catch {
|
|
1358
|
+
return null;
|
|
1359
|
+
}
|
|
1360
|
+
const nvidiaKey = process.env.NVIDIA_API_KEY || process.env.NVCF_API_KEY;
|
|
1361
|
+
const geminiKey = process.env.GEMINI_API_KEY;
|
|
1362
|
+
const openaiKey = process.env.OPENAI_API_KEY;
|
|
1363
|
+
const byokKey = process.env.BYOK_API_KEY;
|
|
1364
|
+
const candidates = [];
|
|
1365
|
+
if (nvidiaKey) {
|
|
1366
|
+
candidates.push({ apiKey: nvidiaKey, baseURL: 'https://integrate.api.nvidia.com/v1', model: 'meta/llama-3.2-11b-vision-instruct', timeout: 35000 });
|
|
1367
|
+
}
|
|
1368
|
+
if (geminiKey) {
|
|
1369
|
+
candidates.push({ apiKey: geminiKey, baseURL: 'https://generativelanguage.googleapis.com/v1beta/openai', model: 'gemini-2.0-flash', timeout: 20000 });
|
|
1370
|
+
}
|
|
1371
|
+
if (openaiKey) {
|
|
1372
|
+
candidates.push({ apiKey: openaiKey, model: 'gpt-4o-mini', timeout: 20000 });
|
|
1373
|
+
}
|
|
1374
|
+
if (byokKey) {
|
|
1375
|
+
candidates.push({ apiKey: byokKey, model: 'gpt-4o-mini', timeout: 20000 });
|
|
1376
|
+
}
|
|
1377
|
+
for (const cand of candidates) {
|
|
1378
|
+
try {
|
|
1379
|
+
const client = new OpenAI({ apiKey: cand.apiKey, baseURL: cand.baseURL, timeout: cand.timeout });
|
|
1380
|
+
const resp = await client.chat.completions.create({
|
|
1381
|
+
model: cand.model,
|
|
1382
|
+
messages: [
|
|
1383
|
+
{
|
|
1384
|
+
role: 'user',
|
|
1385
|
+
content: [
|
|
1386
|
+
{ type: 'text', text: prompt },
|
|
1387
|
+
{ type: 'image_url', image_url: { url: `data:image/png;base64,${imageBase64}` } },
|
|
1388
|
+
],
|
|
1389
|
+
},
|
|
1390
|
+
],
|
|
1391
|
+
max_tokens: maxTokens,
|
|
1392
|
+
});
|
|
1393
|
+
const reply = resp.choices[0]?.message?.content?.trim();
|
|
1394
|
+
if (reply) {
|
|
1395
|
+
return reply;
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
catch {
|
|
1399
|
+
continue;
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
return null;
|
|
1403
|
+
}
|
|
1404
|
+
/**
|
|
1405
|
+
* Visual screen analysis. Takes a full-screen screenshot and provides visual context
|
|
1406
|
+
* (including AI Vision inspection if an API key is available).
|
|
1407
|
+
* All temporary screenshots are automatically deleted upon task completion.
|
|
1408
|
+
*/
|
|
1409
|
+
export async function analyzeScreen(appNameOrScreen, targetQuery) {
|
|
661
1410
|
const capture = await captureScreen(appNameOrScreen);
|
|
662
1411
|
const info = await getScreenInfo(appNameOrScreen);
|
|
1412
|
+
let visualAiAnalysis = '';
|
|
1413
|
+
const outPath = capture.details?.outPath;
|
|
1414
|
+
// Optional Vision Model Analysis if screenshot exists and has valid content
|
|
1415
|
+
if (capture.success && outPath && fs.existsSync(outPath)) {
|
|
1416
|
+
try {
|
|
1417
|
+
if (fs.statSync(outPath).size > 0) {
|
|
1418
|
+
const prompt = targetQuery
|
|
1419
|
+
? `Describe what is visible on this screen specifically regarding: "${targetQuery}". Keep it concise, noting visible UI elements, open windows, tabs, buttons, or error messages.`
|
|
1420
|
+
: `Briefly describe what is currently visible on this desktop screen: active window, main content/tabs, buttons, and system state in 2-3 sentences.`;
|
|
1421
|
+
const reply = await callVisionAi(outPath, prompt, 350);
|
|
1422
|
+
if (reply && reply.trim()) {
|
|
1423
|
+
visualAiAnalysis = `\nAI Visual Screen Analysis:\n${reply.trim()}`;
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
catch {
|
|
1428
|
+
// Vision API call is optional; fallback gracefully to system metadata
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
const activeWin = capture.details?.activeWindow || info.details?.activeWindow || appNameOrScreen || 'Active Desktop';
|
|
1432
|
+
const visWins = Array.isArray(capture.details?.visibleWindows) && capture.details.visibleWindows.length > 0
|
|
1433
|
+
? capture.details.visibleWindows
|
|
1434
|
+
: (Array.isArray(info.details?.visibleWindows) ? info.details.visibleWindows : []);
|
|
1435
|
+
const visibleWindowsText = visWins.length > 0
|
|
1436
|
+
? `\nVisible Open Windows:\n${visWins.slice(0, 8).map((w) => ` - "${w}"`).join('\n')}`
|
|
1437
|
+
: '';
|
|
663
1438
|
const report = [
|
|
664
|
-
`=== SCOUT SCRATCHPAD DESKTOP SCREEN
|
|
1439
|
+
`=== SCOUT SCRATCHPAD DESKTOP SCREEN VISUAL INSPECTION ===`,
|
|
665
1440
|
`Target App / Screen Context: "${appNameOrScreen || 'Desktop'}"`,
|
|
666
|
-
`Screen Resolution: ${info.details?.screenWidth || 1920}x${info.details?.screenHeight || 1080}`,
|
|
667
|
-
`Active Window Focus: "${
|
|
668
|
-
`Mouse Cursor Position: (${info.details?.cursorX || 0}, ${info.details?.cursorY || 0})`,
|
|
669
|
-
`Screenshot
|
|
670
|
-
`
|
|
671
|
-
`Visual Analysis: Screen successfully captured and verified. Target screen is ready for precise mouse clicks, cursor positioning, and direct keystrokes without shell command execution.`,
|
|
1441
|
+
`Screen Resolution: ${info.details?.screenWidth || capture.details?.width || 1920}x${info.details?.screenHeight || capture.details?.height || 1080}`,
|
|
1442
|
+
`Active Window Focus: "${activeWin}"${visibleWindowsText}`,
|
|
1443
|
+
`Mouse Cursor Position: (${info.details?.cursorX || capture.details?.cursorX || 0}, ${info.details?.cursorY || capture.details?.cursorY || 0})`,
|
|
1444
|
+
`Screenshot Status: Captured temporarily for visual inspection. (Will be automatically deleted upon task completion)`,
|
|
1445
|
+
`Visual Status: Screen is visible and verified.${visualAiAnalysis}`,
|
|
672
1446
|
].join('\n');
|
|
673
1447
|
return {
|
|
674
1448
|
success: capture.success,
|
|
@@ -683,14 +1457,19 @@ export async function analyzeScreen(appNameOrScreen) {
|
|
|
683
1457
|
*/
|
|
684
1458
|
export async function takeoverControl(appName, durationMs = 3000, x, y) {
|
|
685
1459
|
const { category, name } = normalizeAppName(appName);
|
|
686
|
-
if (name && name !== 'desktop' && name !== 'system') {
|
|
1460
|
+
if (name && name !== 'desktop' && name !== 'system' && category !== 'browser') {
|
|
687
1461
|
await openApp(name);
|
|
688
1462
|
}
|
|
1463
|
+
// Trigger visual sky-blue aura overlay on screen borders & corners with warning badge & input block
|
|
1464
|
+
try {
|
|
1465
|
+
await showScreenAuraOverlay(Math.min(5000, Math.max(800, durationMs)), "Scout is on the screen.", true);
|
|
1466
|
+
}
|
|
1467
|
+
catch { }
|
|
689
1468
|
const mouseRes = await moveAndClickMouse(x, y, 'left', name || appName);
|
|
690
1469
|
const lockRes = await lockAppInput(name || appName, durationMs);
|
|
691
1470
|
return {
|
|
692
1471
|
success: lockRes.success && mouseRes.success,
|
|
693
|
-
output: `TAKEOVER EXECUTED: Scout Agent has taken over mouse cursor, window focus, and keyboard control on specified screen window ("${name || appName}").
|
|
1472
|
+
output: `TAKEOVER EXECUTED: Scout Agent has taken over mouse cursor, window focus, and keyboard control on specified screen window ("${name || appName}"). Sky-blue aura HUD activated and user interruption locked for ${durationMs}ms. ${mouseRes.output} Agent can view, analyze, type, and click without shell commands.`,
|
|
694
1473
|
details: { appName: name || appName, durationMs, x, y, mouseOutput: mouseRes.output },
|
|
695
1474
|
};
|
|
696
1475
|
}
|
|
@@ -728,59 +1507,101 @@ export async function sendKeystrokes(appName, textToType, pressEnter = true, key
|
|
|
728
1507
|
const sendKeysText = sanitized.replace(/[{}+^~%()\[\]]/g, '{$&}');
|
|
729
1508
|
let sendKeysCombo = '';
|
|
730
1509
|
if (combo) {
|
|
731
|
-
|
|
1510
|
+
const cLower = combo.toLowerCase().replace(/\s+/g, '');
|
|
1511
|
+
if (cLower.includes('ctrl+enter') || cLower.includes('ctrl+return'))
|
|
1512
|
+
sendKeysCombo = '^{ENTER}';
|
|
1513
|
+
else if (cLower.includes('alt+enter') || cLower.includes('alt+return'))
|
|
1514
|
+
sendKeysCombo = '%{ENTER}';
|
|
1515
|
+
else if (cLower.includes('shift+enter') || cLower.includes('shift+return'))
|
|
1516
|
+
sendKeysCombo = '+{ENTER}';
|
|
1517
|
+
else if (cLower.includes('ctrl+a') || cLower === 'select_all')
|
|
732
1518
|
sendKeysCombo = '^a';
|
|
733
|
-
else if (
|
|
1519
|
+
else if (cLower.includes('ctrl+c') || cLower === 'copy')
|
|
734
1520
|
sendKeysCombo = '^c';
|
|
735
|
-
else if (
|
|
1521
|
+
else if (cLower.includes('ctrl+v') || cLower === 'paste')
|
|
736
1522
|
sendKeysCombo = '^v';
|
|
737
|
-
else if (
|
|
1523
|
+
else if (cLower.includes('ctrl+x') || cLower === 'cut')
|
|
1524
|
+
sendKeysCombo = '^x';
|
|
1525
|
+
else if (cLower.includes('ctrl+z') || cLower === 'undo')
|
|
738
1526
|
sendKeysCombo = '^z';
|
|
739
|
-
else if (
|
|
1527
|
+
else if (cLower.includes('ctrl+s') || cLower === 'save')
|
|
740
1528
|
sendKeysCombo = '^s';
|
|
741
|
-
else if (
|
|
1529
|
+
else if (cLower.includes('ctrl+l') || cLower === 'address_bar')
|
|
742
1530
|
sendKeysCombo = '^l';
|
|
743
|
-
else if (
|
|
1531
|
+
else if (cLower.includes('ctrl+t') || cLower === 'new_tab')
|
|
1532
|
+
sendKeysCombo = '^t';
|
|
1533
|
+
else if (cLower.includes('ctrl+w') || cLower === 'close_tab')
|
|
1534
|
+
sendKeysCombo = '^w';
|
|
1535
|
+
else if (cLower.includes('ctrl+r') || cLower === 'reload')
|
|
1536
|
+
sendKeysCombo = '^r';
|
|
1537
|
+
else if (cLower.includes('ctrl+f') || cLower === 'find')
|
|
1538
|
+
sendKeysCombo = '^f';
|
|
1539
|
+
else if (cLower.includes('ctrl+n') || cLower === 'new_window')
|
|
1540
|
+
sendKeysCombo = '^n';
|
|
1541
|
+
else if (cLower.includes('ctrl+shift+i') || cLower.includes('devtools'))
|
|
1542
|
+
sendKeysCombo = '^+i';
|
|
1543
|
+
else if (cLower.includes('alt+d'))
|
|
744
1544
|
sendKeysCombo = '%d';
|
|
745
|
-
else if (
|
|
1545
|
+
else if (cLower.includes('alt+tab'))
|
|
746
1546
|
sendKeysCombo = '%{TAB}';
|
|
747
|
-
else if (
|
|
1547
|
+
else if (cLower.includes('alt+f4'))
|
|
748
1548
|
sendKeysCombo = '%{F4}';
|
|
749
|
-
else if (
|
|
750
|
-
sendKeysCombo = '
|
|
751
|
-
else if (
|
|
1549
|
+
else if (cLower === 'enter' || cLower === 'return')
|
|
1550
|
+
sendKeysCombo = '{ENTER}';
|
|
1551
|
+
else if (cLower === 'esc' || cLower === 'escape')
|
|
752
1552
|
sendKeysCombo = '{ESC}';
|
|
753
|
-
else if (
|
|
1553
|
+
else if (cLower === 'tab')
|
|
754
1554
|
sendKeysCombo = '{TAB}';
|
|
755
|
-
else if (
|
|
1555
|
+
else if (cLower === 'backspace')
|
|
756
1556
|
sendKeysCombo = '{BACKSPACE}';
|
|
757
|
-
else if (
|
|
1557
|
+
else if (cLower === 'space')
|
|
1558
|
+
sendKeysCombo = ' ';
|
|
1559
|
+
else if (cLower === 'up')
|
|
758
1560
|
sendKeysCombo = '{UP}';
|
|
759
|
-
else if (
|
|
1561
|
+
else if (cLower === 'down')
|
|
760
1562
|
sendKeysCombo = '{DOWN}';
|
|
761
|
-
else if (
|
|
1563
|
+
else if (cLower === 'left')
|
|
762
1564
|
sendKeysCombo = '{LEFT}';
|
|
763
|
-
else if (
|
|
1565
|
+
else if (cLower === 'right')
|
|
764
1566
|
sendKeysCombo = '{RIGHT}';
|
|
765
1567
|
else
|
|
766
1568
|
sendKeysCombo = combo;
|
|
767
1569
|
}
|
|
768
1570
|
const psScript = `
|
|
769
|
-
$sig = '
|
|
770
|
-
|
|
1571
|
+
$sig = @'
|
|
1572
|
+
[DllImport("user32.dll")] public static extern bool BlockInput(bool fBlock);
|
|
1573
|
+
[DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr hWnd);
|
|
1574
|
+
[DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
|
|
1575
|
+
[DllImport("user32.dll")] public static extern void keybd_event(byte bVk, byte bScan, uint dwFlags, UIntPtr dwExtraInfo);
|
|
1576
|
+
'@
|
|
1577
|
+
$b = Add-Type -memberDefinition $sig -name ('Win32Block_' + (Get-Random)) -namespace Win32Functions -passThru
|
|
771
1578
|
|
|
772
1579
|
try { [void]$b::BlockInput($true) } catch {}
|
|
773
1580
|
|
|
774
1581
|
Add-Type -AssemblyName System.Windows.Forms
|
|
775
1582
|
$ws = New-Object -ComObject WScript.Shell
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
if (
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
1583
|
+
|
|
1584
|
+
try {
|
|
1585
|
+
$proc = Get-Process -Name chrome, msedge, firefox, brave, opera, "${appName}" -ErrorAction SilentlyContinue | Where-Object { $_.MainWindowHandle -ne 0 } | Select-Object -First 1
|
|
1586
|
+
if ($proc) {
|
|
1587
|
+
$b::keybd_event(0x12, 0, 0, [UIntPtr]::Zero)
|
|
1588
|
+
$b::keybd_event(0x12, 0, 2, [UIntPtr]::Zero)
|
|
1589
|
+
[void]$b::ShowWindow($proc.MainWindowHandle, 9)
|
|
1590
|
+
[void]$b::SetForegroundWindow($proc.MainWindowHandle)
|
|
1591
|
+
Start-Sleep -Milliseconds 250
|
|
1592
|
+
} else {
|
|
1593
|
+
$activated = $ws.AppActivate("${appName}")
|
|
1594
|
+
if (-not $activated) { $activated = $ws.AppActivate("Chrome") }
|
|
1595
|
+
if (-not $activated) { $activated = $ws.AppActivate("Edge") }
|
|
1596
|
+
if (-not $activated) { $activated = $ws.AppActivate("Firefox") }
|
|
1597
|
+
if (-not $activated) { $activated = $ws.AppActivate("Brave") }
|
|
1598
|
+
if (-not $activated) { $activated = $ws.AppActivate("Notepad") }
|
|
1599
|
+
if (-not $activated) { $activated = $ws.AppActivate("Visual Studio Code") }
|
|
1600
|
+
Start-Sleep -Milliseconds 300
|
|
1601
|
+
}
|
|
1602
|
+
} catch {}
|
|
1603
|
+
|
|
1604
|
+
Start-Sleep -Milliseconds 250
|
|
784
1605
|
|
|
785
1606
|
${sendKeysCombo ? `$ws.SendKeys("${sendKeysCombo}")` : ''}
|
|
786
1607
|
|
|
@@ -875,9 +1696,20 @@ Add-Type -AssemblyName System.Windows.Forms
|
|
|
875
1696
|
$sig = '[DllImport("user32.dll")] public static extern void mouse_event(int dwFlags, int dx, int dy, int cButtons, int dwExtraInfo); [DllImport("user32.dll")] public static extern bool SetCursorPos(int X, int Y);'
|
|
876
1697
|
$m = Add-Type -memberDefinition $sig -name 'Win32Mouse' -namespace Win32Functions -passThru
|
|
877
1698
|
|
|
878
|
-
${appName && appName !== 'desktop' && appName !== 'system' ? `
|
|
879
|
-
|
|
880
|
-
$
|
|
1699
|
+
${(appName && appName !== 'desktop' && appName !== 'system') ? `
|
|
1700
|
+
try {
|
|
1701
|
+
$proc = Get-Process -Name chrome, msedge, firefox, brave, "${appName}" -ErrorAction SilentlyContinue | Where-Object { $_.MainWindowHandle -ne 0 } | Select-Object -First 1
|
|
1702
|
+
if ($proc) {
|
|
1703
|
+
$sigFw = '[DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr hWnd);'
|
|
1704
|
+
$fw = Add-Type -memberDefinition $sigFw -name 'Win32FwMouse' -namespace Win32Functions -passThru
|
|
1705
|
+
[void]$fw::SetForegroundWindow($proc.MainWindowHandle)
|
|
1706
|
+
Start-Sleep -Milliseconds 250
|
|
1707
|
+
} else {
|
|
1708
|
+
$ws = New-Object -ComObject WScript.Shell
|
|
1709
|
+
[void]$ws.AppActivate("${appName}")
|
|
1710
|
+
Start-Sleep -Milliseconds 200
|
|
1711
|
+
}
|
|
1712
|
+
} catch {}
|
|
881
1713
|
` : ''}
|
|
882
1714
|
|
|
883
1715
|
$screen = [System.Windows.Forms.Screen]::PrimaryScreen
|
|
@@ -1016,4 +1848,190 @@ end tell
|
|
|
1016
1848
|
};
|
|
1017
1849
|
}
|
|
1018
1850
|
}
|
|
1851
|
+
/**
|
|
1852
|
+
* List all active top-level application windows.
|
|
1853
|
+
*/
|
|
1854
|
+
export async function listWindows() {
|
|
1855
|
+
const platform = process.platform;
|
|
1856
|
+
if (platform === 'win32') {
|
|
1857
|
+
const psScript = `
|
|
1858
|
+
Get-Process | Where-Object { $_.MainWindowTitle -ne '' } | Select-Object Id, ProcessName, MainWindowTitle, MainWindowHandle | ConvertTo-Json
|
|
1859
|
+
`;
|
|
1860
|
+
try {
|
|
1861
|
+
const encodedScript = Buffer.from(psScript, 'utf16le').toString('base64');
|
|
1862
|
+
const { stdout } = await execAsync(`powershell -NoProfile -Sta -EncodedCommand ${encodedScript}`);
|
|
1863
|
+
let rawList = [];
|
|
1864
|
+
try {
|
|
1865
|
+
rawList = JSON.parse(stdout.trim());
|
|
1866
|
+
}
|
|
1867
|
+
catch { }
|
|
1868
|
+
if (!Array.isArray(rawList))
|
|
1869
|
+
rawList = rawList ? [rawList] : [];
|
|
1870
|
+
const windows = rawList.map((w) => ({
|
|
1871
|
+
title: String(w.MainWindowTitle || ''),
|
|
1872
|
+
processName: String(w.ProcessName || ''),
|
|
1873
|
+
handle: String(w.MainWindowHandle || ''),
|
|
1874
|
+
}));
|
|
1875
|
+
const formatted = windows.map((w, idx) => ` ${idx + 1}. [${w.processName}] "${w.title}"`).join('\n');
|
|
1876
|
+
return {
|
|
1877
|
+
success: true,
|
|
1878
|
+
output: `Active Application Windows (${windows.length}):\n${formatted || 'No active top-level windows with title found.'}`,
|
|
1879
|
+
windows,
|
|
1880
|
+
};
|
|
1881
|
+
}
|
|
1882
|
+
catch (err) {
|
|
1883
|
+
return { success: false, output: `Failed to list windows: ${err?.message || String(err)}`, windows: [] };
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
return { success: true, output: 'Window listing only supported on Windows.', windows: [] };
|
|
1887
|
+
}
|
|
1888
|
+
/**
|
|
1889
|
+
* Bring a target window to front and focus it.
|
|
1890
|
+
*/
|
|
1891
|
+
export async function focusWindow(appNameOrTitle) {
|
|
1892
|
+
const platform = process.platform;
|
|
1893
|
+
if (platform === 'win32') {
|
|
1894
|
+
const psScript = `
|
|
1895
|
+
$target = "${appNameOrTitle.replace(/"/g, '`"')}"
|
|
1896
|
+
$proc = Get-Process | Where-Object { ($_.MainWindowTitle -like "*$target*" -or $_.ProcessName -like "*$target*") -and $_.MainWindowHandle -ne 0 } | Select-Object -First 1
|
|
1897
|
+
if ($proc) {
|
|
1898
|
+
$sig = '[DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr hWnd); [DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);'
|
|
1899
|
+
$w = Add-Type -memberDefinition $sig -name 'Win32Fw' -namespace Win32Functions -passThru
|
|
1900
|
+
[void]$w::ShowWindow($proc.MainWindowHandle, 9)
|
|
1901
|
+
[void]$w::SetForegroundWindow($proc.MainWindowHandle)
|
|
1902
|
+
Write-Output "Focused '$($proc.MainWindowTitle)' ($($proc.ProcessName))"
|
|
1903
|
+
} else {
|
|
1904
|
+
$ws = New-Object -ComObject WScript.Shell
|
|
1905
|
+
$activated = $ws.AppActivate($target)
|
|
1906
|
+
if ($activated) { Write-Output "Activated '$target' via WScript.Shell" } else { Write-Output "Window '$target' not found" }
|
|
1907
|
+
}
|
|
1908
|
+
`;
|
|
1909
|
+
try {
|
|
1910
|
+
const encodedScript = Buffer.from(psScript, 'utf16le').toString('base64');
|
|
1911
|
+
const { stdout } = await execAsync(`powershell -NoProfile -Sta -EncodedCommand ${encodedScript}`);
|
|
1912
|
+
return { success: true, output: stdout.trim() || `Focus request completed for "${appNameOrTitle}".` };
|
|
1913
|
+
}
|
|
1914
|
+
catch (err) {
|
|
1915
|
+
return { success: false, output: `Could not focus window: ${err?.message || String(err)}` };
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1918
|
+
return { success: true, output: `Focus request sent for "${appNameOrTitle}".` };
|
|
1919
|
+
}
|
|
1920
|
+
/**
|
|
1921
|
+
* Scroll mouse wheel up or down by specified notch amount.
|
|
1922
|
+
*/
|
|
1923
|
+
export async function scrollWindow(direction = 'down', amount = 3) {
|
|
1924
|
+
const platform = process.platform;
|
|
1925
|
+
if (platform === 'win32') {
|
|
1926
|
+
const clicks = Math.max(1, Math.min(20, amount));
|
|
1927
|
+
const delta = direction === 'up' ? 120 * clicks : -120 * clicks;
|
|
1928
|
+
const psScript = `
|
|
1929
|
+
Add-Type -AssemblyName System.Windows.Forms
|
|
1930
|
+
$sig = '[DllImport("user32.dll")] public static extern void mouse_event(int dwFlags, int dx, int dy, int cButtons, int dwExtraInfo);'
|
|
1931
|
+
$m = Add-Type -memberDefinition $sig -name 'Win32Scroll' -namespace Win32Functions -passThru
|
|
1932
|
+
$m::mouse_event(0x0800, 0, 0, ${delta}, 0)
|
|
1933
|
+
`;
|
|
1934
|
+
try {
|
|
1935
|
+
const encodedScript = Buffer.from(psScript, 'utf16le').toString('base64');
|
|
1936
|
+
await execAsync(`powershell -NoProfile -EncodedCommand ${encodedScript}`);
|
|
1937
|
+
return { success: true, output: `Scrolled mouse wheel ${direction} by ${clicks} notch(es).` };
|
|
1938
|
+
}
|
|
1939
|
+
catch (err) {
|
|
1940
|
+
return { success: false, output: `Failed to scroll: ${err?.message || String(err)}` };
|
|
1941
|
+
}
|
|
1942
|
+
}
|
|
1943
|
+
return { success: true, output: `Scrolled ${direction}` };
|
|
1944
|
+
}
|
|
1945
|
+
/**
|
|
1946
|
+
* Visually ground and click a target UI element by name or visual description.
|
|
1947
|
+
*/
|
|
1948
|
+
export async function findAndClickElement(elementQuery, appName, button = 'left') {
|
|
1949
|
+
const capture = await captureScreen(appName);
|
|
1950
|
+
if (!capture.success || !capture.details?.outPath) {
|
|
1951
|
+
return { success: false, output: `Could not capture screen to locate element: ${capture.output}` };
|
|
1952
|
+
}
|
|
1953
|
+
const screenWidth = capture.details.width || 1920;
|
|
1954
|
+
const screenHeight = capture.details.height || 1080;
|
|
1955
|
+
const outPath = capture.details.outPath;
|
|
1956
|
+
let targetX = undefined;
|
|
1957
|
+
let targetY = undefined;
|
|
1958
|
+
if (fs.existsSync(outPath)) {
|
|
1959
|
+
try {
|
|
1960
|
+
const prompt = `Look at this screenshot and locate the UI element: "${elementQuery}".
|
|
1961
|
+
Return ONLY a valid JSON object with the center point of this element in 0-1000 normalized coordinates:
|
|
1962
|
+
{"point": [y, x]}
|
|
1963
|
+
Where y is between 0 and 1000 (top to bottom) and x is between 0 and 1000 (left to right).
|
|
1964
|
+
If not found, return {"point": null}. Output nothing else.`;
|
|
1965
|
+
const reply = await callVisionAi(outPath, prompt, 80);
|
|
1966
|
+
if (reply) {
|
|
1967
|
+
const match = reply.match(/\{[\s\S]*"point"[\s\S]*\}/);
|
|
1968
|
+
if (match) {
|
|
1969
|
+
const parsed = JSON.parse(match[0]);
|
|
1970
|
+
if (Array.isArray(parsed.point) && parsed.point.length === 2) {
|
|
1971
|
+
const normY = Number(parsed.point[0]);
|
|
1972
|
+
const normX = Number(parsed.point[1]);
|
|
1973
|
+
if (!isNaN(normX) && !isNaN(normY)) {
|
|
1974
|
+
targetX = Math.round((normX / 1000) * screenWidth);
|
|
1975
|
+
targetY = Math.round((normY / 1000) * screenHeight);
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
catch { }
|
|
1982
|
+
}
|
|
1983
|
+
if ((targetX === undefined || targetY === undefined) && process.platform === 'win32') {
|
|
1984
|
+
const psScript = `
|
|
1985
|
+
Add-Type -AssemblyName UIAutomationClient
|
|
1986
|
+
Add-Type -AssemblyName UIAutomationTypes
|
|
1987
|
+
$target = "${elementQuery.replace(/"/g, '""')}"
|
|
1988
|
+
$root = [System.Windows.Automation.AutomationElement]::RootElement
|
|
1989
|
+
|
|
1990
|
+
$condName = New-Object System.Windows.Automation.PropertyCondition([System.Windows.Automation.AutomationElement]::NameProperty, $target)
|
|
1991
|
+
$el = $root.FindFirst([System.Windows.Automation.TreeScope]::Descendants, $condName)
|
|
1992
|
+
|
|
1993
|
+
if (-not $el) {
|
|
1994
|
+
$all = $root.FindAll([System.Windows.Automation.TreeScope]::Descendants, [System.Windows.Automation.Condition]::TrueCondition)
|
|
1995
|
+
foreach ($item in $all) {
|
|
1996
|
+
if ($item.Current.Name -and $item.Current.Name.ToLower().Contains($target.ToLower())) {
|
|
1997
|
+
$el = $item
|
|
1998
|
+
break
|
|
1999
|
+
}
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
|
|
2003
|
+
if ($el) {
|
|
2004
|
+
$rect = $el.Current.BoundingRectangle
|
|
2005
|
+
if ($rect -and $rect.Width -gt 0 -and $rect.Height -gt 0) {
|
|
2006
|
+
$cx = [int]($rect.X + ($rect.Width / 2))
|
|
2007
|
+
$cy = [int]($rect.Y + ($rect.Height / 2))
|
|
2008
|
+
@{ x = $cx; y = $cy; name = $el.Current.Name } | ConvertTo-Json
|
|
2009
|
+
}
|
|
2010
|
+
}
|
|
2011
|
+
`;
|
|
2012
|
+
try {
|
|
2013
|
+
const { stdout } = await execAsync(`powershell -NoProfile -Command "${psScript.replace(/\r?\n/g, ' ')}"`);
|
|
2014
|
+
if (stdout.trim()) {
|
|
2015
|
+
const uia = JSON.parse(stdout.trim());
|
|
2016
|
+
if (uia.x && uia.y) {
|
|
2017
|
+
targetX = uia.x;
|
|
2018
|
+
targetY = uia.y;
|
|
2019
|
+
}
|
|
2020
|
+
}
|
|
2021
|
+
}
|
|
2022
|
+
catch { }
|
|
2023
|
+
}
|
|
2024
|
+
if (targetX !== undefined && targetY !== undefined) {
|
|
2025
|
+
const clickRes = await moveAndClickMouse(targetX, targetY, button, appName);
|
|
2026
|
+
return {
|
|
2027
|
+
success: clickRes.success,
|
|
2028
|
+
output: `Visual Grounding Success: Located "${elementQuery}" at screen coordinates (${targetX}, ${targetY}). ${clickRes.output}`,
|
|
2029
|
+
coordinates: { x: targetX, y: targetY },
|
|
2030
|
+
};
|
|
2031
|
+
}
|
|
2032
|
+
return {
|
|
2033
|
+
success: false,
|
|
2034
|
+
output: `Could not visually or structurally locate UI element "${elementQuery}" on screen. Try calling analyze_screen to view current screen contents, or provide explicit (x, y) coordinates.`,
|
|
2035
|
+
};
|
|
2036
|
+
}
|
|
1019
2037
|
//# sourceMappingURL=appControl.js.map
|