ft-scout 8.0.2 → 8.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -23,6 +23,9 @@ export function normalizeAppName(rawAppName) {
23
23
  if (['code', 'vscode', 'visual-studio-code', 'cursor', 'zed', 'sublime', 'notepad', 'notepad++', 'vim', 'nvim', 'nano', 'editor'].includes(appLower)) {
24
24
  return { category: 'editor', name: appLower || 'editor' };
25
25
  }
26
+ if (['game', 'games', 'gaming', 'play', 'launcher', 'steam', 'epic', 'epicgames', 'riot', 'battlenet', 'ea', 'ubisoft', 'gog'].includes(appLower) || appLower.startsWith('game:')) {
27
+ return { category: 'game', name: appLower };
28
+ }
26
29
  return { category: 'system', name: appLower };
27
30
  }
28
31
  export function parsePayload(payload) {
@@ -102,6 +105,78 @@ export function buildSocialUrl(platformOrAppName, target, text) {
102
105
  }
103
106
  return 'https://www.instagram.com/direct/inbox/';
104
107
  }
108
+ /**
109
+ * Universally search and locate any installed app, game, or store package on the host OS without hardcoding.
110
+ * Automatically resolves Win32 executables, Microsoft Store games, Steam games, and desktop shortcuts.
111
+ */
112
+ export async function findInstalledApp(query) {
113
+ const cleanQuery = (query || '').replace(/[^\w\s-]/g, '').trim().toLowerCase();
114
+ if (!cleanQuery)
115
+ return null;
116
+ const platform = process.platform;
117
+ if (platform === 'win32') {
118
+ try {
119
+ // Query Windows Get-StartApps with wildcard matching
120
+ const safePattern = `*${cleanQuery.replace(/[*_']/g, '')}*`;
121
+ const psCmd = `powershell.exe -NoProfile -NonInteractive -Command "Get-StartApps -Name '${safePattern}' | Select-Object -First 1 Name, AppID | ConvertTo-Json"`;
122
+ const { stdout } = await execAsync(psCmd, { timeout: 3500 });
123
+ if (stdout && stdout.trim()) {
124
+ const parsed = JSON.parse(stdout.trim());
125
+ if (parsed && (parsed.Name || parsed.AppID)) {
126
+ return {
127
+ name: String(parsed.Name || query),
128
+ appId: String(parsed.AppID || ''),
129
+ source: 'start_apps',
130
+ };
131
+ }
132
+ }
133
+ }
134
+ catch { }
135
+ // Fallback: check registered URI scheme protocol (e.g. steam, roblox, spotify, discord, minecraft, riot)
136
+ if (!cleanQuery.includes(' ') && cleanQuery.length >= 3) {
137
+ try {
138
+ const checkReg = `powershell.exe -NoProfile -NonInteractive -Command "Test-Path 'Registry::HKEY_CLASSES_ROOT\\${cleanQuery}'"`;
139
+ const { stdout } = await execAsync(checkReg, { timeout: 1500 });
140
+ if (stdout && stdout.trim().toLowerCase() === 'true') {
141
+ return {
142
+ name: query,
143
+ appId: `${cleanQuery}:`,
144
+ source: 'protocol',
145
+ };
146
+ }
147
+ }
148
+ catch { }
149
+ }
150
+ }
151
+ else if (platform === 'darwin') {
152
+ try {
153
+ const { stdout } = await execAsync(`mdfind "kMDItemKind == 'Application' && kMDItemDisplayName == '*${cleanQuery}*'c" | head -n 1`, { timeout: 3000 });
154
+ if (stdout && stdout.trim()) {
155
+ return {
156
+ name: path.basename(stdout.trim(), '.app'),
157
+ appId: stdout.trim(),
158
+ source: 'path',
159
+ };
160
+ }
161
+ }
162
+ catch { }
163
+ }
164
+ else {
165
+ // Linux: check which or desktop application entries
166
+ try {
167
+ const { stdout } = await execAsync(`which "${cleanQuery}" || find /usr/share/applications -name "*${cleanQuery}*.desktop" | head -n 1`, { timeout: 2500 });
168
+ if (stdout && stdout.trim()) {
169
+ return {
170
+ name: query,
171
+ appId: stdout.trim(),
172
+ source: 'path',
173
+ };
174
+ }
175
+ }
176
+ catch { }
177
+ }
178
+ return null;
179
+ }
105
180
  /**
106
181
  * Cross-platform app opener.
107
182
  * Launches and opens target application on Windows, macOS, or Linux.
@@ -212,16 +287,42 @@ export async function openApp(appNameOrPath, target, options) {
212
287
  }
213
288
  }
214
289
  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}"`;
290
+ // Universal Application, Game & Desktop Launcher (Zero-Hardcode OS Discovery)
291
+ const targetQuery = name || appNameOrPath;
292
+ const installedApp = await findInstalledApp(targetQuery);
293
+ if (installedApp) {
294
+ if (platform === 'win32') {
295
+ if (installedApp.source === 'protocol') {
296
+ const extraArg = cleanTarget ? ` "${cleanTarget}"` : '';
297
+ command = `start ${installedApp.appId}${extraArg}`;
298
+ }
299
+ else {
300
+ command = `explorer.exe "shell:AppsFolder\\${installedApp.appId}"`;
301
+ }
302
+ }
303
+ else if (platform === 'darwin') {
304
+ command = `open -a "${installedApp.appId || installedApp.name}"`;
305
+ }
306
+ else {
307
+ command = `"${installedApp.appId || installedApp.name}" &`;
308
+ }
222
309
  }
223
310
  else {
224
- command = cleanTarget ? `${appExec} "${cleanTarget}" &` : `${appExec} &`;
311
+ // Protocol scheme check (e.g. steam:, roblox:, spotify:, discord:, minecraft:, etc.)
312
+ const isProtocol = /^[a-zA-Z0-9_\-]+:/.test(appNameOrPath) || (cleanTarget && /^[a-zA-Z0-9_\-]+:/.test(cleanTarget));
313
+ if (isProtocol && platform === 'win32') {
314
+ const proto = /^[a-zA-Z0-9_\-]+:/.test(appNameOrPath) ? appNameOrPath : cleanTarget;
315
+ command = `start ${proto}`;
316
+ }
317
+ else if (platform === 'win32') {
318
+ command = cleanTarget ? `start "" "${appNameOrPath}" ${cleanTarget}` : `start "" "${appNameOrPath}"`;
319
+ }
320
+ else if (platform === 'darwin') {
321
+ command = cleanTarget ? `open -a "${appNameOrPath}" "${cleanTarget}"` : `open -a "${appNameOrPath}"`;
322
+ }
323
+ else {
324
+ command = cleanTarget ? `${appNameOrPath} "${cleanTarget}" &` : `${appNameOrPath} &`;
325
+ }
225
326
  }
226
327
  }
227
328
  try {
@@ -435,7 +536,19 @@ export async function executeInApp(appName, action, payload) {
435
536
  if (category === 'browser') {
436
537
  if (['navigate', 'goto', 'open_url'].includes(actLower)) {
437
538
  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}`;
539
+ let targetUrl = rawUrl.trim();
540
+ let navigationNote = '';
541
+ // Universal URL vs Search Query Grounding:
542
+ // If the input is not a valid URL (contains spaces or lacks a dot / scheme), convert into a search
543
+ if (!targetUrl.startsWith('http://') && !targetUrl.startsWith('https://')) {
544
+ if (targetUrl.includes(' ') || !targetUrl.includes('.')) {
545
+ navigationNote = ` [Note: Plain search term passed to navigate; converted to web search query]`;
546
+ targetUrl = `https://www.google.com/search?q=${encodeURIComponent(targetUrl)}`;
547
+ }
548
+ else {
549
+ targetUrl = `https://${targetUrl}`;
550
+ }
551
+ }
439
552
  await openApp(name || 'browser', targetUrl);
440
553
  await new Promise((r) => setTimeout(r, 800));
441
554
  const capture = await captureScreen(name || 'browser');
@@ -443,7 +556,7 @@ export async function executeInApp(appName, action, payload) {
443
556
  success: true,
444
557
  app: name || 'browser',
445
558
  action: actLower,
446
- output: `Browser Navigated Successfully to Target URL: ${targetUrl}\n- Screenshot Saved: ${capture.details?.outPath || 'Active'}`,
559
+ output: `Browser Navigated Successfully to Target URL: ${targetUrl}${navigationNote}\n- Screenshot Saved: ${capture.details?.outPath || 'Active'}`,
447
560
  details: { url: targetUrl, capture: capture.details },
448
561
  };
449
562
  }
@@ -557,6 +670,10 @@ export async function executeInApp(appName, action, payload) {
557
670
  return await openApp(name, filePath);
558
671
  }
559
672
  }
673
+ if (category === 'game' || ['open', 'launch', 'start'].includes(actLower)) {
674
+ const rawTarget = typeof payload === 'string' ? payload : (payload?.target || payload?.game || payload?.url || payload?.query || payload?.id || '');
675
+ return await openApp(name || appName, rawTarget);
676
+ }
560
677
  // Generic fallback app action
561
678
  return {
562
679
  success: true,
@@ -617,49 +734,291 @@ export function getAppCapabilities() {
617
734
  defaultEditorLauncher: platform === 'win32' ? 'code / notepad' : platform === 'darwin' ? 'code / open -a' : 'code / xdg-open',
618
735
  };
619
736
  }
737
+ const WIN_CAPTURE_SCRIPT_CONTENT = `param(
738
+ [string]$OutPath,
739
+ [switch]$InfoOnly
740
+ )
741
+
742
+ $source = @"
743
+ using System;
744
+ using System.Drawing;
745
+ using System.Drawing.Imaging;
746
+ using System.Runtime.InteropServices;
747
+ using System.Text;
748
+ using System.Threading;
749
+ using System.Collections.Generic;
750
+
751
+ public class WinCaptureResult {
752
+ public bool success;
753
+ public string savedPath;
754
+ public int width;
755
+ public int height;
756
+ public int cursorX;
757
+ public int cursorY;
758
+ public string activeWindow;
759
+ public string[] visibleWindows;
760
+ public string error;
761
+ }
762
+
763
+ public class WinCaptureHelper {
764
+ [DllImport("user32.dll", SetLastError = true)]
765
+ public static extern IntPtr OpenInputDesktop(uint dwFlags, bool fInherit, uint dwDesiredAccess);
766
+
767
+ [DllImport("user32.dll", SetLastError = true)]
768
+ public static extern IntPtr OpenDesktop(string lpszDesktop, uint dwFlags, bool fInherit, uint dwDesiredAccess);
769
+
770
+ [DllImport("user32.dll", SetLastError = true)]
771
+ public static extern bool SetThreadDesktop(IntPtr hDesktop);
772
+
773
+ [DllImport("user32.dll", SetLastError = true)]
774
+ public static extern bool CloseDesktop(IntPtr hDesktop);
775
+
776
+ [DllImport("user32.dll")]
777
+ public static extern IntPtr GetDC(IntPtr hWnd);
778
+
779
+ [DllImport("user32.dll")]
780
+ public static extern int ReleaseDC(IntPtr hWnd, IntPtr hDC);
781
+
782
+ [DllImport("gdi32.dll")]
783
+ public static extern IntPtr CreateCompatibleDC(IntPtr hdc);
784
+
785
+ [DllImport("gdi32.dll")]
786
+ public static extern IntPtr CreateCompatibleBitmap(IntPtr hdc, int nWidth, int nHeight);
787
+
788
+ [DllImport("gdi32.dll")]
789
+ public static extern IntPtr SelectObject(IntPtr hdc, IntPtr hgdiobj);
790
+
791
+ [DllImport("gdi32.dll")]
792
+ public static extern bool DeleteDC(IntPtr hdc);
793
+
794
+ [DllImport("gdi32.dll")]
795
+ public static extern bool DeleteObject(IntPtr hObject);
796
+
797
+ [DllImport("gdi32.dll")]
798
+ public static extern bool BitBlt(IntPtr hdcDest, int nXDest, int nYDest, int nWidth, int nHeight, IntPtr hdcSrc, int nXSrc, int nYSrc, int dwRop);
799
+
800
+ [DllImport("user32.dll")]
801
+ public static extern int GetSystemMetrics(int nIndex);
802
+
803
+ public delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
804
+
805
+ [DllImport("user32.dll")]
806
+ public static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam);
807
+
808
+ [DllImport("user32.dll", CharSet = CharSet.Auto)]
809
+ public static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount);
810
+
811
+ [DllImport("user32.dll")]
812
+ public static extern bool IsWindowVisible(IntPtr hWnd);
813
+
814
+ [DllImport("user32.dll")]
815
+ public static extern IntPtr GetForegroundWindow();
816
+
817
+ [StructLayout(LayoutKind.Sequential)]
818
+ public struct POINT {
819
+ public int X;
820
+ public int Y;
821
+ }
822
+
823
+ [DllImport("user32.dll")]
824
+ public static extern bool GetCursorPos(out POINT lpPoint);
825
+
826
+ const uint DESKTOP_ACCESS = 0x01FF | 0x00020000;
827
+ const int SM_CXSCREEN = 0;
828
+ const int SM_CYSCREEN = 1;
829
+ const int SRCCOPY = 0x00CC0020;
830
+ const int CAPTUREBLT = 0x40000000;
831
+
832
+ public static WinCaptureResult GetInfo() {
833
+ var res = new WinCaptureResult();
834
+ res.success = true;
835
+ res.error = "";
836
+ var visibleWindows = new List<string>();
837
+
838
+ Thread worker = new Thread(() => {
839
+ IntPtr hDesk = OpenInputDesktop(0, false, DESKTOP_ACCESS);
840
+ if (hDesk == IntPtr.Zero) {
841
+ hDesk = OpenDesktop("Default", 0, false, DESKTOP_ACCESS);
842
+ }
843
+ if (hDesk != IntPtr.Zero) {
844
+ SetThreadDesktop(hDesk);
845
+ }
846
+
847
+ res.width = GetSystemMetrics(SM_CXSCREEN);
848
+ res.height = GetSystemMetrics(SM_CYSCREEN);
849
+ if (res.width <= 0) res.width = 1920;
850
+ if (res.height <= 0) res.height = 1080;
851
+
852
+ POINT pt;
853
+ if (GetCursorPos(out pt)) {
854
+ res.cursorX = pt.X;
855
+ res.cursorY = pt.Y;
856
+ }
857
+
858
+ IntPtr fg = GetForegroundWindow();
859
+ if (fg != IntPtr.Zero) {
860
+ var sbFg = new StringBuilder(256);
861
+ GetWindowText(fg, sbFg, 256);
862
+ res.activeWindow = sbFg.ToString().Trim();
863
+ } else {
864
+ res.activeWindow = "";
865
+ }
866
+
867
+ EnumWindows((hWnd, lParam) => {
868
+ if (IsWindowVisible(hWnd)) {
869
+ var title = new StringBuilder(256);
870
+ GetWindowText(hWnd, title, 256);
871
+ string t = title.ToString().Trim();
872
+ if (t.Length > 0 && !visibleWindows.Contains(t) && t != "Program Manager" && t != "Windows Input Experience") {
873
+ visibleWindows.Add(t);
874
+ }
875
+ }
876
+ return true;
877
+ }, IntPtr.Zero);
878
+
879
+ if (hDesk != IntPtr.Zero) {
880
+ CloseDesktop(hDesk);
881
+ }
882
+ });
883
+
884
+ worker.SetApartmentState(ApartmentState.STA);
885
+ worker.Start();
886
+ worker.Join();
887
+
888
+ res.visibleWindows = visibleWindows.ToArray();
889
+ return res;
890
+ }
891
+
892
+ public static WinCaptureResult Capture(string filePath) {
893
+ var res = new WinCaptureResult();
894
+ res.savedPath = filePath;
895
+ res.error = "";
896
+ var visibleWindows = new List<string>();
897
+
898
+ Thread worker = new Thread(() => {
899
+ IntPtr hDesk = OpenInputDesktop(0, false, DESKTOP_ACCESS);
900
+ if (hDesk == IntPtr.Zero) {
901
+ hDesk = OpenDesktop("Default", 0, false, DESKTOP_ACCESS);
902
+ }
903
+ if (hDesk != IntPtr.Zero) {
904
+ SetThreadDesktop(hDesk);
905
+ }
906
+
907
+ res.width = GetSystemMetrics(SM_CXSCREEN);
908
+ res.height = GetSystemMetrics(SM_CYSCREEN);
909
+ if (res.width <= 0) res.width = 1920;
910
+ if (res.height <= 0) res.height = 1080;
911
+
912
+ POINT pt;
913
+ if (GetCursorPos(out pt)) {
914
+ res.cursorX = pt.X;
915
+ res.cursorY = pt.Y;
916
+ }
917
+
918
+ IntPtr fg = GetForegroundWindow();
919
+ if (fg != IntPtr.Zero) {
920
+ var sbFg = new StringBuilder(256);
921
+ GetWindowText(fg, sbFg, 256);
922
+ res.activeWindow = sbFg.ToString().Trim();
923
+ } else {
924
+ res.activeWindow = "";
925
+ }
926
+
927
+ EnumWindows((hWnd, lParam) => {
928
+ if (IsWindowVisible(hWnd)) {
929
+ var title = new StringBuilder(256);
930
+ GetWindowText(hWnd, title, 256);
931
+ string t = title.ToString().Trim();
932
+ if (t.Length > 0 && !visibleWindows.Contains(t) && t != "Program Manager" && t != "Windows Input Experience") {
933
+ visibleWindows.Add(t);
934
+ }
935
+ }
936
+ return true;
937
+ }, IntPtr.Zero);
938
+
939
+ try {
940
+ IntPtr hdcSrc = GetDC(IntPtr.Zero);
941
+ IntPtr hdcDest = CreateCompatibleDC(hdcSrc);
942
+ IntPtr hBitmap = CreateCompatibleBitmap(hdcSrc, res.width, res.height);
943
+ IntPtr hOld = SelectObject(hdcDest, hBitmap);
944
+ bool bltOk = BitBlt(hdcDest, 0, 0, res.width, res.height, hdcSrc, 0, 0, SRCCOPY | CAPTUREBLT);
945
+
946
+ SelectObject(hdcDest, hOld);
947
+ DeleteDC(hdcDest);
948
+ ReleaseDC(IntPtr.Zero, hdcSrc);
949
+
950
+ if (bltOk) {
951
+ using (Bitmap bmp = Bitmap.FromHbitmap(hBitmap)) {
952
+ bmp.Save(filePath, ImageFormat.Png);
953
+ }
954
+ res.success = true;
955
+ } else {
956
+ res.error = "BitBlt failed";
957
+ }
958
+ DeleteObject(hBitmap);
959
+ } catch (Exception ex) {
960
+ res.error = ex.Message;
961
+ }
962
+
963
+ if (hDesk != IntPtr.Zero) {
964
+ CloseDesktop(hDesk);
965
+ }
966
+ });
967
+
968
+ worker.SetApartmentState(ApartmentState.STA);
969
+ worker.Start();
970
+ worker.Join();
971
+
972
+ res.visibleWindows = visibleWindows.ToArray();
973
+ return res;
974
+ }
975
+ }
976
+ "@
977
+
978
+ Add-Type -TypeDefinition $source -ReferencedAssemblies System.Drawing, System.Windows.Forms
979
+
980
+ if ($InfoOnly) {
981
+ [WinCaptureHelper]::GetInfo() | ConvertTo-Json -Compress
982
+ } else {
983
+ [WinCaptureHelper]::Capture($OutPath) | ConvertTo-Json -Compress
984
+ }
985
+ `;
986
+ export function ensureWinCaptureScript() {
987
+ const scriptsDir = path.join(process.cwd(), '.ft', 'scripts');
988
+ if (!fs.existsSync(scriptsDir)) {
989
+ fs.mkdirSync(scriptsDir, { recursive: true });
990
+ }
991
+ const scriptPath = path.join(scriptsDir, 'winCapture.ps1');
992
+ fs.writeFileSync(scriptPath, WIN_CAPTURE_SCRIPT_CONTENT, 'utf8');
993
+ return scriptPath;
994
+ }
620
995
  /**
621
- * Get screen metrics, active window title, and cursor position.
996
+ * Get screen metrics, active window title, visible windows, and cursor position.
622
997
  */
623
998
  export async function getScreenInfo(appName) {
624
999
  const platform = process.platform;
625
1000
  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
1001
  try {
649
- const encodedScript = Buffer.from(psScript, 'utf16le').toString('base64');
650
- const { stdout } = await execAsync(`powershell -NoProfile -EncodedCommand ${encodedScript}`);
1002
+ const scriptPath = ensureWinCaptureScript();
1003
+ const { stdout } = await execAsync(`powershell -NoProfile -ExecutionPolicy Bypass -File "${scriptPath}" -InfoOnly`);
651
1004
  const data = JSON.parse(stdout.trim());
1005
+ const visList = Array.isArray(data.visibleWindows) && data.visibleWindows.length > 0
1006
+ ? ` | Visible Windows: ${data.visibleWindows.slice(0, 5).join(', ')}`
1007
+ : '';
652
1008
  return {
653
1009
  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,
1010
+ output: `Screen Info: Resolution ${data.screenWidth}x${data.screenHeight} | Cursor at (${data.cursorX}, ${data.cursorY}) | Active Window: "${data.activeWindow || appName || 'Desktop'}"${visList}`,
1011
+ details: {
1012
+ ...data,
1013
+ activeWindow: data.activeWindow || appName || 'Desktop',
1014
+ },
656
1015
  };
657
1016
  }
658
- catch (err) {
1017
+ catch {
659
1018
  return {
660
1019
  success: true,
661
1020
  output: `Screen Info (Default): Resolution 1920x1080 | Active Window: "${appName || 'Desktop'}"`,
662
- details: { screenWidth: 1920, screenHeight: 1080, activeWindow: appName || 'Desktop' },
1021
+ details: { screenWidth: 1920, screenHeight: 1080, activeWindow: appName || 'Desktop', cursorX: 0, cursorY: 0 },
663
1022
  };
664
1023
  }
665
1024
  }
@@ -897,51 +1256,56 @@ export async function captureScreen(appNameOrScreen) {
897
1256
  const filename = `screen_${Date.now()}.png`;
898
1257
  const outPath = path.join(screenshotsDir, filename);
899
1258
  if (platform === 'win32') {
900
- const psScript = `
1259
+ try {
1260
+ const scriptPath = ensureWinCaptureScript();
1261
+ const { stdout } = await execAsync(`powershell -NoProfile -ExecutionPolicy Bypass -File "${scriptPath}" -OutPath "${outPath}"`);
1262
+ const meta = JSON.parse(stdout.trim());
1263
+ if (meta.success && fs.existsSync(outPath) && fs.statSync(outPath).size > 0) {
1264
+ registerCapturedScreenshot(outPath);
1265
+ showScreenAuraOverlay(1800, "Scout is viewing the screen...", false).catch(() => { });
1266
+ const visibleList = Array.isArray(meta.visibleWindows) && meta.visibleWindows.length > 0
1267
+ ? `\n- Visible Windows: ${meta.visibleWindows.slice(0, 6).map((w) => `"${w}"`).join(', ')}`
1268
+ : '';
1269
+ return {
1270
+ success: true,
1271
+ output: `Screen Captured Successfully!\n- Screenshot Saved To: ${outPath}\n- Resolution: ${meta.width}x${meta.height}\n- Active Window: "${meta.activeWindow || 'Desktop'}"${visibleList}\n- Seeing Status: Screen is visible and captured. (NOTE: This temporary screenshot will be automatically deleted upon task completion).`,
1272
+ details: { ...meta, outPath },
1273
+ };
1274
+ }
1275
+ throw new Error(meta.error || 'Desktop capture returned false');
1276
+ }
1277
+ catch (err) {
1278
+ // Fallback to Graphics.CopyFromScreen
1279
+ const psScript = `
901
1280
  Add-Type -AssemblyName System.Windows.Forms
902
1281
  Add-Type -AssemblyName System.Drawing
903
-
904
1282
  $screen = [System.Windows.Forms.Screen]::PrimaryScreen
905
1283
  $bounds = $screen.Bounds
906
1284
  $bmp = New-Object System.Drawing.Bitmap($bounds.Width, $bounds.Height)
907
1285
  $graphics = [System.Drawing.Graphics]::FromImage($bmp)
908
- $graphics.CopyFromScreen($bounds.Location, [System.Drawing.Point]::Empty, $bounds.Size)
1286
+ try { $graphics.CopyFromScreen($bounds.Location, [System.Drawing.Point]::Empty, $bounds.Size) } catch {}
909
1287
  $bmp.Save("${outPath.replace(/\\/g, '\\\\')}", [System.Drawing.Imaging.ImageFormat]::Png)
910
1288
  $graphics.Dispose()
911
1289
  $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
1290
+ @{ savedPath = "${outPath.replace(/\\/g, '\\\\')}"; width = $bounds.Width; height = $bounds.Height; activeWindow = "${appNameOrScreen || 'Desktop'}" } | ConvertTo-Json
926
1291
  `;
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
- };
1292
+ try {
1293
+ const encodedScript = Buffer.from(psScript, 'utf16le').toString('base64');
1294
+ const { stdout } = await execAsync(`powershell -NoProfile -EncodedCommand ${encodedScript}`);
1295
+ const meta = JSON.parse(stdout.trim());
1296
+ registerCapturedScreenshot(outPath);
1297
+ return {
1298
+ success: true,
1299
+ output: `Screen Captured Successfully!\n- Screenshot Saved To: ${outPath}\n- Resolution: ${meta.width}x${meta.height}\n- Active Window: "${meta.activeWindow || 'Desktop'}"\n- Seeing Status: Screen is visible and captured.`,
1300
+ details: { ...meta, outPath },
1301
+ };
1302
+ }
1303
+ catch (fallbackErr) {
1304
+ return {
1305
+ success: false,
1306
+ output: `Failed to capture screen: ${err?.message || fallbackErr?.message || String(err)}`,
1307
+ };
1308
+ }
945
1309
  }
946
1310
  }
947
1311
  else if (platform === 'darwin') {
@@ -979,6 +1343,64 @@ $activeTitle = $sb.ToString()
979
1343
  }
980
1344
  }
981
1345
  }
1346
+ /**
1347
+ * Call Vision AI with graceful multi-provider fallback.
1348
+ * Tries NVIDIA Vision (meta/llama-3.2-11b-vision-instruct), Gemini (gemini-2.0-flash), OpenAI (gpt-4o-mini), and BYOK.
1349
+ */
1350
+ export async function callVisionAi(outPath, prompt, maxTokens = 350) {
1351
+ if (!outPath || !fs.existsSync(outPath))
1352
+ return null;
1353
+ let imageBase64;
1354
+ try {
1355
+ imageBase64 = fs.readFileSync(outPath).toString('base64');
1356
+ }
1357
+ catch {
1358
+ return null;
1359
+ }
1360
+ const nvidiaKey = process.env.NVIDIA_API_KEY || process.env.NVCF_API_KEY;
1361
+ const geminiKey = process.env.GEMINI_API_KEY;
1362
+ const openaiKey = process.env.OPENAI_API_KEY;
1363
+ const byokKey = process.env.BYOK_API_KEY;
1364
+ const candidates = [];
1365
+ if (nvidiaKey) {
1366
+ candidates.push({ apiKey: nvidiaKey, baseURL: 'https://integrate.api.nvidia.com/v1', model: 'meta/llama-3.2-11b-vision-instruct', timeout: 35000 });
1367
+ }
1368
+ if (geminiKey) {
1369
+ candidates.push({ apiKey: geminiKey, baseURL: 'https://generativelanguage.googleapis.com/v1beta/openai', model: 'gemini-2.0-flash', timeout: 20000 });
1370
+ }
1371
+ if (openaiKey) {
1372
+ candidates.push({ apiKey: openaiKey, model: 'gpt-4o-mini', timeout: 20000 });
1373
+ }
1374
+ if (byokKey) {
1375
+ candidates.push({ apiKey: byokKey, model: 'gpt-4o-mini', timeout: 20000 });
1376
+ }
1377
+ for (const cand of candidates) {
1378
+ try {
1379
+ const client = new OpenAI({ apiKey: cand.apiKey, baseURL: cand.baseURL, timeout: cand.timeout });
1380
+ const resp = await client.chat.completions.create({
1381
+ model: cand.model,
1382
+ messages: [
1383
+ {
1384
+ role: 'user',
1385
+ content: [
1386
+ { type: 'text', text: prompt },
1387
+ { type: 'image_url', image_url: { url: `data:image/png;base64,${imageBase64}` } },
1388
+ ],
1389
+ },
1390
+ ],
1391
+ max_tokens: maxTokens,
1392
+ });
1393
+ const reply = resp.choices[0]?.message?.content?.trim();
1394
+ if (reply) {
1395
+ return reply;
1396
+ }
1397
+ }
1398
+ catch {
1399
+ continue;
1400
+ }
1401
+ }
1402
+ return null;
1403
+ }
982
1404
  /**
983
1405
  * Visual screen analysis. Takes a full-screen screenshot and provides visual context
984
1406
  * (including AI Vision inspection if an API key is available).
@@ -989,36 +1411,14 @@ export async function analyzeScreen(appNameOrScreen, targetQuery) {
989
1411
  const info = await getScreenInfo(appNameOrScreen);
990
1412
  let visualAiAnalysis = '';
991
1413
  const outPath = capture.details?.outPath;
992
- // Optional Vision Model Analysis if screenshot exists and API key is present
1414
+ // Optional Vision Model Analysis if screenshot exists and has valid content
993
1415
  if (capture.success && outPath && fs.existsSync(outPath)) {
994
1416
  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 });
1417
+ if (fs.statSync(outPath).size > 0) {
1005
1418
  const prompt = targetQuery
1006
1419
  ? `Describe what is visible on this screen specifically regarding: "${targetQuery}". Keep it concise, noting visible UI elements, open windows, tabs, buttons, or error messages.`
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;
1420
+ : `Briefly describe what is currently visible on this desktop screen: active window, main content/tabs, buttons, and system state in 2-3 sentences.`;
1421
+ const reply = await callVisionAi(outPath, prompt, 350);
1022
1422
  if (reply && reply.trim()) {
1023
1423
  visualAiAnalysis = `\nAI Visual Screen Analysis:\n${reply.trim()}`;
1024
1424
  }
@@ -1028,12 +1428,19 @@ export async function analyzeScreen(appNameOrScreen, targetQuery) {
1028
1428
  // Vision API call is optional; fallback gracefully to system metadata
1029
1429
  }
1030
1430
  }
1431
+ const activeWin = capture.details?.activeWindow || info.details?.activeWindow || appNameOrScreen || 'Active Desktop';
1432
+ const visWins = Array.isArray(capture.details?.visibleWindows) && capture.details.visibleWindows.length > 0
1433
+ ? capture.details.visibleWindows
1434
+ : (Array.isArray(info.details?.visibleWindows) ? info.details.visibleWindows : []);
1435
+ const visibleWindowsText = visWins.length > 0
1436
+ ? `\nVisible Open Windows:\n${visWins.slice(0, 8).map((w) => ` - "${w}"`).join('\n')}`
1437
+ : '';
1031
1438
  const report = [
1032
1439
  `=== SCOUT SCRATCHPAD DESKTOP SCREEN VISUAL INSPECTION ===`,
1033
1440
  `Target App / Screen Context: "${appNameOrScreen || 'Desktop'}"`,
1034
1441
  `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})`,
1442
+ `Active Window Focus: "${activeWin}"${visibleWindowsText}`,
1443
+ `Mouse Cursor Position: (${info.details?.cursorX || capture.details?.cursorX || 0}, ${info.details?.cursorY || capture.details?.cursorY || 0})`,
1037
1444
  `Screenshot Status: Captured temporarily for visual inspection. (Will be automatically deleted upon task completion)`,
1038
1445
  `Visual Status: Screen is visible and verified.${visualAiAnalysis}`,
1039
1446
  ].join('\n');
@@ -1548,45 +1955,25 @@ export async function findAndClickElement(elementQuery, appName, button = 'left'
1548
1955
  const outPath = capture.details.outPath;
1549
1956
  let targetX = undefined;
1550
1957
  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)) {
1958
+ if (fs.existsSync(outPath)) {
1556
1959
  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
1960
  const prompt = `Look at this screenshot and locate the UI element: "${elementQuery}".
1563
1961
  Return ONLY a valid JSON object with the center point of this element in 0-1000 normalized coordinates:
1564
1962
  {"point": [y, x]}
1565
1963
  Where y is between 0 and 1000 (top to bottom) and x is between 0 and 1000 (left to right).
1566
1964
  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);
1965
+ const reply = await callVisionAi(outPath, prompt, 80);
1966
+ if (reply) {
1967
+ const match = reply.match(/\{[\s\S]*"point"[\s\S]*\}/);
1968
+ if (match) {
1969
+ const parsed = JSON.parse(match[0]);
1970
+ if (Array.isArray(parsed.point) && parsed.point.length === 2) {
1971
+ const normY = Number(parsed.point[0]);
1972
+ const normX = Number(parsed.point[1]);
1973
+ if (!isNaN(normX) && !isNaN(normY)) {
1974
+ targetX = Math.round((normX / 1000) * screenWidth);
1975
+ targetY = Math.round((normY / 1000) * screenHeight);
1976
+ }
1590
1977
  }
1591
1978
  }
1592
1979
  }