ft-scout 8.0.2 → 8.0.4
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 +6 -6
- package/README.md +29 -13
- package/bin/src/commands/agent.d.ts.map +1 -1
- package/bin/src/commands/agent.js +147 -27
- 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 +88 -7
- 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 +807 -150
- package/bin/src/engine/appControl.js.map +1 -1
- package/bin/src/engine/liveScreenEngine.d.ts.map +1 -0
- package/bin/src/engine/liveScreenEngine.js +390 -0
- package/bin/src/engine/liveScreenEngine.js.map +1 -0
- package/bin/src/engine/scripts/winCapture.ps1 +248 -0
- package/bin/src/engine/voiceEngine.d.ts.map +1 -1
- package/bin/src/engine/voiceEngine.js +1 -0
- package/bin/src/engine/voiceEngine.js.map +1 -1
- package/bin/src/index.js +5 -4
- package/bin/src/index.js.map +1 -1
- package/bin/src/utils/branding.js +1 -1
- package/firebase.json +20 -6
- package/package.json +1 -1
- package/web/app.js +13 -4
- package/web/styles.css +96 -1
|
@@ -2,11 +2,12 @@ import fs from 'fs';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import http from 'http';
|
|
4
4
|
import https from 'https';
|
|
5
|
-
import { exec } from 'child_process';
|
|
5
|
+
import { exec, spawn, ChildProcess } from 'child_process';
|
|
6
6
|
import { promisify } from 'util';
|
|
7
7
|
import OpenAI from 'openai';
|
|
8
8
|
import dotenv from 'dotenv';
|
|
9
9
|
import chalk from 'chalk';
|
|
10
|
+
import { startLiveScreenShare, stopLiveScreenShare, isLiveScreenShareActive, getLiveScreenFrame, analyzeLiveScreen, callVisionAiWithFrame, setLiveScreenTargetApp, getLiveScreenTargetApp, } from './liveScreenEngine.js';
|
|
10
11
|
dotenv.config({ quiet: true });
|
|
11
12
|
const execAsync = promisify(exec);
|
|
12
13
|
/**
|
|
@@ -23,6 +24,9 @@ export function normalizeAppName(rawAppName) {
|
|
|
23
24
|
if (['code', 'vscode', 'visual-studio-code', 'cursor', 'zed', 'sublime', 'notepad', 'notepad++', 'vim', 'nvim', 'nano', 'editor'].includes(appLower)) {
|
|
24
25
|
return { category: 'editor', name: appLower || 'editor' };
|
|
25
26
|
}
|
|
27
|
+
if (['game', 'games', 'gaming', 'play', 'launcher', 'steam', 'epic', 'epicgames', 'riot', 'battlenet', 'ea', 'ubisoft', 'gog'].includes(appLower) || appLower.startsWith('game:')) {
|
|
28
|
+
return { category: 'game', name: appLower };
|
|
29
|
+
}
|
|
26
30
|
return { category: 'system', name: appLower };
|
|
27
31
|
}
|
|
28
32
|
export function parsePayload(payload) {
|
|
@@ -102,12 +106,87 @@ export function buildSocialUrl(platformOrAppName, target, text) {
|
|
|
102
106
|
}
|
|
103
107
|
return 'https://www.instagram.com/direct/inbox/';
|
|
104
108
|
}
|
|
109
|
+
/**
|
|
110
|
+
* Universally search and locate any installed app, game, or store package on the host OS without hardcoding.
|
|
111
|
+
* Automatically resolves Win32 executables, Microsoft Store games, Steam games, and desktop shortcuts.
|
|
112
|
+
*/
|
|
113
|
+
export async function findInstalledApp(query) {
|
|
114
|
+
const cleanQuery = (query || '').replace(/[^\w\s-]/g, '').trim().toLowerCase();
|
|
115
|
+
if (!cleanQuery)
|
|
116
|
+
return null;
|
|
117
|
+
const platform = process.platform;
|
|
118
|
+
if (platform === 'win32') {
|
|
119
|
+
try {
|
|
120
|
+
// Query Windows Get-StartApps with wildcard matching
|
|
121
|
+
const safePattern = `*${cleanQuery.replace(/[*_']/g, '')}*`;
|
|
122
|
+
const psCmd = `powershell.exe -NoProfile -NonInteractive -Command "Get-StartApps -Name '${safePattern}' | Select-Object -First 1 Name, AppID | ConvertTo-Json"`;
|
|
123
|
+
const { stdout } = await execAsync(psCmd, { timeout: 3500 });
|
|
124
|
+
if (stdout && stdout.trim()) {
|
|
125
|
+
const parsed = JSON.parse(stdout.trim());
|
|
126
|
+
if (parsed && (parsed.Name || parsed.AppID)) {
|
|
127
|
+
return {
|
|
128
|
+
name: String(parsed.Name || query),
|
|
129
|
+
appId: String(parsed.AppID || ''),
|
|
130
|
+
source: 'start_apps',
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
catch { }
|
|
136
|
+
// Fallback: check registered URI scheme protocol (e.g. steam, roblox, spotify, discord, minecraft, riot)
|
|
137
|
+
if (!cleanQuery.includes(' ') && cleanQuery.length >= 3) {
|
|
138
|
+
try {
|
|
139
|
+
const checkReg = `powershell.exe -NoProfile -NonInteractive -Command "Test-Path 'Registry::HKEY_CLASSES_ROOT\\${cleanQuery}'"`;
|
|
140
|
+
const { stdout } = await execAsync(checkReg, { timeout: 1500 });
|
|
141
|
+
if (stdout && stdout.trim().toLowerCase() === 'true') {
|
|
142
|
+
return {
|
|
143
|
+
name: query,
|
|
144
|
+
appId: `${cleanQuery}:`,
|
|
145
|
+
source: 'protocol',
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
catch { }
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
else if (platform === 'darwin') {
|
|
153
|
+
try {
|
|
154
|
+
const { stdout } = await execAsync(`mdfind "kMDItemKind == 'Application' && kMDItemDisplayName == '*${cleanQuery}*'c" | head -n 1`, { timeout: 3000 });
|
|
155
|
+
if (stdout && stdout.trim()) {
|
|
156
|
+
return {
|
|
157
|
+
name: path.basename(stdout.trim(), '.app'),
|
|
158
|
+
appId: stdout.trim(),
|
|
159
|
+
source: 'path',
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
catch { }
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
// Linux: check which or desktop application entries
|
|
167
|
+
try {
|
|
168
|
+
const { stdout } = await execAsync(`which "${cleanQuery}" || find /usr/share/applications -name "*${cleanQuery}*.desktop" | head -n 1`, { timeout: 2500 });
|
|
169
|
+
if (stdout && stdout.trim()) {
|
|
170
|
+
return {
|
|
171
|
+
name: query,
|
|
172
|
+
appId: stdout.trim(),
|
|
173
|
+
source: 'path',
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
catch { }
|
|
178
|
+
}
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
105
181
|
/**
|
|
106
182
|
* Cross-platform app opener.
|
|
107
183
|
* Launches and opens target application on Windows, macOS, or Linux.
|
|
108
184
|
*/
|
|
109
185
|
export async function openApp(appNameOrPath, target, options) {
|
|
110
186
|
const { category, name } = normalizeAppName(appNameOrPath);
|
|
187
|
+
if (name && name !== 'desktop' && name !== 'system') {
|
|
188
|
+
setLiveScreenTargetApp(name);
|
|
189
|
+
}
|
|
111
190
|
const platform = process.platform;
|
|
112
191
|
let command = '';
|
|
113
192
|
const cleanTarget = target ? target.trim() : '';
|
|
@@ -212,16 +291,42 @@ export async function openApp(appNameOrPath, target, options) {
|
|
|
212
291
|
}
|
|
213
292
|
}
|
|
214
293
|
else {
|
|
215
|
-
//
|
|
216
|
-
const
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
294
|
+
// Universal Application, Game & Desktop Launcher (Zero-Hardcode OS Discovery)
|
|
295
|
+
const targetQuery = name || appNameOrPath;
|
|
296
|
+
const installedApp = await findInstalledApp(targetQuery);
|
|
297
|
+
if (installedApp) {
|
|
298
|
+
if (platform === 'win32') {
|
|
299
|
+
if (installedApp.source === 'protocol') {
|
|
300
|
+
const extraArg = cleanTarget ? ` "${cleanTarget}"` : '';
|
|
301
|
+
command = `start ${installedApp.appId}${extraArg}`;
|
|
302
|
+
}
|
|
303
|
+
else {
|
|
304
|
+
command = `explorer.exe "shell:AppsFolder\\${installedApp.appId}"`;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
else if (platform === 'darwin') {
|
|
308
|
+
command = `open -a "${installedApp.appId || installedApp.name}"`;
|
|
309
|
+
}
|
|
310
|
+
else {
|
|
311
|
+
command = `"${installedApp.appId || installedApp.name}" &`;
|
|
312
|
+
}
|
|
222
313
|
}
|
|
223
314
|
else {
|
|
224
|
-
|
|
315
|
+
// Protocol scheme check (e.g. steam:, roblox:, spotify:, discord:, minecraft:, etc.)
|
|
316
|
+
const isProtocol = /^[a-zA-Z0-9_\-]+:/.test(appNameOrPath) || (cleanTarget && /^[a-zA-Z0-9_\-]+:/.test(cleanTarget));
|
|
317
|
+
if (isProtocol && platform === 'win32') {
|
|
318
|
+
const proto = /^[a-zA-Z0-9_\-]+:/.test(appNameOrPath) ? appNameOrPath : cleanTarget;
|
|
319
|
+
command = `start ${proto}`;
|
|
320
|
+
}
|
|
321
|
+
else if (platform === 'win32') {
|
|
322
|
+
command = cleanTarget ? `start "" "${appNameOrPath}" ${cleanTarget}` : `start "" "${appNameOrPath}"`;
|
|
323
|
+
}
|
|
324
|
+
else if (platform === 'darwin') {
|
|
325
|
+
command = cleanTarget ? `open -a "${appNameOrPath}" "${cleanTarget}"` : `open -a "${appNameOrPath}"`;
|
|
326
|
+
}
|
|
327
|
+
else {
|
|
328
|
+
command = cleanTarget ? `${appNameOrPath} "${cleanTarget}" &` : `${appNameOrPath} &`;
|
|
329
|
+
}
|
|
225
330
|
}
|
|
226
331
|
}
|
|
227
332
|
try {
|
|
@@ -251,6 +356,10 @@ export async function openApp(appNameOrPath, target, options) {
|
|
|
251
356
|
export async function executeInApp(appName, action, payload) {
|
|
252
357
|
const { category, name } = normalizeAppName(appName);
|
|
253
358
|
const actLower = (action || '').trim().toLowerCase();
|
|
359
|
+
// Focus live screen sharing on the active app being operated
|
|
360
|
+
if (name && name !== 'desktop' && name !== 'system') {
|
|
361
|
+
setLiveScreenTargetApp(name);
|
|
362
|
+
}
|
|
254
363
|
// Universal Scratchpad Desktop Automation Actions (Takeover, Screen Seeing, Keystrokes, Mouse Clicks, Focus Locks)
|
|
255
364
|
if (['takeover', 'takeover_control', 'takeover_screen', 'takeover_app'].includes(actLower)) {
|
|
256
365
|
const parsed = parsePayload(payload);
|
|
@@ -275,7 +384,38 @@ export async function executeInApp(appName, action, payload) {
|
|
|
275
384
|
output: res.output,
|
|
276
385
|
};
|
|
277
386
|
}
|
|
387
|
+
if (['start_screen_share', 'live_screen_share', 'screen_share', 'start_live_stream'].includes(actLower)) {
|
|
388
|
+
const targetApp = (name && name !== 'desktop' && name !== 'system') ? name : undefined;
|
|
389
|
+
const res = await startLiveScreenShare({ targetApp });
|
|
390
|
+
return {
|
|
391
|
+
success: res.success,
|
|
392
|
+
app: name || appName,
|
|
393
|
+
action: actLower,
|
|
394
|
+
output: res.message,
|
|
395
|
+
details: { active: true, targetApp },
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
if (['stop_screen_share', 'end_screen_share', 'stop_live_stream'].includes(actLower)) {
|
|
399
|
+
const res = stopLiveScreenShare();
|
|
400
|
+
return {
|
|
401
|
+
success: res.success,
|
|
402
|
+
app: name || appName,
|
|
403
|
+
action: actLower,
|
|
404
|
+
output: res.summary,
|
|
405
|
+
details: { active: false },
|
|
406
|
+
};
|
|
407
|
+
}
|
|
278
408
|
if (['capture_screen', 'screenshot', 'screen_shot'].includes(actLower)) {
|
|
409
|
+
if (isLiveScreenShareActive()) {
|
|
410
|
+
const liveRes = await analyzeLiveScreen();
|
|
411
|
+
return {
|
|
412
|
+
success: liveRes.success,
|
|
413
|
+
app: name || appName,
|
|
414
|
+
action: actLower,
|
|
415
|
+
output: liveRes.output,
|
|
416
|
+
details: liveRes.details,
|
|
417
|
+
};
|
|
418
|
+
}
|
|
279
419
|
const res = await captureScreen(name || appName);
|
|
280
420
|
return {
|
|
281
421
|
success: res.success,
|
|
@@ -285,9 +425,9 @@ export async function executeInApp(appName, action, payload) {
|
|
|
285
425
|
details: res.details,
|
|
286
426
|
};
|
|
287
427
|
}
|
|
288
|
-
if (['see_screen', 'inspect_screen', 'view_screen', 'analyze_screen', 'inspect_desktop', 'visual_analysis', 'see_and_analyze'].includes(actLower)) {
|
|
428
|
+
if (['see_screen', 'inspect_screen', 'view_screen', 'analyze_screen', 'inspect_desktop', 'visual_analysis', 'see_and_analyze', 'live_screen_stream', 'screen_stream'].includes(actLower)) {
|
|
289
429
|
const query = typeof payload === 'string' ? payload : (payload?.query || payload?.target || payload?.text || undefined);
|
|
290
|
-
const res = await
|
|
430
|
+
const res = await analyzeLiveScreen(query);
|
|
291
431
|
return {
|
|
292
432
|
success: res.success,
|
|
293
433
|
app: name || appName,
|
|
@@ -435,7 +575,19 @@ export async function executeInApp(appName, action, payload) {
|
|
|
435
575
|
if (category === 'browser') {
|
|
436
576
|
if (['navigate', 'goto', 'open_url'].includes(actLower)) {
|
|
437
577
|
const rawUrl = typeof payload === 'string' ? payload : (payload?.url || payload?.target || 'https://google.com');
|
|
438
|
-
|
|
578
|
+
let targetUrl = rawUrl.trim();
|
|
579
|
+
let navigationNote = '';
|
|
580
|
+
// Universal URL vs Search Query Grounding:
|
|
581
|
+
// If the input is not a valid URL (contains spaces or lacks a dot / scheme), convert into a search
|
|
582
|
+
if (!targetUrl.startsWith('http://') && !targetUrl.startsWith('https://')) {
|
|
583
|
+
if (targetUrl.includes(' ') || !targetUrl.includes('.')) {
|
|
584
|
+
navigationNote = ` [Note: Plain search term passed to navigate; converted to web search query]`;
|
|
585
|
+
targetUrl = `https://www.google.com/search?q=${encodeURIComponent(targetUrl)}`;
|
|
586
|
+
}
|
|
587
|
+
else {
|
|
588
|
+
targetUrl = `https://${targetUrl}`;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
439
591
|
await openApp(name || 'browser', targetUrl);
|
|
440
592
|
await new Promise((r) => setTimeout(r, 800));
|
|
441
593
|
const capture = await captureScreen(name || 'browser');
|
|
@@ -443,7 +595,7 @@ export async function executeInApp(appName, action, payload) {
|
|
|
443
595
|
success: true,
|
|
444
596
|
app: name || 'browser',
|
|
445
597
|
action: actLower,
|
|
446
|
-
output: `Browser Navigated Successfully to Target URL: ${targetUrl}\n- Screenshot Saved: ${capture.details?.outPath || 'Active'}`,
|
|
598
|
+
output: `Browser Navigated Successfully to Target URL: ${targetUrl}${navigationNote}\n- Screenshot Saved: ${capture.details?.outPath || 'Active'}`,
|
|
447
599
|
details: { url: targetUrl, capture: capture.details },
|
|
448
600
|
};
|
|
449
601
|
}
|
|
@@ -557,6 +709,10 @@ export async function executeInApp(appName, action, payload) {
|
|
|
557
709
|
return await openApp(name, filePath);
|
|
558
710
|
}
|
|
559
711
|
}
|
|
712
|
+
if (category === 'game' || ['open', 'launch', 'start'].includes(actLower)) {
|
|
713
|
+
const rawTarget = typeof payload === 'string' ? payload : (payload?.target || payload?.game || payload?.url || payload?.query || payload?.id || '');
|
|
714
|
+
return await openApp(name || appName, rawTarget);
|
|
715
|
+
}
|
|
560
716
|
// Generic fallback app action
|
|
561
717
|
return {
|
|
562
718
|
success: true,
|
|
@@ -617,49 +773,291 @@ export function getAppCapabilities() {
|
|
|
617
773
|
defaultEditorLauncher: platform === 'win32' ? 'code / notepad' : platform === 'darwin' ? 'code / open -a' : 'code / xdg-open',
|
|
618
774
|
};
|
|
619
775
|
}
|
|
776
|
+
const WIN_CAPTURE_SCRIPT_CONTENT = `param(
|
|
777
|
+
[string]$OutPath,
|
|
778
|
+
[switch]$InfoOnly
|
|
779
|
+
)
|
|
780
|
+
|
|
781
|
+
$source = @"
|
|
782
|
+
using System;
|
|
783
|
+
using System.Drawing;
|
|
784
|
+
using System.Drawing.Imaging;
|
|
785
|
+
using System.Runtime.InteropServices;
|
|
786
|
+
using System.Text;
|
|
787
|
+
using System.Threading;
|
|
788
|
+
using System.Collections.Generic;
|
|
789
|
+
|
|
790
|
+
public class WinCaptureResult {
|
|
791
|
+
public bool success;
|
|
792
|
+
public string savedPath;
|
|
793
|
+
public int width;
|
|
794
|
+
public int height;
|
|
795
|
+
public int cursorX;
|
|
796
|
+
public int cursorY;
|
|
797
|
+
public string activeWindow;
|
|
798
|
+
public string[] visibleWindows;
|
|
799
|
+
public string error;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
public class WinCaptureHelper {
|
|
803
|
+
[DllImport("user32.dll", SetLastError = true)]
|
|
804
|
+
public static extern IntPtr OpenInputDesktop(uint dwFlags, bool fInherit, uint dwDesiredAccess);
|
|
805
|
+
|
|
806
|
+
[DllImport("user32.dll", SetLastError = true)]
|
|
807
|
+
public static extern IntPtr OpenDesktop(string lpszDesktop, uint dwFlags, bool fInherit, uint dwDesiredAccess);
|
|
808
|
+
|
|
809
|
+
[DllImport("user32.dll", SetLastError = true)]
|
|
810
|
+
public static extern bool SetThreadDesktop(IntPtr hDesktop);
|
|
811
|
+
|
|
812
|
+
[DllImport("user32.dll", SetLastError = true)]
|
|
813
|
+
public static extern bool CloseDesktop(IntPtr hDesktop);
|
|
814
|
+
|
|
815
|
+
[DllImport("user32.dll")]
|
|
816
|
+
public static extern IntPtr GetDC(IntPtr hWnd);
|
|
817
|
+
|
|
818
|
+
[DllImport("user32.dll")]
|
|
819
|
+
public static extern int ReleaseDC(IntPtr hWnd, IntPtr hDC);
|
|
820
|
+
|
|
821
|
+
[DllImport("gdi32.dll")]
|
|
822
|
+
public static extern IntPtr CreateCompatibleDC(IntPtr hdc);
|
|
823
|
+
|
|
824
|
+
[DllImport("gdi32.dll")]
|
|
825
|
+
public static extern IntPtr CreateCompatibleBitmap(IntPtr hdc, int nWidth, int nHeight);
|
|
826
|
+
|
|
827
|
+
[DllImport("gdi32.dll")]
|
|
828
|
+
public static extern IntPtr SelectObject(IntPtr hdc, IntPtr hgdiobj);
|
|
829
|
+
|
|
830
|
+
[DllImport("gdi32.dll")]
|
|
831
|
+
public static extern bool DeleteDC(IntPtr hdc);
|
|
832
|
+
|
|
833
|
+
[DllImport("gdi32.dll")]
|
|
834
|
+
public static extern bool DeleteObject(IntPtr hObject);
|
|
835
|
+
|
|
836
|
+
[DllImport("gdi32.dll")]
|
|
837
|
+
public static extern bool BitBlt(IntPtr hdcDest, int nXDest, int nYDest, int nWidth, int nHeight, IntPtr hdcSrc, int nXSrc, int nYSrc, int dwRop);
|
|
838
|
+
|
|
839
|
+
[DllImport("user32.dll")]
|
|
840
|
+
public static extern int GetSystemMetrics(int nIndex);
|
|
841
|
+
|
|
842
|
+
public delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
|
|
843
|
+
|
|
844
|
+
[DllImport("user32.dll")]
|
|
845
|
+
public static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam);
|
|
846
|
+
|
|
847
|
+
[DllImport("user32.dll", CharSet = CharSet.Auto)]
|
|
848
|
+
public static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount);
|
|
849
|
+
|
|
850
|
+
[DllImport("user32.dll")]
|
|
851
|
+
public static extern bool IsWindowVisible(IntPtr hWnd);
|
|
852
|
+
|
|
853
|
+
[DllImport("user32.dll")]
|
|
854
|
+
public static extern IntPtr GetForegroundWindow();
|
|
855
|
+
|
|
856
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
857
|
+
public struct POINT {
|
|
858
|
+
public int X;
|
|
859
|
+
public int Y;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
[DllImport("user32.dll")]
|
|
863
|
+
public static extern bool GetCursorPos(out POINT lpPoint);
|
|
864
|
+
|
|
865
|
+
const uint DESKTOP_ACCESS = 0x01FF | 0x00020000;
|
|
866
|
+
const int SM_CXSCREEN = 0;
|
|
867
|
+
const int SM_CYSCREEN = 1;
|
|
868
|
+
const int SRCCOPY = 0x00CC0020;
|
|
869
|
+
const int CAPTUREBLT = 0x40000000;
|
|
870
|
+
|
|
871
|
+
public static WinCaptureResult GetInfo() {
|
|
872
|
+
var res = new WinCaptureResult();
|
|
873
|
+
res.success = true;
|
|
874
|
+
res.error = "";
|
|
875
|
+
var visibleWindows = new List<string>();
|
|
876
|
+
|
|
877
|
+
Thread worker = new Thread(() => {
|
|
878
|
+
IntPtr hDesk = OpenInputDesktop(0, false, DESKTOP_ACCESS);
|
|
879
|
+
if (hDesk == IntPtr.Zero) {
|
|
880
|
+
hDesk = OpenDesktop("Default", 0, false, DESKTOP_ACCESS);
|
|
881
|
+
}
|
|
882
|
+
if (hDesk != IntPtr.Zero) {
|
|
883
|
+
SetThreadDesktop(hDesk);
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
res.width = GetSystemMetrics(SM_CXSCREEN);
|
|
887
|
+
res.height = GetSystemMetrics(SM_CYSCREEN);
|
|
888
|
+
if (res.width <= 0) res.width = 1920;
|
|
889
|
+
if (res.height <= 0) res.height = 1080;
|
|
890
|
+
|
|
891
|
+
POINT pt;
|
|
892
|
+
if (GetCursorPos(out pt)) {
|
|
893
|
+
res.cursorX = pt.X;
|
|
894
|
+
res.cursorY = pt.Y;
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
IntPtr fg = GetForegroundWindow();
|
|
898
|
+
if (fg != IntPtr.Zero) {
|
|
899
|
+
var sbFg = new StringBuilder(256);
|
|
900
|
+
GetWindowText(fg, sbFg, 256);
|
|
901
|
+
res.activeWindow = sbFg.ToString().Trim();
|
|
902
|
+
} else {
|
|
903
|
+
res.activeWindow = "";
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
EnumWindows((hWnd, lParam) => {
|
|
907
|
+
if (IsWindowVisible(hWnd)) {
|
|
908
|
+
var title = new StringBuilder(256);
|
|
909
|
+
GetWindowText(hWnd, title, 256);
|
|
910
|
+
string t = title.ToString().Trim();
|
|
911
|
+
if (t.Length > 0 && !visibleWindows.Contains(t) && t != "Program Manager" && t != "Windows Input Experience") {
|
|
912
|
+
visibleWindows.Add(t);
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
return true;
|
|
916
|
+
}, IntPtr.Zero);
|
|
917
|
+
|
|
918
|
+
if (hDesk != IntPtr.Zero) {
|
|
919
|
+
CloseDesktop(hDesk);
|
|
920
|
+
}
|
|
921
|
+
});
|
|
922
|
+
|
|
923
|
+
worker.SetApartmentState(ApartmentState.STA);
|
|
924
|
+
worker.Start();
|
|
925
|
+
worker.Join();
|
|
926
|
+
|
|
927
|
+
res.visibleWindows = visibleWindows.ToArray();
|
|
928
|
+
return res;
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
public static WinCaptureResult Capture(string filePath) {
|
|
932
|
+
var res = new WinCaptureResult();
|
|
933
|
+
res.savedPath = filePath;
|
|
934
|
+
res.error = "";
|
|
935
|
+
var visibleWindows = new List<string>();
|
|
936
|
+
|
|
937
|
+
Thread worker = new Thread(() => {
|
|
938
|
+
IntPtr hDesk = OpenInputDesktop(0, false, DESKTOP_ACCESS);
|
|
939
|
+
if (hDesk == IntPtr.Zero) {
|
|
940
|
+
hDesk = OpenDesktop("Default", 0, false, DESKTOP_ACCESS);
|
|
941
|
+
}
|
|
942
|
+
if (hDesk != IntPtr.Zero) {
|
|
943
|
+
SetThreadDesktop(hDesk);
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
res.width = GetSystemMetrics(SM_CXSCREEN);
|
|
947
|
+
res.height = GetSystemMetrics(SM_CYSCREEN);
|
|
948
|
+
if (res.width <= 0) res.width = 1920;
|
|
949
|
+
if (res.height <= 0) res.height = 1080;
|
|
950
|
+
|
|
951
|
+
POINT pt;
|
|
952
|
+
if (GetCursorPos(out pt)) {
|
|
953
|
+
res.cursorX = pt.X;
|
|
954
|
+
res.cursorY = pt.Y;
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
IntPtr fg = GetForegroundWindow();
|
|
958
|
+
if (fg != IntPtr.Zero) {
|
|
959
|
+
var sbFg = new StringBuilder(256);
|
|
960
|
+
GetWindowText(fg, sbFg, 256);
|
|
961
|
+
res.activeWindow = sbFg.ToString().Trim();
|
|
962
|
+
} else {
|
|
963
|
+
res.activeWindow = "";
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
EnumWindows((hWnd, lParam) => {
|
|
967
|
+
if (IsWindowVisible(hWnd)) {
|
|
968
|
+
var title = new StringBuilder(256);
|
|
969
|
+
GetWindowText(hWnd, title, 256);
|
|
970
|
+
string t = title.ToString().Trim();
|
|
971
|
+
if (t.Length > 0 && !visibleWindows.Contains(t) && t != "Program Manager" && t != "Windows Input Experience") {
|
|
972
|
+
visibleWindows.Add(t);
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
return true;
|
|
976
|
+
}, IntPtr.Zero);
|
|
977
|
+
|
|
978
|
+
try {
|
|
979
|
+
IntPtr hdcSrc = GetDC(IntPtr.Zero);
|
|
980
|
+
IntPtr hdcDest = CreateCompatibleDC(hdcSrc);
|
|
981
|
+
IntPtr hBitmap = CreateCompatibleBitmap(hdcSrc, res.width, res.height);
|
|
982
|
+
IntPtr hOld = SelectObject(hdcDest, hBitmap);
|
|
983
|
+
bool bltOk = BitBlt(hdcDest, 0, 0, res.width, res.height, hdcSrc, 0, 0, SRCCOPY | CAPTUREBLT);
|
|
984
|
+
|
|
985
|
+
SelectObject(hdcDest, hOld);
|
|
986
|
+
DeleteDC(hdcDest);
|
|
987
|
+
ReleaseDC(IntPtr.Zero, hdcSrc);
|
|
988
|
+
|
|
989
|
+
if (bltOk) {
|
|
990
|
+
using (Bitmap bmp = Bitmap.FromHbitmap(hBitmap)) {
|
|
991
|
+
bmp.Save(filePath, ImageFormat.Png);
|
|
992
|
+
}
|
|
993
|
+
res.success = true;
|
|
994
|
+
} else {
|
|
995
|
+
res.error = "BitBlt failed";
|
|
996
|
+
}
|
|
997
|
+
DeleteObject(hBitmap);
|
|
998
|
+
} catch (Exception ex) {
|
|
999
|
+
res.error = ex.Message;
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
if (hDesk != IntPtr.Zero) {
|
|
1003
|
+
CloseDesktop(hDesk);
|
|
1004
|
+
}
|
|
1005
|
+
});
|
|
1006
|
+
|
|
1007
|
+
worker.SetApartmentState(ApartmentState.STA);
|
|
1008
|
+
worker.Start();
|
|
1009
|
+
worker.Join();
|
|
1010
|
+
|
|
1011
|
+
res.visibleWindows = visibleWindows.ToArray();
|
|
1012
|
+
return res;
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
"@
|
|
1016
|
+
|
|
1017
|
+
Add-Type -TypeDefinition $source -ReferencedAssemblies System.Drawing, System.Windows.Forms
|
|
1018
|
+
|
|
1019
|
+
if ($InfoOnly) {
|
|
1020
|
+
[WinCaptureHelper]::GetInfo() | ConvertTo-Json -Compress
|
|
1021
|
+
} else {
|
|
1022
|
+
[WinCaptureHelper]::Capture($OutPath) | ConvertTo-Json -Compress
|
|
1023
|
+
}
|
|
1024
|
+
`;
|
|
1025
|
+
export function ensureWinCaptureScript() {
|
|
1026
|
+
const scriptsDir = path.join(process.cwd(), '.ft', 'scripts');
|
|
1027
|
+
if (!fs.existsSync(scriptsDir)) {
|
|
1028
|
+
fs.mkdirSync(scriptsDir, { recursive: true });
|
|
1029
|
+
}
|
|
1030
|
+
const scriptPath = path.join(scriptsDir, 'winCapture.ps1');
|
|
1031
|
+
fs.writeFileSync(scriptPath, WIN_CAPTURE_SCRIPT_CONTENT, 'utf8');
|
|
1032
|
+
return scriptPath;
|
|
1033
|
+
}
|
|
620
1034
|
/**
|
|
621
|
-
* Get screen metrics, active window title, and cursor position.
|
|
1035
|
+
* Get screen metrics, active window title, visible windows, and cursor position.
|
|
622
1036
|
*/
|
|
623
1037
|
export async function getScreenInfo(appName) {
|
|
624
1038
|
const platform = process.platform;
|
|
625
1039
|
if (platform === 'win32') {
|
|
626
|
-
const psScript = `
|
|
627
|
-
Add-Type -AssemblyName System.Windows.Forms
|
|
628
|
-
Add-Type -AssemblyName System.Drawing
|
|
629
|
-
|
|
630
|
-
$screen = [System.Windows.Forms.Screen]::PrimaryScreen
|
|
631
|
-
$cursor = [System.Windows.Forms.Cursor]::Position
|
|
632
|
-
|
|
633
|
-
$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);'
|
|
634
|
-
$w = Add-Type -memberDefinition $sig -name 'Win32Window' -namespace Win32Functions -passThru
|
|
635
|
-
$hwnd = $w::GetForegroundWindow()
|
|
636
|
-
$sb = New-Object System.Text.StringBuilder 256
|
|
637
|
-
[void]$w::GetWindowText($hwnd, $sb, 256)
|
|
638
|
-
$activeTitle = $sb.ToString()
|
|
639
|
-
|
|
640
|
-
@{
|
|
641
|
-
screenWidth = $screen.Bounds.Width
|
|
642
|
-
screenHeight = $screen.Bounds.Height
|
|
643
|
-
cursorX = $cursor.X
|
|
644
|
-
cursorY = $cursor.Y
|
|
645
|
-
activeWindow = $activeTitle
|
|
646
|
-
} | ConvertTo-Json
|
|
647
|
-
`;
|
|
648
1040
|
try {
|
|
649
|
-
const
|
|
650
|
-
const { stdout } = await execAsync(`powershell -NoProfile -
|
|
1041
|
+
const scriptPath = ensureWinCaptureScript();
|
|
1042
|
+
const { stdout } = await execAsync(`powershell -NoProfile -ExecutionPolicy Bypass -File "${scriptPath}" -InfoOnly`);
|
|
651
1043
|
const data = JSON.parse(stdout.trim());
|
|
1044
|
+
const visList = Array.isArray(data.visibleWindows) && data.visibleWindows.length > 0
|
|
1045
|
+
? ` | Visible Windows: ${data.visibleWindows.slice(0, 5).join(', ')}`
|
|
1046
|
+
: '';
|
|
652
1047
|
return {
|
|
653
1048
|
success: true,
|
|
654
|
-
output: `Screen Info: Resolution ${data.screenWidth}x${data.screenHeight} | Cursor at (${data.cursorX}, ${data.cursorY}) | Active Window: "${data.activeWindow || 'Desktop'}"`,
|
|
655
|
-
details:
|
|
1049
|
+
output: `Screen Info: Resolution ${data.screenWidth}x${data.screenHeight} | Cursor at (${data.cursorX}, ${data.cursorY}) | Active Window: "${data.activeWindow || appName || 'Desktop'}"${visList}`,
|
|
1050
|
+
details: {
|
|
1051
|
+
...data,
|
|
1052
|
+
activeWindow: data.activeWindow || appName || 'Desktop',
|
|
1053
|
+
},
|
|
656
1054
|
};
|
|
657
1055
|
}
|
|
658
|
-
catch
|
|
1056
|
+
catch {
|
|
659
1057
|
return {
|
|
660
1058
|
success: true,
|
|
661
1059
|
output: `Screen Info (Default): Resolution 1920x1080 | Active Window: "${appName || 'Desktop'}"`,
|
|
662
|
-
details: { screenWidth: 1920, screenHeight: 1080, activeWindow: appName || 'Desktop' },
|
|
1060
|
+
details: { screenWidth: 1920, screenHeight: 1080, activeWindow: appName || 'Desktop', cursorX: 0, cursorY: 0 },
|
|
663
1061
|
};
|
|
664
1062
|
}
|
|
665
1063
|
}
|
|
@@ -671,12 +1069,208 @@ $activeTitle = $sb.ToString()
|
|
|
671
1069
|
};
|
|
672
1070
|
}
|
|
673
1071
|
}
|
|
1072
|
+
let persistentAuraProcess = null;
|
|
1073
|
+
let persistentAuraFlagPath = null;
|
|
1074
|
+
/**
|
|
1075
|
+
* Start persistent futuristic sky-blue aura overlay on screen borders and corners.
|
|
1076
|
+
* Stays continuously active and animating as long as live screen sharing is running.
|
|
1077
|
+
*/
|
|
1078
|
+
export async function startScreenAuraOverlay(message = "Live Screen Share Active • Streaming to Model") {
|
|
1079
|
+
const platform = process.platform;
|
|
1080
|
+
if (platform !== 'win32')
|
|
1081
|
+
return;
|
|
1082
|
+
// Stop any existing overlay first
|
|
1083
|
+
stopScreenAuraOverlay();
|
|
1084
|
+
try {
|
|
1085
|
+
const scriptsDir = path.join(process.cwd(), '.ft', 'scripts');
|
|
1086
|
+
if (!fs.existsSync(scriptsDir)) {
|
|
1087
|
+
fs.mkdirSync(scriptsDir, { recursive: true });
|
|
1088
|
+
}
|
|
1089
|
+
const psFile = path.join(scriptsDir, 'scout_aura_persistent.ps1');
|
|
1090
|
+
const flagFile = path.join(scriptsDir, 'aura_active.flag');
|
|
1091
|
+
fs.writeFileSync(flagFile, String(Date.now()), 'utf8');
|
|
1092
|
+
persistentAuraFlagPath = flagFile;
|
|
1093
|
+
const subtitleText = " Perception Active";
|
|
1094
|
+
const parentPid = process.pid;
|
|
1095
|
+
const overlayPs = `
|
|
1096
|
+
Add-Type -AssemblyName PresentationFramework, PresentationCore, WindowsBase
|
|
1097
|
+
|
|
1098
|
+
[xml]$xaml = @"
|
|
1099
|
+
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
|
1100
|
+
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
|
1101
|
+
Title="Scout Aura Overlay"
|
|
1102
|
+
WindowStyle="None"
|
|
1103
|
+
AllowsTransparency="True"
|
|
1104
|
+
Background="Transparent"
|
|
1105
|
+
Topmost="True"
|
|
1106
|
+
ShowInTaskbar="False"
|
|
1107
|
+
ShowActivated="False"
|
|
1108
|
+
IsHitTestVisible="False">
|
|
1109
|
+
<Window.Resources>
|
|
1110
|
+
<Storyboard x:Key="AuraPulse" RepeatBehavior="Forever" AutoReverse="True">
|
|
1111
|
+
<DoubleAnimation Storyboard.TargetName="AuraBorder" Storyboard.TargetProperty="Opacity"
|
|
1112
|
+
From="0.4" To="1.0" Duration="0:0:0.6"/>
|
|
1113
|
+
<DoubleAnimation Storyboard.TargetName="CornerTL" Storyboard.TargetProperty="Opacity"
|
|
1114
|
+
From="0.5" To="1.0" Duration="0:0:0.6"/>
|
|
1115
|
+
<DoubleAnimation Storyboard.TargetName="CornerTR" Storyboard.TargetProperty="Opacity"
|
|
1116
|
+
From="0.5" To="1.0" Duration="0:0:0.6"/>
|
|
1117
|
+
<DoubleAnimation Storyboard.TargetName="CornerBL" Storyboard.TargetProperty="Opacity"
|
|
1118
|
+
From="0.5" To="1.0" Duration="0:0:0.6"/>
|
|
1119
|
+
<DoubleAnimation Storyboard.TargetName="CornerBR" Storyboard.TargetProperty="Opacity"
|
|
1120
|
+
From="0.5" To="1.0" Duration="0:0:0.6"/>
|
|
1121
|
+
<DoubleAnimation Storyboard.TargetName="AuraGlow" Storyboard.TargetProperty="Opacity"
|
|
1122
|
+
From="0.25" To="0.9" Duration="0:0:0.6"/>
|
|
1123
|
+
</Storyboard>
|
|
1124
|
+
</Window.Resources>
|
|
1125
|
+
|
|
1126
|
+
<Grid IsHitTestVisible="False">
|
|
1127
|
+
<!-- Outer Glowing Sky Blue Aura Border -->
|
|
1128
|
+
<Border x:Name="AuraGlow" BorderThickness="6" BorderBrush="#00d8ff" Margin="0">
|
|
1129
|
+
<Border.Effect>
|
|
1130
|
+
<DropShadowEffect Color="#00e5ff" BlurRadius="45" ShadowDepth="0" Opacity="0.95"/>
|
|
1131
|
+
</Border.Effect>
|
|
1132
|
+
</Border>
|
|
1133
|
+
<Border x:Name="AuraBorder" BorderThickness="3" BorderBrush="#38bdf8" Margin="0">
|
|
1134
|
+
<Border.Effect>
|
|
1135
|
+
<DropShadowEffect Color="#00e5ff" BlurRadius="25" ShadowDepth="0" Opacity="1"/>
|
|
1136
|
+
</Border.Effect>
|
|
1137
|
+
</Border>
|
|
1138
|
+
|
|
1139
|
+
<!-- Sky Blue Corner Brackets with Aura Glow -->
|
|
1140
|
+
<!-- Top Left Corner -->
|
|
1141
|
+
<Canvas HorizontalAlignment="Left" VerticalAlignment="Top" Width="120" Height="120" Margin="6,6,0,0">
|
|
1142
|
+
<Path x:Name="CornerTL" Data="M 0,90 L 0,0 L 90,0" Stroke="#00e5ff" StrokeThickness="6">
|
|
1143
|
+
<Path.Effect>
|
|
1144
|
+
<DropShadowEffect Color="#00e5ff" BlurRadius="30" ShadowDepth="0" Opacity="1"/>
|
|
1145
|
+
</Path.Effect>
|
|
1146
|
+
</Path>
|
|
1147
|
+
</Canvas>
|
|
1148
|
+
|
|
1149
|
+
<!-- Top Right Corner -->
|
|
1150
|
+
<Canvas HorizontalAlignment="Right" VerticalAlignment="Top" Width="120" Height="120" Margin="0,6,6,0">
|
|
1151
|
+
<Path x:Name="CornerTR" Data="M 30,0 L 120,0 L 120,90" Stroke="#00e5ff" StrokeThickness="6">
|
|
1152
|
+
<Path.Effect>
|
|
1153
|
+
<DropShadowEffect Color="#00e5ff" BlurRadius="30" ShadowDepth="0" Opacity="1"/>
|
|
1154
|
+
</Path.Effect>
|
|
1155
|
+
</Path>
|
|
1156
|
+
</Canvas>
|
|
1157
|
+
|
|
1158
|
+
<!-- Bottom Left Corner -->
|
|
1159
|
+
<Canvas HorizontalAlignment="Left" VerticalAlignment="Bottom" Width="120" Height="120" Margin="6,0,0,6">
|
|
1160
|
+
<Path x:Name="CornerBL" Data="M 0,30 L 0,120 L 90,120" Stroke="#00e5ff" StrokeThickness="6">
|
|
1161
|
+
<Path.Effect>
|
|
1162
|
+
<DropShadowEffect Color="#00e5ff" BlurRadius="30" ShadowDepth="0" Opacity="1"/>
|
|
1163
|
+
</Path.Effect>
|
|
1164
|
+
</Path>
|
|
1165
|
+
</Canvas>
|
|
1166
|
+
|
|
1167
|
+
<!-- Bottom Right Corner -->
|
|
1168
|
+
<Canvas HorizontalAlignment="Right" VerticalAlignment="Bottom" Width="120" Height="120" Margin="0,0,6,6">
|
|
1169
|
+
<Path x:Name="CornerBR" Data="M 30,120 L 120,120 L 120,30" Stroke="#00e5ff" StrokeThickness="6">
|
|
1170
|
+
<Path.Effect>
|
|
1171
|
+
<DropShadowEffect Color="#00e5ff" BlurRadius="30" ShadowDepth="0" Opacity="1"/>
|
|
1172
|
+
</Path.Effect>
|
|
1173
|
+
</Path>
|
|
1174
|
+
</Canvas>
|
|
1175
|
+
|
|
1176
|
+
<!-- Top Floating Status Badge -->
|
|
1177
|
+
<Border HorizontalAlignment="Center" VerticalAlignment="Top" Margin="0,22,0,0"
|
|
1178
|
+
Background="#EE03121F" BorderBrush="#00e5ff" BorderThickness="1.8" CornerRadius="24" Padding="26,10">
|
|
1179
|
+
<Border.Effect>
|
|
1180
|
+
<DropShadowEffect Color="#00e5ff" BlurRadius="25" ShadowDepth="0" Opacity="0.95"/>
|
|
1181
|
+
</Border.Effect>
|
|
1182
|
+
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
|
1183
|
+
<Ellipse Width="12" Height="12" Fill="#00e5ff" Margin="0,0,12,0">
|
|
1184
|
+
<Ellipse.Effect>
|
|
1185
|
+
<DropShadowEffect Color="#00e5ff" BlurRadius="12" ShadowDepth="0"/>
|
|
1186
|
+
</Ellipse.Effect>
|
|
1187
|
+
</Ellipse>
|
|
1188
|
+
<TextBlock Text="${message}" Foreground="#ffffff" FontSize="16" FontWeight="Bold" VerticalAlignment="Center"/>
|
|
1189
|
+
<TextBlock Text="${subtitleText}" Foreground="#38bdf8" FontSize="13.5" FontWeight="SemiBold" Margin="10,0,0,0" VerticalAlignment="Center"/>
|
|
1190
|
+
</StackPanel>
|
|
1191
|
+
</Border>
|
|
1192
|
+
</Grid>
|
|
1193
|
+
</Window>
|
|
1194
|
+
"@
|
|
1195
|
+
|
|
1196
|
+
$reader = (New-Object System.Xml.XmlNodeReader $xaml)
|
|
1197
|
+
$window = [System.Windows.Markup.XamlReader]::Load($reader)
|
|
1198
|
+
|
|
1199
|
+
$window.Left = [System.Windows.SystemParameters]::VirtualScreenLeft
|
|
1200
|
+
$window.Top = [System.Windows.SystemParameters]::VirtualScreenTop
|
|
1201
|
+
$window.Width = [System.Windows.SystemParameters]::VirtualScreenWidth
|
|
1202
|
+
$window.Height = [System.Windows.SystemParameters]::VirtualScreenHeight
|
|
1203
|
+
$window.Topmost = $true
|
|
1204
|
+
$window.ShowActivated = $false
|
|
1205
|
+
|
|
1206
|
+
$storyboard = $window.Resources["AuraPulse"]
|
|
1207
|
+
$window.Add_Loaded({
|
|
1208
|
+
$storyboard.Begin($window)
|
|
1209
|
+
})
|
|
1210
|
+
|
|
1211
|
+
$flagPath = "${flagFile.replace(/\\/g, '\\\\')}"
|
|
1212
|
+
$parentPid = ${parentPid}
|
|
1213
|
+
|
|
1214
|
+
# Check if screen sharing is active or parent exited every 350ms
|
|
1215
|
+
$timer = New-Object System.Windows.Threading.DispatcherTimer
|
|
1216
|
+
$timer.Interval = [TimeSpan]::FromMilliseconds(350)
|
|
1217
|
+
$timer.Add_Tick({
|
|
1218
|
+
$shouldClose = $false
|
|
1219
|
+
if (-not (Test-Path $flagPath)) {
|
|
1220
|
+
$shouldClose = $true
|
|
1221
|
+
} else {
|
|
1222
|
+
$proc = Get-Process -Id $parentPid -ErrorAction SilentlyContinue
|
|
1223
|
+
if (-not $proc) {
|
|
1224
|
+
$shouldClose = $true
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
if ($shouldClose) {
|
|
1228
|
+
$timer.Stop()
|
|
1229
|
+
$window.Close()
|
|
1230
|
+
}
|
|
1231
|
+
})
|
|
1232
|
+
$timer.Start()
|
|
1233
|
+
|
|
1234
|
+
[void]$window.ShowDialog()
|
|
1235
|
+
`;
|
|
1236
|
+
fs.writeFileSync(psFile, overlayPs, 'utf8');
|
|
1237
|
+
const child = spawn('powershell', ['-WindowStyle', 'Hidden', '-NoProfile', '-Sta', '-ExecutionPolicy', 'Bypass', '-File', psFile], {
|
|
1238
|
+
detached: false,
|
|
1239
|
+
stdio: 'ignore',
|
|
1240
|
+
});
|
|
1241
|
+
child.unref();
|
|
1242
|
+
persistentAuraProcess = child;
|
|
1243
|
+
}
|
|
1244
|
+
catch { }
|
|
1245
|
+
}
|
|
1246
|
+
/**
|
|
1247
|
+
* Stop persistent screen aura animation when live screen sharing stops.
|
|
1248
|
+
*/
|
|
1249
|
+
export function stopScreenAuraOverlay() {
|
|
1250
|
+
if (persistentAuraFlagPath && fs.existsSync(persistentAuraFlagPath)) {
|
|
1251
|
+
try {
|
|
1252
|
+
fs.unlinkSync(persistentAuraFlagPath);
|
|
1253
|
+
}
|
|
1254
|
+
catch { }
|
|
1255
|
+
persistentAuraFlagPath = null;
|
|
1256
|
+
}
|
|
1257
|
+
if (persistentAuraProcess) {
|
|
1258
|
+
try {
|
|
1259
|
+
persistentAuraProcess.kill();
|
|
1260
|
+
}
|
|
1261
|
+
catch { }
|
|
1262
|
+
persistentAuraProcess = null;
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
674
1265
|
/**
|
|
675
1266
|
* Display a futuristic sky-blue aura animation on screen borders and corners,
|
|
676
|
-
* show the warning "Scout is on the screen.", and block external inputs
|
|
677
|
-
*
|
|
1267
|
+
* show the warning "Scout is on the screen.", and optionally block external inputs.
|
|
1268
|
+
* If durationMs <= 0, aura stays on continuously until stopScreenAuraOverlay() is called.
|
|
678
1269
|
*/
|
|
679
1270
|
export async function showScreenAuraOverlay(durationMs = 2200, message = "Scout is viewing the screen.", blockInput = false) {
|
|
1271
|
+
if (durationMs <= 0) {
|
|
1272
|
+
return await startScreenAuraOverlay(message);
|
|
1273
|
+
}
|
|
680
1274
|
const platform = process.platform;
|
|
681
1275
|
if (platform !== 'win32')
|
|
682
1276
|
return;
|
|
@@ -694,7 +1288,7 @@ try { [void]$b::BlockInput($true) } catch {}
|
|
|
694
1288
|
const unblockSnippet = blockInput ? `
|
|
695
1289
|
try { [void]$b::BlockInput($false) } catch {}
|
|
696
1290
|
` : '';
|
|
697
|
-
const subtitleText = blockInput ? " • External Input Locked" : "
|
|
1291
|
+
const subtitleText = blockInput ? " • External Input Locked" : " Perception Active";
|
|
698
1292
|
const overlayPs = `
|
|
699
1293
|
Add-Type -AssemblyName PresentationFramework, PresentationCore, WindowsBase
|
|
700
1294
|
${blockInputSnippet}
|
|
@@ -897,51 +1491,56 @@ export async function captureScreen(appNameOrScreen) {
|
|
|
897
1491
|
const filename = `screen_${Date.now()}.png`;
|
|
898
1492
|
const outPath = path.join(screenshotsDir, filename);
|
|
899
1493
|
if (platform === 'win32') {
|
|
900
|
-
|
|
1494
|
+
try {
|
|
1495
|
+
const scriptPath = ensureWinCaptureScript();
|
|
1496
|
+
const { stdout } = await execAsync(`powershell -NoProfile -ExecutionPolicy Bypass -File "${scriptPath}" -OutPath "${outPath}"`);
|
|
1497
|
+
const meta = JSON.parse(stdout.trim());
|
|
1498
|
+
if (meta.success && fs.existsSync(outPath) && fs.statSync(outPath).size > 0) {
|
|
1499
|
+
registerCapturedScreenshot(outPath);
|
|
1500
|
+
showScreenAuraOverlay(1800, "Scout is viewing the screen...", false).catch(() => { });
|
|
1501
|
+
const visibleList = Array.isArray(meta.visibleWindows) && meta.visibleWindows.length > 0
|
|
1502
|
+
? `\n- Visible Windows: ${meta.visibleWindows.slice(0, 6).map((w) => `"${w}"`).join(', ')}`
|
|
1503
|
+
: '';
|
|
1504
|
+
return {
|
|
1505
|
+
success: true,
|
|
1506
|
+
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).`,
|
|
1507
|
+
details: { ...meta, outPath },
|
|
1508
|
+
};
|
|
1509
|
+
}
|
|
1510
|
+
throw new Error(meta.error || 'Desktop capture returned false');
|
|
1511
|
+
}
|
|
1512
|
+
catch (err) {
|
|
1513
|
+
// Fallback to Graphics.CopyFromScreen
|
|
1514
|
+
const psScript = `
|
|
901
1515
|
Add-Type -AssemblyName System.Windows.Forms
|
|
902
1516
|
Add-Type -AssemblyName System.Drawing
|
|
903
|
-
|
|
904
1517
|
$screen = [System.Windows.Forms.Screen]::PrimaryScreen
|
|
905
1518
|
$bounds = $screen.Bounds
|
|
906
1519
|
$bmp = New-Object System.Drawing.Bitmap($bounds.Width, $bounds.Height)
|
|
907
1520
|
$graphics = [System.Drawing.Graphics]::FromImage($bmp)
|
|
908
|
-
$graphics.CopyFromScreen($bounds.Location, [System.Drawing.Point]::Empty, $bounds.Size)
|
|
1521
|
+
try { $graphics.CopyFromScreen($bounds.Location, [System.Drawing.Point]::Empty, $bounds.Size) } catch {}
|
|
909
1522
|
$bmp.Save("${outPath.replace(/\\/g, '\\\\')}", [System.Drawing.Imaging.ImageFormat]::Png)
|
|
910
1523
|
$graphics.Dispose()
|
|
911
1524
|
$bmp.Dispose()
|
|
912
|
-
|
|
913
|
-
$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);'
|
|
914
|
-
$w = Add-Type -memberDefinition $sig -name 'Win32Window' -namespace Win32Functions -passThru
|
|
915
|
-
$hwnd = $w::GetForegroundWindow()
|
|
916
|
-
$sb = New-Object System.Text.StringBuilder 256
|
|
917
|
-
[void]$w::GetWindowText($hwnd, $sb, 256)
|
|
918
|
-
$activeTitle = $sb.ToString()
|
|
919
|
-
|
|
920
|
-
@{
|
|
921
|
-
savedPath = "${outPath.replace(/\\/g, '\\\\')}"
|
|
922
|
-
width = $bounds.Width
|
|
923
|
-
height = $bounds.Height
|
|
924
|
-
activeWindow = $activeTitle
|
|
925
|
-
} | ConvertTo-Json
|
|
1525
|
+
@{ savedPath = "${outPath.replace(/\\/g, '\\\\')}"; width = $bounds.Width; height = $bounds.Height; activeWindow = "${appNameOrScreen || 'Desktop'}" } | ConvertTo-Json
|
|
926
1526
|
`;
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
};
|
|
1527
|
+
try {
|
|
1528
|
+
const encodedScript = Buffer.from(psScript, 'utf16le').toString('base64');
|
|
1529
|
+
const { stdout } = await execAsync(`powershell -NoProfile -EncodedCommand ${encodedScript}`);
|
|
1530
|
+
const meta = JSON.parse(stdout.trim());
|
|
1531
|
+
registerCapturedScreenshot(outPath);
|
|
1532
|
+
return {
|
|
1533
|
+
success: true,
|
|
1534
|
+
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.`,
|
|
1535
|
+
details: { ...meta, outPath },
|
|
1536
|
+
};
|
|
1537
|
+
}
|
|
1538
|
+
catch (fallbackErr) {
|
|
1539
|
+
return {
|
|
1540
|
+
success: false,
|
|
1541
|
+
output: `Failed to capture screen: ${err?.message || fallbackErr?.message || String(err)}`,
|
|
1542
|
+
};
|
|
1543
|
+
}
|
|
945
1544
|
}
|
|
946
1545
|
}
|
|
947
1546
|
else if (platform === 'darwin') {
|
|
@@ -979,6 +1578,64 @@ $activeTitle = $sb.ToString()
|
|
|
979
1578
|
}
|
|
980
1579
|
}
|
|
981
1580
|
}
|
|
1581
|
+
/**
|
|
1582
|
+
* Call Vision AI with graceful multi-provider fallback.
|
|
1583
|
+
* Tries NVIDIA Vision (meta/llama-3.2-11b-vision-instruct), Gemini (gemini-2.0-flash), OpenAI (gpt-4o-mini), and BYOK.
|
|
1584
|
+
*/
|
|
1585
|
+
export async function callVisionAi(outPath, prompt, maxTokens = 350) {
|
|
1586
|
+
if (!outPath || !fs.existsSync(outPath))
|
|
1587
|
+
return null;
|
|
1588
|
+
let imageBase64;
|
|
1589
|
+
try {
|
|
1590
|
+
imageBase64 = fs.readFileSync(outPath).toString('base64');
|
|
1591
|
+
}
|
|
1592
|
+
catch {
|
|
1593
|
+
return null;
|
|
1594
|
+
}
|
|
1595
|
+
const nvidiaKey = process.env.NVIDIA_API_KEY || process.env.NVCF_API_KEY;
|
|
1596
|
+
const geminiKey = process.env.GEMINI_API_KEY;
|
|
1597
|
+
const openaiKey = process.env.OPENAI_API_KEY;
|
|
1598
|
+
const byokKey = process.env.BYOK_API_KEY;
|
|
1599
|
+
const candidates = [];
|
|
1600
|
+
if (nvidiaKey) {
|
|
1601
|
+
candidates.push({ apiKey: nvidiaKey, baseURL: 'https://integrate.api.nvidia.com/v1', model: 'meta/llama-3.2-11b-vision-instruct', timeout: 35000 });
|
|
1602
|
+
}
|
|
1603
|
+
if (geminiKey) {
|
|
1604
|
+
candidates.push({ apiKey: geminiKey, baseURL: 'https://generativelanguage.googleapis.com/v1beta/openai', model: 'gemini-2.0-flash', timeout: 20000 });
|
|
1605
|
+
}
|
|
1606
|
+
if (openaiKey) {
|
|
1607
|
+
candidates.push({ apiKey: openaiKey, model: 'gpt-4o-mini', timeout: 20000 });
|
|
1608
|
+
}
|
|
1609
|
+
if (byokKey) {
|
|
1610
|
+
candidates.push({ apiKey: byokKey, model: 'gpt-4o-mini', timeout: 20000 });
|
|
1611
|
+
}
|
|
1612
|
+
for (const cand of candidates) {
|
|
1613
|
+
try {
|
|
1614
|
+
const client = new OpenAI({ apiKey: cand.apiKey, baseURL: cand.baseURL, timeout: cand.timeout });
|
|
1615
|
+
const resp = await client.chat.completions.create({
|
|
1616
|
+
model: cand.model,
|
|
1617
|
+
messages: [
|
|
1618
|
+
{
|
|
1619
|
+
role: 'user',
|
|
1620
|
+
content: [
|
|
1621
|
+
{ type: 'text', text: prompt },
|
|
1622
|
+
{ type: 'image_url', image_url: { url: `data:image/png;base64,${imageBase64}` } },
|
|
1623
|
+
],
|
|
1624
|
+
},
|
|
1625
|
+
],
|
|
1626
|
+
max_tokens: maxTokens,
|
|
1627
|
+
});
|
|
1628
|
+
const reply = resp.choices[0]?.message?.content?.trim();
|
|
1629
|
+
if (reply) {
|
|
1630
|
+
return reply;
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
catch {
|
|
1634
|
+
continue;
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
return null;
|
|
1638
|
+
}
|
|
982
1639
|
/**
|
|
983
1640
|
* Visual screen analysis. Takes a full-screen screenshot and provides visual context
|
|
984
1641
|
* (including AI Vision inspection if an API key is available).
|
|
@@ -989,36 +1646,14 @@ export async function analyzeScreen(appNameOrScreen, targetQuery) {
|
|
|
989
1646
|
const info = await getScreenInfo(appNameOrScreen);
|
|
990
1647
|
let visualAiAnalysis = '';
|
|
991
1648
|
const outPath = capture.details?.outPath;
|
|
992
|
-
// Optional Vision Model Analysis if screenshot exists and
|
|
1649
|
+
// Optional Vision Model Analysis if screenshot exists and has valid content
|
|
993
1650
|
if (capture.success && outPath && fs.existsSync(outPath)) {
|
|
994
1651
|
try {
|
|
995
|
-
|
|
996
|
-
const openaiKey = process.env.OPENAI_API_KEY;
|
|
997
|
-
const byokKey = process.env.BYOK_API_KEY;
|
|
998
|
-
const apiKey = geminiKey || openaiKey || byokKey;
|
|
999
|
-
if (apiKey) {
|
|
1000
|
-
const imageBase64 = fs.readFileSync(outPath).toString('base64');
|
|
1001
|
-
const isGemini = Boolean(geminiKey);
|
|
1002
|
-
const baseURL = isGemini ? 'https://generativelanguage.googleapis.com/v1beta/openai' : undefined;
|
|
1003
|
-
const model = isGemini ? 'gemini-2.5-flash' : 'gpt-4o-mini';
|
|
1004
|
-
const client = new OpenAI({ apiKey, baseURL, timeout: 15000 });
|
|
1652
|
+
if (fs.statSync(outPath).size > 0) {
|
|
1005
1653
|
const prompt = targetQuery
|
|
1006
1654
|
? `Describe what is visible on this screen specifically regarding: "${targetQuery}". Keep it concise, noting visible UI elements, open windows, tabs, buttons, or error messages.`
|
|
1007
|
-
: `Briefly describe what is currently visible on this desktop screen: active window, main content/tabs, buttons, and system state.`;
|
|
1008
|
-
const
|
|
1009
|
-
model,
|
|
1010
|
-
messages: [
|
|
1011
|
-
{
|
|
1012
|
-
role: 'user',
|
|
1013
|
-
content: [
|
|
1014
|
-
{ type: 'text', text: prompt },
|
|
1015
|
-
{ type: 'image_url', image_url: { url: `data:image/png;base64,${imageBase64}` } },
|
|
1016
|
-
],
|
|
1017
|
-
},
|
|
1018
|
-
],
|
|
1019
|
-
max_tokens: 350,
|
|
1020
|
-
});
|
|
1021
|
-
const reply = resp.choices[0]?.message?.content;
|
|
1655
|
+
: `Briefly describe what is currently visible on this desktop screen: active window, main content/tabs, buttons, and system state in 2-3 sentences.`;
|
|
1656
|
+
const reply = await callVisionAi(outPath, prompt, 350);
|
|
1022
1657
|
if (reply && reply.trim()) {
|
|
1023
1658
|
visualAiAnalysis = `\nAI Visual Screen Analysis:\n${reply.trim()}`;
|
|
1024
1659
|
}
|
|
@@ -1028,12 +1663,19 @@ export async function analyzeScreen(appNameOrScreen, targetQuery) {
|
|
|
1028
1663
|
// Vision API call is optional; fallback gracefully to system metadata
|
|
1029
1664
|
}
|
|
1030
1665
|
}
|
|
1666
|
+
const activeWin = capture.details?.activeWindow || info.details?.activeWindow || appNameOrScreen || 'Active Desktop';
|
|
1667
|
+
const visWins = Array.isArray(capture.details?.visibleWindows) && capture.details.visibleWindows.length > 0
|
|
1668
|
+
? capture.details.visibleWindows
|
|
1669
|
+
: (Array.isArray(info.details?.visibleWindows) ? info.details.visibleWindows : []);
|
|
1670
|
+
const visibleWindowsText = visWins.length > 0
|
|
1671
|
+
? `\nVisible Open Windows:\n${visWins.slice(0, 8).map((w) => ` - "${w}"`).join('\n')}`
|
|
1672
|
+
: '';
|
|
1031
1673
|
const report = [
|
|
1032
1674
|
`=== SCOUT SCRATCHPAD DESKTOP SCREEN VISUAL INSPECTION ===`,
|
|
1033
1675
|
`Target App / Screen Context: "${appNameOrScreen || 'Desktop'}"`,
|
|
1034
1676
|
`Screen Resolution: ${info.details?.screenWidth || capture.details?.width || 1920}x${info.details?.screenHeight || capture.details?.height || 1080}`,
|
|
1035
|
-
`Active Window Focus: "${
|
|
1036
|
-
`Mouse Cursor Position: (${info.details?.cursorX || 0}, ${info.details?.cursorY || 0})`,
|
|
1677
|
+
`Active Window Focus: "${activeWin}"${visibleWindowsText}`,
|
|
1678
|
+
`Mouse Cursor Position: (${info.details?.cursorX || capture.details?.cursorX || 0}, ${info.details?.cursorY || capture.details?.cursorY || 0})`,
|
|
1037
1679
|
`Screenshot Status: Captured temporarily for visual inspection. (Will be automatically deleted upon task completion)`,
|
|
1038
1680
|
`Visual Status: Screen is visible and verified.${visualAiAnalysis}`,
|
|
1039
1681
|
].join('\n');
|
|
@@ -1050,12 +1692,18 @@ export async function analyzeScreen(appNameOrScreen, targetQuery) {
|
|
|
1050
1692
|
*/
|
|
1051
1693
|
export async function takeoverControl(appName, durationMs = 3000, x, y) {
|
|
1052
1694
|
const { category, name } = normalizeAppName(appName);
|
|
1695
|
+
const targetApp = (name && name !== 'desktop' && name !== 'system') ? name : undefined;
|
|
1696
|
+
if (targetApp) {
|
|
1697
|
+
setLiveScreenTargetApp(targetApp);
|
|
1698
|
+
}
|
|
1053
1699
|
if (name && name !== 'desktop' && name !== 'system' && category !== 'browser') {
|
|
1054
1700
|
await openApp(name);
|
|
1055
1701
|
}
|
|
1056
1702
|
// Trigger visual sky-blue aura overlay on screen borders & corners with warning badge & input block
|
|
1057
1703
|
try {
|
|
1058
|
-
|
|
1704
|
+
const auraLabel = targetApp ? `Scout is controlling ${targetApp}...` : "Scout is on the screen.";
|
|
1705
|
+
await showScreenAuraOverlay(Math.min(5000, Math.max(800, durationMs)), auraLabel, true);
|
|
1706
|
+
startLiveScreenShare({ targetApp }).catch(() => { });
|
|
1059
1707
|
}
|
|
1060
1708
|
catch { }
|
|
1061
1709
|
const mouseRes = await moveAndClickMouse(x, y, 'left', name || appName);
|
|
@@ -1539,60 +2187,69 @@ $m::mouse_event(0x0800, 0, 0, ${delta}, 0)
|
|
|
1539
2187
|
* Visually ground and click a target UI element by name or visual description.
|
|
1540
2188
|
*/
|
|
1541
2189
|
export async function findAndClickElement(elementQuery, appName, button = 'left') {
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
return { success: false, output: `Could not capture screen to locate element: ${capture.output}` };
|
|
1545
|
-
}
|
|
1546
|
-
const screenWidth = capture.details.width || 1920;
|
|
1547
|
-
const screenHeight = capture.details.height || 1080;
|
|
1548
|
-
const outPath = capture.details.outPath;
|
|
2190
|
+
let screenWidth = 1920;
|
|
2191
|
+
let screenHeight = 1080;
|
|
1549
2192
|
let targetX = undefined;
|
|
1550
2193
|
let targetY = undefined;
|
|
1551
|
-
|
|
1552
|
-
const
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
2194
|
+
// Use in-memory live screen frame first without writing screenshot files to disk
|
|
2195
|
+
const liveFrame = await getLiveScreenFrame();
|
|
2196
|
+
if (liveFrame) {
|
|
2197
|
+
screenWidth = liveFrame.width;
|
|
2198
|
+
screenHeight = liveFrame.height;
|
|
1556
2199
|
try {
|
|
1557
|
-
const
|
|
1558
|
-
const isGemini = Boolean(geminiKey);
|
|
1559
|
-
const baseURL = isGemini ? 'https://generativelanguage.googleapis.com/v1beta/openai' : undefined;
|
|
1560
|
-
const model = isGemini ? 'gemini-2.5-flash' : 'gpt-4o-mini';
|
|
1561
|
-
const client = new OpenAI({ apiKey, baseURL, timeout: 15000 });
|
|
1562
|
-
const prompt = `Look at this screenshot and locate the UI element: "${elementQuery}".
|
|
2200
|
+
const prompt = `Look at this screen and locate the UI element: "${elementQuery}".
|
|
1563
2201
|
Return ONLY a valid JSON object with the center point of this element in 0-1000 normalized coordinates:
|
|
1564
2202
|
{"point": [y, x]}
|
|
1565
2203
|
Where y is between 0 and 1000 (top to bottom) and x is between 0 and 1000 (left to right).
|
|
1566
2204
|
If not found, return {"point": null}. Output nothing else.`;
|
|
1567
|
-
const
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
});
|
|
1580
|
-
const reply = resp.choices[0]?.message?.content || '';
|
|
1581
|
-
const match = reply.match(/\{[\s\S]*"point"[\s\S]*\}/);
|
|
1582
|
-
if (match) {
|
|
1583
|
-
const parsed = JSON.parse(match[0]);
|
|
1584
|
-
if (Array.isArray(parsed.point) && parsed.point.length === 2) {
|
|
1585
|
-
const normY = Number(parsed.point[0]);
|
|
1586
|
-
const normX = Number(parsed.point[1]);
|
|
1587
|
-
if (!isNaN(normX) && !isNaN(normY)) {
|
|
1588
|
-
targetX = Math.round((normX / 1000) * screenWidth);
|
|
1589
|
-
targetY = Math.round((normY / 1000) * screenHeight);
|
|
2205
|
+
const reply = await callVisionAiWithFrame(liveFrame, prompt, 80);
|
|
2206
|
+
if (reply) {
|
|
2207
|
+
const match = reply.match(/\{[\s\S]*"point"[\s\S]*\}/);
|
|
2208
|
+
if (match) {
|
|
2209
|
+
const parsed = JSON.parse(match[0]);
|
|
2210
|
+
if (Array.isArray(parsed.point) && parsed.point.length === 2) {
|
|
2211
|
+
const normY = Number(parsed.point[0]);
|
|
2212
|
+
const normX = Number(parsed.point[1]);
|
|
2213
|
+
if (!isNaN(normX) && !isNaN(normY)) {
|
|
2214
|
+
targetX = Math.round((normX / 1000) * screenWidth);
|
|
2215
|
+
targetY = Math.round((normY / 1000) * screenHeight);
|
|
2216
|
+
}
|
|
1590
2217
|
}
|
|
1591
2218
|
}
|
|
1592
2219
|
}
|
|
1593
2220
|
}
|
|
1594
2221
|
catch { }
|
|
1595
2222
|
}
|
|
2223
|
+
else {
|
|
2224
|
+
const capture = await captureScreen(appName);
|
|
2225
|
+
if (capture.success && capture.details?.outPath && fs.existsSync(capture.details.outPath)) {
|
|
2226
|
+
screenWidth = capture.details.width || 1920;
|
|
2227
|
+
screenHeight = capture.details.height || 1080;
|
|
2228
|
+
try {
|
|
2229
|
+
const prompt = `Look at this screenshot and locate the UI element: "${elementQuery}".
|
|
2230
|
+
Return ONLY a valid JSON object with the center point of this element in 0-1000 normalized coordinates:
|
|
2231
|
+
{"point": [y, x]}
|
|
2232
|
+
Where y is between 0 and 1000 (top to bottom) and x is between 0 and 1000 (left to right).
|
|
2233
|
+
If not found, return {"point": null}. Output nothing else.`;
|
|
2234
|
+
const reply = await callVisionAi(capture.details.outPath, prompt, 80);
|
|
2235
|
+
if (reply) {
|
|
2236
|
+
const match = reply.match(/\{[\s\S]*"point"[\s\S]*\}/);
|
|
2237
|
+
if (match) {
|
|
2238
|
+
const parsed = JSON.parse(match[0]);
|
|
2239
|
+
if (Array.isArray(parsed.point) && parsed.point.length === 2) {
|
|
2240
|
+
const normY = Number(parsed.point[0]);
|
|
2241
|
+
const normX = Number(parsed.point[1]);
|
|
2242
|
+
if (!isNaN(normX) && !isNaN(normY)) {
|
|
2243
|
+
targetX = Math.round((normX / 1000) * screenWidth);
|
|
2244
|
+
targetY = Math.round((normY / 1000) * screenHeight);
|
|
2245
|
+
}
|
|
2246
|
+
}
|
|
2247
|
+
}
|
|
2248
|
+
}
|
|
2249
|
+
}
|
|
2250
|
+
catch { }
|
|
2251
|
+
}
|
|
2252
|
+
}
|
|
1596
2253
|
if ((targetX === undefined || targetY === undefined) && process.platform === 'win32') {
|
|
1597
2254
|
const psScript = `
|
|
1598
2255
|
Add-Type -AssemblyName UIAutomationClient
|