ft-scout 8.0.0 → 8.0.2

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.
@@ -80,6 +84,16 @@ export function buildSocialUrl(platformOrAppName, target, text) {
80
84
  if (isLinkedIn) {
81
85
  return `https://www.linkedin.com/messaging/`;
82
86
  }
87
+ const isEmail = pLower.includes('gmail') || pLower.includes('mail') || pLower.includes('email') || (cleanTarget.includes('@') && cleanTarget.includes('.')) || cleanTarget.includes('mail.google.com');
88
+ if (isEmail) {
89
+ const to = cleanTarget.includes('@') && !cleanTarget.startsWith('http') ? cleanTarget : (parsedPayload.to || parsedPayload.recipient || '');
90
+ const subject = parsedPayload.subject || parsedPayload.su || 'Message from Scout';
91
+ const body = rawMsgText || parsedPayload.body || '';
92
+ if (to || body) {
93
+ return `https://mail.google.com/mail/?view=cm&fs=1${to ? `&to=${encodeURIComponent(to)}` : ''}${subject ? `&su=${encodeURIComponent(subject)}` : ''}${body ? `&body=${encodeURIComponent(body)}` : ''}`;
94
+ }
95
+ return 'https://mail.google.com';
96
+ }
83
97
  if (cleanTarget.startsWith('http://') || cleanTarget.startsWith('https://')) {
84
98
  return cleanTarget;
85
99
  }
@@ -261,7 +275,7 @@ export async function executeInApp(appName, action, payload) {
261
275
  output: res.output,
262
276
  };
263
277
  }
264
- if (['capture_screen', 'screenshot', 'see_screen', 'inspect_screen', 'screen_shot', 'view_screen'].includes(actLower)) {
278
+ if (['capture_screen', 'screenshot', 'screen_shot'].includes(actLower)) {
265
279
  const res = await captureScreen(name || appName);
266
280
  return {
267
281
  success: res.success,
@@ -271,8 +285,9 @@ export async function executeInApp(appName, action, payload) {
271
285
  details: res.details,
272
286
  };
273
287
  }
274
- if (['analyze_screen', 'inspect_desktop', 'visual_analysis', 'see_and_analyze'].includes(actLower)) {
275
- const res = await analyzeScreen(name || appName);
288
+ if (['see_screen', 'inspect_screen', 'view_screen', 'analyze_screen', 'inspect_desktop', 'visual_analysis', 'see_and_analyze'].includes(actLower)) {
289
+ const query = typeof payload === 'string' ? payload : (payload?.query || payload?.target || payload?.text || undefined);
290
+ const res = await analyzeScreen(name || appName, query);
276
291
  return {
277
292
  success: res.success,
278
293
  app: name || appName,
@@ -293,9 +308,26 @@ export async function executeInApp(appName, action, payload) {
293
308
  }
294
309
  if (['type_text', 'send_keys', 'type', 'keystrokes', 'key_combo', 'press_hotkey', 'hotkey'].includes(actLower)) {
295
310
  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);
311
+ const isHotKeyAction = ['key_combo', 'press_hotkey', 'hotkey', 'send_keys'].includes(actLower);
312
+ let keyCombo = undefined;
313
+ let textToType = '';
314
+ if (typeof parsed === 'object') {
315
+ keyCombo = parsed.keyCombo || parsed.combo || parsed.hotkey || parsed.keys || parsed.key;
316
+ textToType = parsed.text || parsed.content || parsed.message || '';
317
+ }
318
+ if (!keyCombo && typeof payload === 'string') {
319
+ const trimmed = payload.trim();
320
+ if (isHotKeyAction || trimmed.includes('+') || ['enter', 'return', 'tab', 'esc', 'escape', 'backspace', 'space', 'up', 'down', 'left', 'right'].includes(trimmed.toLowerCase())) {
321
+ keyCombo = trimmed;
322
+ }
323
+ else {
324
+ textToType = trimmed;
325
+ }
326
+ }
327
+ else if (!textToType && typeof payload === 'string' && !payload.trim().startsWith('{')) {
328
+ textToType = payload;
329
+ }
330
+ const pressEnter = typeof parsed === 'object' && parsed.enter !== undefined ? parsed.enter : !isHotKeyAction;
299
331
  const ksRes = await sendKeystrokes(name || appName, textToType, pressEnter, keyCombo);
300
332
  return {
301
333
  success: ksRes.success,
@@ -305,14 +337,34 @@ export async function executeInApp(appName, action, payload) {
305
337
  details: { appName: name || appName, textToType, pressEnter, keyCombo },
306
338
  };
307
339
  }
308
- if (['click', 'click_app', 'click_at', 'move_mouse', 'drag_mouse', 'scroll'].includes(actLower)) {
340
+ if (['click_element', 'click_ui', 'click_button', 'tap_element'].includes(actLower)) {
341
+ const parsed = parsePayload(payload);
342
+ const query = typeof parsed === 'object' ? (parsed.element || parsed.target || parsed.name || parsed.query || '') : String(parsed || payload);
343
+ const button = typeof parsed === 'object' && parsed.button ? parsed.button : 'left';
344
+ const res = await findAndClickElement(query, name || appName, button);
345
+ return {
346
+ success: res.success,
347
+ app: name || appName,
348
+ action: actLower,
349
+ output: res.output,
350
+ details: res.coordinates,
351
+ };
352
+ }
353
+ if (['click', 'click_app', 'click_at', 'move_mouse', 'drag_mouse'].includes(actLower)) {
309
354
  const parsedPayload = parsePayload(payload);
355
+ if (typeof parsedPayload === 'object' && parsedPayload.element && parsedPayload.x === undefined) {
356
+ const res = await findAndClickElement(parsedPayload.element, name || appName, parsedPayload.button || 'left');
357
+ return {
358
+ success: res.success,
359
+ app: name || appName,
360
+ action: actLower,
361
+ output: res.output,
362
+ details: res.coordinates,
363
+ };
364
+ }
310
365
  const x = typeof parsedPayload.x === 'number' ? parsedPayload.x : undefined;
311
366
  const y = typeof parsedPayload.y === 'number' ? parsedPayload.y : undefined;
312
367
  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
368
  const clickRes = await moveAndClickMouse(x, y, button, name || appName);
317
369
  return {
318
370
  success: clickRes.success,
@@ -322,6 +374,52 @@ export async function executeInApp(appName, action, payload) {
322
374
  details: { appName: name || appName, x, y, button },
323
375
  };
324
376
  }
377
+ if (['scroll', 'scroll_window', 'scroll_down', 'scroll_up', 'scroll_page'].includes(actLower)) {
378
+ const parsed = parsePayload(payload);
379
+ let direction = 'down';
380
+ let amount = 3;
381
+ if (typeof parsed === 'object') {
382
+ if (parsed.direction === 'up' || parsed.dir === 'up')
383
+ direction = 'up';
384
+ if (typeof parsed.amount === 'number')
385
+ amount = parsed.amount;
386
+ if (typeof parsed.clicks === 'number')
387
+ amount = parsed.clicks;
388
+ }
389
+ else if (typeof payload === 'string' && payload.toLowerCase().includes('up')) {
390
+ direction = 'up';
391
+ }
392
+ if (actLower === 'scroll_up')
393
+ direction = 'up';
394
+ const res = await scrollWindow(direction, amount);
395
+ return {
396
+ success: res.success,
397
+ app: name || appName,
398
+ action: actLower,
399
+ output: res.output,
400
+ };
401
+ }
402
+ if (['list_windows', 'get_windows', 'open_windows', 'show_windows'].includes(actLower)) {
403
+ const res = await listWindows();
404
+ return {
405
+ success: res.success,
406
+ app: name || appName,
407
+ action: actLower,
408
+ output: res.output,
409
+ details: res.windows,
410
+ };
411
+ }
412
+ if (['focus_window', 'switch_window', 'bring_to_front', 'select_window'].includes(actLower)) {
413
+ const parsed = parsePayload(payload);
414
+ const target = typeof parsed === 'object' ? (parsed.title || parsed.app || parsed.name || '') : String(parsed || payload || name || appName);
415
+ const res = await focusWindow(target);
416
+ return {
417
+ success: res.success,
418
+ app: target,
419
+ action: actLower,
420
+ output: res.output,
421
+ };
422
+ }
325
423
  if (['lock_app', 'block_interruption', 'lock_input', 'lock'].includes(actLower)) {
326
424
  const parsedPayload = parsePayload(payload);
327
425
  const durationMs = typeof parsedPayload.duration === 'number' ? parsedPayload.duration : 3000;
@@ -394,16 +492,27 @@ export async function executeInApp(appName, action, payload) {
394
492
  details: { query, searchUrl, capture: capture.details },
395
493
  };
396
494
  }
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')) {
495
+ if (['send_dm', 'open_dm', 'dm', 'send_message', 'message', 'social_dm', 'send_mail', 'send_email', 'mail', 'email'].includes(actLower)) {
496
+ const parsed = parsePayload(payload);
497
+ const recipient = typeof payload === 'string' && payload.includes('@') && !payload.startsWith('{')
498
+ ? payload
499
+ : (parsed.recipient || parsed.to || parsed.user || parsed.target || parsed.handle || parsed.username || '');
500
+ let msgText = typeof parsed === 'object' ? (parsed.text || parsed.message || parsed.content || parsed.body) : undefined;
501
+ if (!msgText && typeof payload === 'string' && payload.length > 0 && !payload.startsWith('@') && !payload.startsWith('http') && !payload.includes('@')) {
401
502
  msgText = payload;
402
503
  }
403
- const targetUrl = buildSocialUrl(name, recipient, msgText);
504
+ const isEmail = ['send_mail', 'send_email', 'mail', 'email'].includes(actLower) || recipient.includes('@');
505
+ const targetUrl = buildSocialUrl(isEmail ? 'gmail' : name, recipient, msgText);
404
506
  const openRes = await openApp('browser', targetUrl);
405
507
  let keystrokeOutput = '';
406
- if (msgText) {
508
+ if (isEmail) {
509
+ // For Gmail direct compose: wait 2.5s for page & compose dialog to render, then press Ctrl+Enter to send!
510
+ await lockAppInput('browser', 3500);
511
+ await new Promise((r) => setTimeout(r, 2200));
512
+ const ksRes = await sendKeystrokes('browser', '', false, 'ctrl+enter');
513
+ keystrokeOutput = `\nAuto-Send Keystroke Dispatch (Ctrl+Enter): ${ksRes.output}`;
514
+ }
515
+ else if (msgText) {
407
516
  // Lock user interruption during scratchpad keystroke dispatch so user clicks don't interrupt
408
517
  await lockAppInput(name || 'browser', 3000);
409
518
  await new Promise((r) => setTimeout(r, 1200));
@@ -414,8 +523,8 @@ export async function executeInApp(appName, action, payload) {
414
523
  success: true,
415
524
  app: name,
416
525
  action: actLower,
417
- output: `Opened ${name} Direct Message UI for target recipient "${recipient}" at ${targetUrl}.${keystrokeOutput}`,
418
- details: { platform: name, recipient, msgText, targetUrl },
526
+ output: `Executed ${isEmail ? 'Gmail / Email Dispatch' : `${name} Direct Message`} for target recipient "${recipient}" at ${targetUrl}.${keystrokeOutput}`,
527
+ details: { platform: isEmail ? 'gmail' : name, recipient, msgText, targetUrl },
419
528
  };
420
529
  }
421
530
  }
@@ -562,8 +671,219 @@ $activeTitle = $sb.ToString()
562
671
  };
563
672
  }
564
673
  }
674
+ /**
675
+ * Display a futuristic sky-blue aura animation on screen borders and corners,
676
+ * show the warning "Scout is on the screen.", and block external inputs
677
+ * during live screen capture and automation.
678
+ */
679
+ export async function showScreenAuraOverlay(durationMs = 2200, message = "Scout is viewing the screen.", blockInput = false) {
680
+ const platform = process.platform;
681
+ if (platform !== 'win32')
682
+ return;
683
+ try {
684
+ const scriptsDir = path.join(process.cwd(), '.ft', 'scripts');
685
+ if (!fs.existsSync(scriptsDir)) {
686
+ fs.mkdirSync(scriptsDir, { recursive: true });
687
+ }
688
+ const psFile = path.join(scriptsDir, 'scout_aura_overlay.ps1');
689
+ const blockInputSnippet = blockInput ? `
690
+ $sig = '[DllImport("user32.dll")] public static extern bool BlockInput(bool fBlock);'
691
+ $b = Add-Type -memberDefinition $sig -name 'Win32AuraBlock' -namespace Win32Functions -passThru
692
+ try { [void]$b::BlockInput($true) } catch {}
693
+ ` : '';
694
+ const unblockSnippet = blockInput ? `
695
+ try { [void]$b::BlockInput($false) } catch {}
696
+ ` : '';
697
+ const subtitleText = blockInput ? " • External Input Locked" : " • Perception Active";
698
+ const overlayPs = `
699
+ Add-Type -AssemblyName PresentationFramework, PresentationCore, WindowsBase
700
+ ${blockInputSnippet}
701
+
702
+ [xml]$xaml = @"
703
+ <Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
704
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
705
+ Title="Scout Aura Overlay"
706
+ WindowStyle="None"
707
+ AllowsTransparency="True"
708
+ Background="Transparent"
709
+ Topmost="True"
710
+ ShowInTaskbar="False"
711
+ ShowActivated="False"
712
+ IsHitTestVisible="False">
713
+ <Window.Resources>
714
+ <Storyboard x:Key="AuraPulse" RepeatBehavior="Forever" AutoReverse="True">
715
+ <DoubleAnimation Storyboard.TargetName="AuraBorder" Storyboard.TargetProperty="Opacity"
716
+ From="0.4" To="1.0" Duration="0:0:0.6"/>
717
+ <DoubleAnimation Storyboard.TargetName="CornerTL" Storyboard.TargetProperty="Opacity"
718
+ From="0.5" To="1.0" Duration="0:0:0.6"/>
719
+ <DoubleAnimation Storyboard.TargetName="CornerTR" Storyboard.TargetProperty="Opacity"
720
+ From="0.5" To="1.0" Duration="0:0:0.6"/>
721
+ <DoubleAnimation Storyboard.TargetName="CornerBL" Storyboard.TargetProperty="Opacity"
722
+ From="0.5" To="1.0" Duration="0:0:0.6"/>
723
+ <DoubleAnimation Storyboard.TargetName="CornerBR" Storyboard.TargetProperty="Opacity"
724
+ From="0.5" To="1.0" Duration="0:0:0.6"/>
725
+ <DoubleAnimation Storyboard.TargetName="AuraGlow" Storyboard.TargetProperty="Opacity"
726
+ From="0.25" To="0.9" Duration="0:0:0.6"/>
727
+ </Storyboard>
728
+ </Window.Resources>
729
+
730
+ <Grid IsHitTestVisible="False">
731
+ <!-- Outer Glowing Sky Blue Aura Border -->
732
+ <Border x:Name="AuraGlow" BorderThickness="6" BorderBrush="#00d8ff" Margin="0">
733
+ <Border.Effect>
734
+ <DropShadowEffect Color="#00e5ff" BlurRadius="45" ShadowDepth="0" Opacity="0.95"/>
735
+ </Border.Effect>
736
+ </Border>
737
+ <Border x:Name="AuraBorder" BorderThickness="3" BorderBrush="#38bdf8" Margin="0">
738
+ <Border.Effect>
739
+ <DropShadowEffect Color="#00e5ff" BlurRadius="25" ShadowDepth="0" Opacity="1"/>
740
+ </Border.Effect>
741
+ </Border>
742
+
743
+ <!-- Sky Blue Corner Brackets with Aura Glow -->
744
+ <!-- Top Left Corner -->
745
+ <Canvas HorizontalAlignment="Left" VerticalAlignment="Top" Width="120" Height="120" Margin="6,6,0,0">
746
+ <Path x:Name="CornerTL" Data="M 0,90 L 0,0 L 90,0" Stroke="#00e5ff" StrokeThickness="6">
747
+ <Path.Effect>
748
+ <DropShadowEffect Color="#00e5ff" BlurRadius="30" ShadowDepth="0" Opacity="1"/>
749
+ </Path.Effect>
750
+ </Path>
751
+ </Canvas>
752
+
753
+ <!-- Top Right Corner -->
754
+ <Canvas HorizontalAlignment="Right" VerticalAlignment="Top" Width="120" Height="120" Margin="0,6,6,0">
755
+ <Path x:Name="CornerTR" Data="M 30,0 L 120,0 L 120,90" Stroke="#00e5ff" StrokeThickness="6">
756
+ <Path.Effect>
757
+ <DropShadowEffect Color="#00e5ff" BlurRadius="30" ShadowDepth="0" Opacity="1"/>
758
+ </Path.Effect>
759
+ </Path>
760
+ </Canvas>
761
+
762
+ <!-- Bottom Left Corner -->
763
+ <Canvas HorizontalAlignment="Left" VerticalAlignment="Bottom" Width="120" Height="120" Margin="6,0,0,6">
764
+ <Path x:Name="CornerBL" Data="M 0,30 L 0,120 L 90,120" Stroke="#00e5ff" StrokeThickness="6">
765
+ <Path.Effect>
766
+ <DropShadowEffect Color="#00e5ff" BlurRadius="30" ShadowDepth="0" Opacity="1"/>
767
+ </Path.Effect>
768
+ </Path>
769
+ </Canvas>
770
+
771
+ <!-- Bottom Right Corner -->
772
+ <Canvas HorizontalAlignment="Right" VerticalAlignment="Bottom" Width="120" Height="120" Margin="0,0,6,6">
773
+ <Path x:Name="CornerBR" Data="M 30,120 L 120,120 L 120,30" Stroke="#00e5ff" StrokeThickness="6">
774
+ <Path.Effect>
775
+ <DropShadowEffect Color="#00e5ff" BlurRadius="30" ShadowDepth="0" Opacity="1"/>
776
+ </Path.Effect>
777
+ </Path>
778
+ </Canvas>
779
+
780
+ <!-- Top Floating Status Badge -->
781
+ <Border HorizontalAlignment="Center" VerticalAlignment="Top" Margin="0,22,0,0"
782
+ Background="#EE03121F" BorderBrush="#00e5ff" BorderThickness="1.8" CornerRadius="24" Padding="26,10">
783
+ <Border.Effect>
784
+ <DropShadowEffect Color="#00e5ff" BlurRadius="25" ShadowDepth="0" Opacity="0.95"/>
785
+ </Border.Effect>
786
+ <StackPanel Orientation="Horizontal" VerticalAlignment="Center">
787
+ <Ellipse Width="12" Height="12" Fill="#00e5ff" Margin="0,0,12,0">
788
+ <Ellipse.Effect>
789
+ <DropShadowEffect Color="#00e5ff" BlurRadius="12" ShadowDepth="0"/>
790
+ </Ellipse.Effect>
791
+ </Ellipse>
792
+ <TextBlock Text="${message}" Foreground="#ffffff" FontSize="16" FontWeight="Bold" VerticalAlignment="Center"/>
793
+ <TextBlock Text="${subtitleText}" Foreground="#38bdf8" FontSize="13.5" FontWeight="SemiBold" Margin="10,0,0,0" VerticalAlignment="Center"/>
794
+ </StackPanel>
795
+ </Border>
796
+ </Grid>
797
+ </Window>
798
+ "@
799
+
800
+ $reader = (New-Object System.Xml.XmlNodeReader $xaml)
801
+ $window = [System.Windows.Markup.XamlReader]::Load($reader)
802
+
803
+ $window.Left = [System.Windows.SystemParameters]::VirtualScreenLeft
804
+ $window.Top = [System.Windows.SystemParameters]::VirtualScreenTop
805
+ $window.Width = [System.Windows.SystemParameters]::VirtualScreenWidth
806
+ $window.Height = [System.Windows.SystemParameters]::VirtualScreenHeight
807
+ $window.Topmost = $true
808
+ $window.ShowActivated = $false
809
+
810
+ $storyboard = $window.Resources["AuraPulse"]
811
+ $window.Add_Loaded({
812
+ $storyboard.Begin($window)
813
+ })
814
+
815
+ # Safety timer to close overlay and guarantee unblock
816
+ $timer = New-Object System.Windows.Threading.DispatcherTimer
817
+ $timer.Interval = [TimeSpan]::FromMilliseconds(${durationMs})
818
+ $timer.Add_Tick({
819
+ $timer.Stop()
820
+ ${unblockSnippet}
821
+ $window.Close()
822
+ })
823
+ $timer.Start()
824
+
825
+ [void]$window.ShowDialog()
826
+ ${unblockSnippet}
827
+ `;
828
+ fs.writeFileSync(psFile, overlayPs, 'utf8');
829
+ await execAsync(`powershell -WindowStyle Hidden -NoProfile -Sta -ExecutionPolicy Bypass -File "${psFile}"`, { timeout: durationMs + 3000 });
830
+ }
831
+ catch {
832
+ if (blockInput) {
833
+ try {
834
+ 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)"`);
835
+ }
836
+ catch { }
837
+ }
838
+ }
839
+ }
840
+ const capturedScreenshots = new Set();
841
+ /**
842
+ * Register a captured screenshot file path for automatic post-task cleanup.
843
+ */
844
+ export function registerCapturedScreenshot(filePath) {
845
+ if (filePath) {
846
+ capturedScreenshots.add(path.resolve(filePath));
847
+ }
848
+ }
849
+ /**
850
+ * Clean up all captured screenshots permanently from disk after task completion.
851
+ */
852
+ export function cleanupScreenshots() {
853
+ let deletedCount = 0;
854
+ // 1. Delete all tracked screenshots
855
+ for (const filePath of capturedScreenshots) {
856
+ try {
857
+ if (fs.existsSync(filePath)) {
858
+ fs.unlinkSync(filePath);
859
+ deletedCount++;
860
+ }
861
+ }
862
+ catch { }
863
+ }
864
+ capturedScreenshots.clear();
865
+ // 2. Clean any remaining files in .ft/screenshots
866
+ try {
867
+ const screenshotsDir = path.join(process.cwd(), '.ft', 'screenshots');
868
+ if (fs.existsSync(screenshotsDir)) {
869
+ const files = fs.readdirSync(screenshotsDir);
870
+ for (const file of files) {
871
+ if (file.startsWith('screen_') && (file.endsWith('.png') || file.endsWith('.jpg'))) {
872
+ try {
873
+ fs.unlinkSync(path.join(screenshotsDir, file));
874
+ deletedCount++;
875
+ }
876
+ catch { }
877
+ }
878
+ }
879
+ }
880
+ }
881
+ catch { }
882
+ return { success: true, deletedCount };
883
+ }
565
884
  /**
566
885
  * Capture full desktop screen or specified window screenshot without running shell commands.
886
+ * Screenshots are tracked and will be deleted automatically upon task completion.
567
887
  */
568
888
  export async function captureScreen(appNameOrScreen) {
569
889
  const platform = process.platform;
@@ -608,9 +928,12 @@ $activeTitle = $sb.ToString()
608
928
  const encodedScript = Buffer.from(psScript, 'utf16le').toString('base64');
609
929
  const { stdout } = await execAsync(`powershell -NoProfile -EncodedCommand ${encodedScript}`);
610
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(() => { });
611
934
  return {
612
935
  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.`,
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).`,
614
937
  details: { ...meta, outPath },
615
938
  };
616
939
  }
@@ -624,9 +947,10 @@ $activeTitle = $sb.ToString()
624
947
  else if (platform === 'darwin') {
625
948
  try {
626
949
  await execAsync(`screencapture -x "${outPath}"`);
950
+ registerCapturedScreenshot(outPath);
627
951
  return {
628
952
  success: true,
629
- output: `macOS Screen Captured Successfully! Saved To: ${outPath}`,
953
+ output: `macOS Screen Captured Successfully! Saved To: ${outPath}\n(NOTE: This temporary screenshot will be automatically deleted upon task completion).`,
630
954
  details: { outPath, platform: 'darwin' },
631
955
  };
632
956
  }
@@ -640,9 +964,10 @@ $activeTitle = $sb.ToString()
640
964
  else {
641
965
  try {
642
966
  await execAsync(`import -window root "${outPath}" || scrot "${outPath}"`);
967
+ registerCapturedScreenshot(outPath);
643
968
  return {
644
969
  success: true,
645
- output: `Linux Screen Captured Successfully! Saved To: ${outPath}`,
970
+ output: `Linux Screen Captured Successfully! Saved To: ${outPath}\n(NOTE: This temporary screenshot will be automatically deleted upon task completion).`,
646
971
  details: { outPath, platform: 'linux' },
647
972
  };
648
973
  }
@@ -655,20 +980,62 @@ $activeTitle = $sb.ToString()
655
980
  }
656
981
  }
657
982
  /**
658
- * Analyze specified screen or active window visually without running shell commands.
983
+ * Visual screen analysis. Takes a full-screen screenshot and provides visual context
984
+ * (including AI Vision inspection if an API key is available).
985
+ * All temporary screenshots are automatically deleted upon task completion.
659
986
  */
660
- export async function analyzeScreen(appNameOrScreen) {
987
+ export async function analyzeScreen(appNameOrScreen, targetQuery) {
661
988
  const capture = await captureScreen(appNameOrScreen);
662
989
  const info = await getScreenInfo(appNameOrScreen);
990
+ let visualAiAnalysis = '';
991
+ const outPath = capture.details?.outPath;
992
+ // Optional Vision Model Analysis if screenshot exists and API key is present
993
+ if (capture.success && outPath && fs.existsSync(outPath)) {
994
+ 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 });
1005
+ const prompt = targetQuery
1006
+ ? `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;
1022
+ if (reply && reply.trim()) {
1023
+ visualAiAnalysis = `\nAI Visual Screen Analysis:\n${reply.trim()}`;
1024
+ }
1025
+ }
1026
+ }
1027
+ catch {
1028
+ // Vision API call is optional; fallback gracefully to system metadata
1029
+ }
1030
+ }
663
1031
  const report = [
664
- `=== SCOUT SCRATCHPAD DESKTOP SCREEN SEEING & VISUAL ANALYSIS ===`,
1032
+ `=== SCOUT SCRATCHPAD DESKTOP SCREEN VISUAL INSPECTION ===`,
665
1033
  `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'}"`,
1034
+ `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'}"`,
668
1036
  `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.`,
1037
+ `Screenshot Status: Captured temporarily for visual inspection. (Will be automatically deleted upon task completion)`,
1038
+ `Visual Status: Screen is visible and verified.${visualAiAnalysis}`,
672
1039
  ].join('\n');
673
1040
  return {
674
1041
  success: capture.success,
@@ -683,14 +1050,19 @@ export async function analyzeScreen(appNameOrScreen) {
683
1050
  */
684
1051
  export async function takeoverControl(appName, durationMs = 3000, x, y) {
685
1052
  const { category, name } = normalizeAppName(appName);
686
- if (name && name !== 'desktop' && name !== 'system') {
1053
+ if (name && name !== 'desktop' && name !== 'system' && category !== 'browser') {
687
1054
  await openApp(name);
688
1055
  }
1056
+ // Trigger visual sky-blue aura overlay on screen borders & corners with warning badge & input block
1057
+ try {
1058
+ await showScreenAuraOverlay(Math.min(5000, Math.max(800, durationMs)), "Scout is on the screen.", true);
1059
+ }
1060
+ catch { }
689
1061
  const mouseRes = await moveAndClickMouse(x, y, 'left', name || appName);
690
1062
  const lockRes = await lockAppInput(name || appName, durationMs);
691
1063
  return {
692
1064
  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.`,
1065
+ 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
1066
  details: { appName: name || appName, durationMs, x, y, mouseOutput: mouseRes.output },
695
1067
  };
696
1068
  }
@@ -728,59 +1100,101 @@ export async function sendKeystrokes(appName, textToType, pressEnter = true, key
728
1100
  const sendKeysText = sanitized.replace(/[{}+^~%()\[\]]/g, '{$&}');
729
1101
  let sendKeysCombo = '';
730
1102
  if (combo) {
731
- if (combo.includes('ctrl+a') || combo === 'select_all')
1103
+ const cLower = combo.toLowerCase().replace(/\s+/g, '');
1104
+ if (cLower.includes('ctrl+enter') || cLower.includes('ctrl+return'))
1105
+ sendKeysCombo = '^{ENTER}';
1106
+ else if (cLower.includes('alt+enter') || cLower.includes('alt+return'))
1107
+ sendKeysCombo = '%{ENTER}';
1108
+ else if (cLower.includes('shift+enter') || cLower.includes('shift+return'))
1109
+ sendKeysCombo = '+{ENTER}';
1110
+ else if (cLower.includes('ctrl+a') || cLower === 'select_all')
732
1111
  sendKeysCombo = '^a';
733
- else if (combo.includes('ctrl+c') || combo === 'copy')
1112
+ else if (cLower.includes('ctrl+c') || cLower === 'copy')
734
1113
  sendKeysCombo = '^c';
735
- else if (combo.includes('ctrl+v') || combo === 'paste')
1114
+ else if (cLower.includes('ctrl+v') || cLower === 'paste')
736
1115
  sendKeysCombo = '^v';
737
- else if (combo.includes('ctrl+z') || combo === 'undo')
1116
+ else if (cLower.includes('ctrl+x') || cLower === 'cut')
1117
+ sendKeysCombo = '^x';
1118
+ else if (cLower.includes('ctrl+z') || cLower === 'undo')
738
1119
  sendKeysCombo = '^z';
739
- else if (combo.includes('ctrl+s') || combo === 'save')
1120
+ else if (cLower.includes('ctrl+s') || cLower === 'save')
740
1121
  sendKeysCombo = '^s';
741
- else if (combo.includes('ctrl+l') || combo === 'address_bar')
1122
+ else if (cLower.includes('ctrl+l') || cLower === 'address_bar')
742
1123
  sendKeysCombo = '^l';
743
- else if (combo.includes('alt+d'))
1124
+ else if (cLower.includes('ctrl+t') || cLower === 'new_tab')
1125
+ sendKeysCombo = '^t';
1126
+ else if (cLower.includes('ctrl+w') || cLower === 'close_tab')
1127
+ sendKeysCombo = '^w';
1128
+ else if (cLower.includes('ctrl+r') || cLower === 'reload')
1129
+ sendKeysCombo = '^r';
1130
+ else if (cLower.includes('ctrl+f') || cLower === 'find')
1131
+ sendKeysCombo = '^f';
1132
+ else if (cLower.includes('ctrl+n') || cLower === 'new_window')
1133
+ sendKeysCombo = '^n';
1134
+ else if (cLower.includes('ctrl+shift+i') || cLower.includes('devtools'))
1135
+ sendKeysCombo = '^+i';
1136
+ else if (cLower.includes('alt+d'))
744
1137
  sendKeysCombo = '%d';
745
- else if (combo.includes('alt+tab'))
1138
+ else if (cLower.includes('alt+tab'))
746
1139
  sendKeysCombo = '%{TAB}';
747
- else if (combo.includes('alt+f4'))
1140
+ else if (cLower.includes('alt+f4'))
748
1141
  sendKeysCombo = '%{F4}';
749
- else if (combo === 'enter' || combo === 'return')
750
- sendKeysCombo = '~';
751
- else if (combo === 'esc' || combo === 'escape')
1142
+ else if (cLower === 'enter' || cLower === 'return')
1143
+ sendKeysCombo = '{ENTER}';
1144
+ else if (cLower === 'esc' || cLower === 'escape')
752
1145
  sendKeysCombo = '{ESC}';
753
- else if (combo === 'tab')
1146
+ else if (cLower === 'tab')
754
1147
  sendKeysCombo = '{TAB}';
755
- else if (combo === 'backspace')
1148
+ else if (cLower === 'backspace')
756
1149
  sendKeysCombo = '{BACKSPACE}';
757
- else if (combo === 'up')
1150
+ else if (cLower === 'space')
1151
+ sendKeysCombo = ' ';
1152
+ else if (cLower === 'up')
758
1153
  sendKeysCombo = '{UP}';
759
- else if (combo === 'down')
1154
+ else if (cLower === 'down')
760
1155
  sendKeysCombo = '{DOWN}';
761
- else if (combo === 'left')
1156
+ else if (cLower === 'left')
762
1157
  sendKeysCombo = '{LEFT}';
763
- else if (combo === 'right')
1158
+ else if (cLower === 'right')
764
1159
  sendKeysCombo = '{RIGHT}';
765
1160
  else
766
1161
  sendKeysCombo = combo;
767
1162
  }
768
1163
  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
1164
+ $sig = @'
1165
+ [DllImport("user32.dll")] public static extern bool BlockInput(bool fBlock);
1166
+ [DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr hWnd);
1167
+ [DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
1168
+ [DllImport("user32.dll")] public static extern void keybd_event(byte bVk, byte bScan, uint dwFlags, UIntPtr dwExtraInfo);
1169
+ '@
1170
+ $b = Add-Type -memberDefinition $sig -name ('Win32Block_' + (Get-Random)) -namespace Win32Functions -passThru
771
1171
 
772
1172
  try { [void]$b::BlockInput($true) } catch {}
773
1173
 
774
1174
  Add-Type -AssemblyName System.Windows.Forms
775
1175
  $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
1176
+
1177
+ try {
1178
+ $proc = Get-Process -Name chrome, msedge, firefox, brave, opera, "${appName}" -ErrorAction SilentlyContinue | Where-Object { $_.MainWindowHandle -ne 0 } | Select-Object -First 1
1179
+ if ($proc) {
1180
+ $b::keybd_event(0x12, 0, 0, [UIntPtr]::Zero)
1181
+ $b::keybd_event(0x12, 0, 2, [UIntPtr]::Zero)
1182
+ [void]$b::ShowWindow($proc.MainWindowHandle, 9)
1183
+ [void]$b::SetForegroundWindow($proc.MainWindowHandle)
1184
+ Start-Sleep -Milliseconds 250
1185
+ } else {
1186
+ $activated = $ws.AppActivate("${appName}")
1187
+ if (-not $activated) { $activated = $ws.AppActivate("Chrome") }
1188
+ if (-not $activated) { $activated = $ws.AppActivate("Edge") }
1189
+ if (-not $activated) { $activated = $ws.AppActivate("Firefox") }
1190
+ if (-not $activated) { $activated = $ws.AppActivate("Brave") }
1191
+ if (-not $activated) { $activated = $ws.AppActivate("Notepad") }
1192
+ if (-not $activated) { $activated = $ws.AppActivate("Visual Studio Code") }
1193
+ Start-Sleep -Milliseconds 300
1194
+ }
1195
+ } catch {}
1196
+
1197
+ Start-Sleep -Milliseconds 250
784
1198
 
785
1199
  ${sendKeysCombo ? `$ws.SendKeys("${sendKeysCombo}")` : ''}
786
1200
 
@@ -875,9 +1289,20 @@ Add-Type -AssemblyName System.Windows.Forms
875
1289
  $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
1290
  $m = Add-Type -memberDefinition $sig -name 'Win32Mouse' -namespace Win32Functions -passThru
877
1291
 
878
- ${appName && appName !== 'desktop' && appName !== 'system' ? `
879
- $ws = New-Object -ComObject WScript.Shell
880
- $ws.AppActivate("${appName}")
1292
+ ${(appName && appName !== 'desktop' && appName !== 'system') ? `
1293
+ try {
1294
+ $proc = Get-Process -Name chrome, msedge, firefox, brave, "${appName}" -ErrorAction SilentlyContinue | Where-Object { $_.MainWindowHandle -ne 0 } | Select-Object -First 1
1295
+ if ($proc) {
1296
+ $sigFw = '[DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr hWnd);'
1297
+ $fw = Add-Type -memberDefinition $sigFw -name 'Win32FwMouse' -namespace Win32Functions -passThru
1298
+ [void]$fw::SetForegroundWindow($proc.MainWindowHandle)
1299
+ Start-Sleep -Milliseconds 250
1300
+ } else {
1301
+ $ws = New-Object -ComObject WScript.Shell
1302
+ [void]$ws.AppActivate("${appName}")
1303
+ Start-Sleep -Milliseconds 200
1304
+ }
1305
+ } catch {}
881
1306
  ` : ''}
882
1307
 
883
1308
  $screen = [System.Windows.Forms.Screen]::PrimaryScreen
@@ -1016,4 +1441,210 @@ end tell
1016
1441
  };
1017
1442
  }
1018
1443
  }
1444
+ /**
1445
+ * List all active top-level application windows.
1446
+ */
1447
+ export async function listWindows() {
1448
+ const platform = process.platform;
1449
+ if (platform === 'win32') {
1450
+ const psScript = `
1451
+ Get-Process | Where-Object { $_.MainWindowTitle -ne '' } | Select-Object Id, ProcessName, MainWindowTitle, MainWindowHandle | ConvertTo-Json
1452
+ `;
1453
+ try {
1454
+ const encodedScript = Buffer.from(psScript, 'utf16le').toString('base64');
1455
+ const { stdout } = await execAsync(`powershell -NoProfile -Sta -EncodedCommand ${encodedScript}`);
1456
+ let rawList = [];
1457
+ try {
1458
+ rawList = JSON.parse(stdout.trim());
1459
+ }
1460
+ catch { }
1461
+ if (!Array.isArray(rawList))
1462
+ rawList = rawList ? [rawList] : [];
1463
+ const windows = rawList.map((w) => ({
1464
+ title: String(w.MainWindowTitle || ''),
1465
+ processName: String(w.ProcessName || ''),
1466
+ handle: String(w.MainWindowHandle || ''),
1467
+ }));
1468
+ const formatted = windows.map((w, idx) => ` ${idx + 1}. [${w.processName}] "${w.title}"`).join('\n');
1469
+ return {
1470
+ success: true,
1471
+ output: `Active Application Windows (${windows.length}):\n${formatted || 'No active top-level windows with title found.'}`,
1472
+ windows,
1473
+ };
1474
+ }
1475
+ catch (err) {
1476
+ return { success: false, output: `Failed to list windows: ${err?.message || String(err)}`, windows: [] };
1477
+ }
1478
+ }
1479
+ return { success: true, output: 'Window listing only supported on Windows.', windows: [] };
1480
+ }
1481
+ /**
1482
+ * Bring a target window to front and focus it.
1483
+ */
1484
+ export async function focusWindow(appNameOrTitle) {
1485
+ const platform = process.platform;
1486
+ if (platform === 'win32') {
1487
+ const psScript = `
1488
+ $target = "${appNameOrTitle.replace(/"/g, '`"')}"
1489
+ $proc = Get-Process | Where-Object { ($_.MainWindowTitle -like "*$target*" -or $_.ProcessName -like "*$target*") -and $_.MainWindowHandle -ne 0 } | Select-Object -First 1
1490
+ if ($proc) {
1491
+ $sig = '[DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr hWnd); [DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);'
1492
+ $w = Add-Type -memberDefinition $sig -name 'Win32Fw' -namespace Win32Functions -passThru
1493
+ [void]$w::ShowWindow($proc.MainWindowHandle, 9)
1494
+ [void]$w::SetForegroundWindow($proc.MainWindowHandle)
1495
+ Write-Output "Focused '$($proc.MainWindowTitle)' ($($proc.ProcessName))"
1496
+ } else {
1497
+ $ws = New-Object -ComObject WScript.Shell
1498
+ $activated = $ws.AppActivate($target)
1499
+ if ($activated) { Write-Output "Activated '$target' via WScript.Shell" } else { Write-Output "Window '$target' not found" }
1500
+ }
1501
+ `;
1502
+ try {
1503
+ const encodedScript = Buffer.from(psScript, 'utf16le').toString('base64');
1504
+ const { stdout } = await execAsync(`powershell -NoProfile -Sta -EncodedCommand ${encodedScript}`);
1505
+ return { success: true, output: stdout.trim() || `Focus request completed for "${appNameOrTitle}".` };
1506
+ }
1507
+ catch (err) {
1508
+ return { success: false, output: `Could not focus window: ${err?.message || String(err)}` };
1509
+ }
1510
+ }
1511
+ return { success: true, output: `Focus request sent for "${appNameOrTitle}".` };
1512
+ }
1513
+ /**
1514
+ * Scroll mouse wheel up or down by specified notch amount.
1515
+ */
1516
+ export async function scrollWindow(direction = 'down', amount = 3) {
1517
+ const platform = process.platform;
1518
+ if (platform === 'win32') {
1519
+ const clicks = Math.max(1, Math.min(20, amount));
1520
+ const delta = direction === 'up' ? 120 * clicks : -120 * clicks;
1521
+ const psScript = `
1522
+ Add-Type -AssemblyName System.Windows.Forms
1523
+ $sig = '[DllImport("user32.dll")] public static extern void mouse_event(int dwFlags, int dx, int dy, int cButtons, int dwExtraInfo);'
1524
+ $m = Add-Type -memberDefinition $sig -name 'Win32Scroll' -namespace Win32Functions -passThru
1525
+ $m::mouse_event(0x0800, 0, 0, ${delta}, 0)
1526
+ `;
1527
+ try {
1528
+ const encodedScript = Buffer.from(psScript, 'utf16le').toString('base64');
1529
+ await execAsync(`powershell -NoProfile -EncodedCommand ${encodedScript}`);
1530
+ return { success: true, output: `Scrolled mouse wheel ${direction} by ${clicks} notch(es).` };
1531
+ }
1532
+ catch (err) {
1533
+ return { success: false, output: `Failed to scroll: ${err?.message || String(err)}` };
1534
+ }
1535
+ }
1536
+ return { success: true, output: `Scrolled ${direction}` };
1537
+ }
1538
+ /**
1539
+ * Visually ground and click a target UI element by name or visual description.
1540
+ */
1541
+ export async function findAndClickElement(elementQuery, appName, button = 'left') {
1542
+ const capture = await captureScreen(appName);
1543
+ if (!capture.success || !capture.details?.outPath) {
1544
+ return { success: false, output: `Could not capture screen to locate element: ${capture.output}` };
1545
+ }
1546
+ const screenWidth = capture.details.width || 1920;
1547
+ const screenHeight = capture.details.height || 1080;
1548
+ const outPath = capture.details.outPath;
1549
+ let targetX = undefined;
1550
+ 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)) {
1556
+ try {
1557
+ const imageBase64 = fs.readFileSync(outPath).toString('base64');
1558
+ const isGemini = Boolean(geminiKey);
1559
+ const baseURL = isGemini ? 'https://generativelanguage.googleapis.com/v1beta/openai' : undefined;
1560
+ const model = isGemini ? 'gemini-2.5-flash' : 'gpt-4o-mini';
1561
+ const client = new OpenAI({ apiKey, baseURL, timeout: 15000 });
1562
+ const prompt = `Look at this screenshot and locate the UI element: "${elementQuery}".
1563
+ Return ONLY a valid JSON object with the center point of this element in 0-1000 normalized coordinates:
1564
+ {"point": [y, x]}
1565
+ Where y is between 0 and 1000 (top to bottom) and x is between 0 and 1000 (left to right).
1566
+ 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);
1590
+ }
1591
+ }
1592
+ }
1593
+ }
1594
+ catch { }
1595
+ }
1596
+ if ((targetX === undefined || targetY === undefined) && process.platform === 'win32') {
1597
+ const psScript = `
1598
+ Add-Type -AssemblyName UIAutomationClient
1599
+ Add-Type -AssemblyName UIAutomationTypes
1600
+ $target = "${elementQuery.replace(/"/g, '""')}"
1601
+ $root = [System.Windows.Automation.AutomationElement]::RootElement
1602
+
1603
+ $condName = New-Object System.Windows.Automation.PropertyCondition([System.Windows.Automation.AutomationElement]::NameProperty, $target)
1604
+ $el = $root.FindFirst([System.Windows.Automation.TreeScope]::Descendants, $condName)
1605
+
1606
+ if (-not $el) {
1607
+ $all = $root.FindAll([System.Windows.Automation.TreeScope]::Descendants, [System.Windows.Automation.Condition]::TrueCondition)
1608
+ foreach ($item in $all) {
1609
+ if ($item.Current.Name -and $item.Current.Name.ToLower().Contains($target.ToLower())) {
1610
+ $el = $item
1611
+ break
1612
+ }
1613
+ }
1614
+ }
1615
+
1616
+ if ($el) {
1617
+ $rect = $el.Current.BoundingRectangle
1618
+ if ($rect -and $rect.Width -gt 0 -and $rect.Height -gt 0) {
1619
+ $cx = [int]($rect.X + ($rect.Width / 2))
1620
+ $cy = [int]($rect.Y + ($rect.Height / 2))
1621
+ @{ x = $cx; y = $cy; name = $el.Current.Name } | ConvertTo-Json
1622
+ }
1623
+ }
1624
+ `;
1625
+ try {
1626
+ const { stdout } = await execAsync(`powershell -NoProfile -Command "${psScript.replace(/\r?\n/g, ' ')}"`);
1627
+ if (stdout.trim()) {
1628
+ const uia = JSON.parse(stdout.trim());
1629
+ if (uia.x && uia.y) {
1630
+ targetX = uia.x;
1631
+ targetY = uia.y;
1632
+ }
1633
+ }
1634
+ }
1635
+ catch { }
1636
+ }
1637
+ if (targetX !== undefined && targetY !== undefined) {
1638
+ const clickRes = await moveAndClickMouse(targetX, targetY, button, appName);
1639
+ return {
1640
+ success: clickRes.success,
1641
+ output: `Visual Grounding Success: Located "${elementQuery}" at screen coordinates (${targetX}, ${targetY}). ${clickRes.output}`,
1642
+ coordinates: { x: targetX, y: targetY },
1643
+ };
1644
+ }
1645
+ return {
1646
+ success: false,
1647
+ 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.`,
1648
+ };
1649
+ }
1019
1650
  //# sourceMappingURL=appControl.js.map