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.
@@ -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
- // Custom system app / binary
216
- const appExec = appNameOrPath;
217
- if (platform === 'win32') {
218
- command = cleanTarget ? `start "" "${appExec}" ${cleanTarget}` : `start "" "${appExec}"`;
219
- }
220
- else if (platform === 'darwin') {
221
- command = cleanTarget ? `open -a "${appExec}" "${cleanTarget}"` : `open -a "${appExec}"`;
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
- command = cleanTarget ? `${appExec} "${cleanTarget}" &` : `${appExec} &`;
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 analyzeScreen(name || appName, query);
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
- const targetUrl = rawUrl.startsWith('http://') || rawUrl.startsWith('https://') ? rawUrl : `https://${rawUrl}`;
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 encodedScript = Buffer.from(psScript, 'utf16le').toString('base64');
650
- const { stdout } = await execAsync(`powershell -NoProfile -EncodedCommand ${encodedScript}`);
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: data,
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 (err) {
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
- * during live screen capture and automation.
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" : " Perception Active";
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
- const psScript = `
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
- try {
928
- const encodedScript = Buffer.from(psScript, 'utf16le').toString('base64');
929
- const { stdout } = await execAsync(`powershell -NoProfile -EncodedCommand ${encodedScript}`);
930
- const meta = JSON.parse(stdout.trim());
931
- registerCapturedScreenshot(outPath);
932
- // Trigger visual sky-blue aura overlay on screen corners & borders
933
- showScreenAuraOverlay(1800, "Scout is viewing the screen...", false).catch(() => { });
934
- return {
935
- success: true,
936
- 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. (NOTE: This temporary screenshot will be automatically deleted upon task completion).`,
937
- details: { ...meta, outPath },
938
- };
939
- }
940
- catch (err) {
941
- return {
942
- success: false,
943
- output: `Failed to capture screen: ${err?.message || String(err)}`,
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 API key is present
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
- const geminiKey = process.env.GEMINI_API_KEY;
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 resp = await client.chat.completions.create({
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: "${capture.details?.activeWindow || info.details?.activeWindow || appNameOrScreen || 'Active Desktop'}"`,
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
- await showScreenAuraOverlay(Math.min(5000, Math.max(800, durationMs)), "Scout is on the screen.", true);
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
- const capture = await captureScreen(appName);
1543
- if (!capture.success || !capture.details?.outPath) {
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
- const geminiKey = process.env.GEMINI_API_KEY;
1552
- const openaiKey = process.env.OPENAI_API_KEY;
1553
- const byokKey = process.env.BYOK_API_KEY;
1554
- const apiKey = geminiKey || openaiKey || byokKey;
1555
- if (apiKey && fs.existsSync(outPath)) {
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 imageBase64 = fs.readFileSync(outPath).toString('base64');
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 resp = await client.chat.completions.create({
1568
- model,
1569
- messages: [
1570
- {
1571
- role: 'user',
1572
- content: [
1573
- { type: 'text', text: prompt },
1574
- { type: 'image_url', image_url: { url: `data:image/png;base64,${imageBase64}` } },
1575
- ],
1576
- },
1577
- ],
1578
- max_tokens: 80,
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