ft-scout 8.0.1 → 8.0.3

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