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.
- package/README.md +40 -5
- package/bin/src/commands/agent.d.ts.map +1 -1
- package/bin/src/commands/agent.js +12 -7
- package/bin/src/commands/agent.js.map +1 -1
- package/bin/src/engine/agentEngine.d.ts.map +1 -1
- package/bin/src/engine/agentEngine.js +290 -177
- package/bin/src/engine/agentEngine.js.map +1 -1
- package/bin/src/engine/appControl.d.ts.map +1 -1
- package/bin/src/engine/appControl.js +693 -62
- package/bin/src/engine/appControl.js.map +1 -1
- package/bin/src/engine/llm.d.ts.map +1 -1
- package/bin/src/engine/llm.js +4 -3
- package/bin/src/engine/llm.js.map +1 -1
- package/bin/src/engine/voiceEngine.d.ts.map +1 -1
- package/bin/src/engine/voiceEngine.js +67 -199
- package/bin/src/engine/voiceEngine.js.map +1 -1
- package/bin/src/index.js +4 -4
- package/bin/src/utils/branding.js +1 -1
- package/package.json +2 -2
- package/web/app.js +2 -2
- package/web/styles.css +1 -1
|
@@ -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', '
|
|
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
|
|
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
|
|
297
|
-
|
|
298
|
-
|
|
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 (['
|
|
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
|
|
399
|
-
|
|
400
|
-
|
|
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
|
|
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 (
|
|
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: `
|
|
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
|
|
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
|
-
*
|
|
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
|
|
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
|
|
670
|
-
`
|
|
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}").
|
|
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
|
-
|
|
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 (
|
|
1112
|
+
else if (cLower.includes('ctrl+c') || cLower === 'copy')
|
|
734
1113
|
sendKeysCombo = '^c';
|
|
735
|
-
else if (
|
|
1114
|
+
else if (cLower.includes('ctrl+v') || cLower === 'paste')
|
|
736
1115
|
sendKeysCombo = '^v';
|
|
737
|
-
else if (
|
|
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 (
|
|
1120
|
+
else if (cLower.includes('ctrl+s') || cLower === 'save')
|
|
740
1121
|
sendKeysCombo = '^s';
|
|
741
|
-
else if (
|
|
1122
|
+
else if (cLower.includes('ctrl+l') || cLower === 'address_bar')
|
|
742
1123
|
sendKeysCombo = '^l';
|
|
743
|
-
else if (
|
|
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 (
|
|
1138
|
+
else if (cLower.includes('alt+tab'))
|
|
746
1139
|
sendKeysCombo = '%{TAB}';
|
|
747
|
-
else if (
|
|
1140
|
+
else if (cLower.includes('alt+f4'))
|
|
748
1141
|
sendKeysCombo = '%{F4}';
|
|
749
|
-
else if (
|
|
750
|
-
sendKeysCombo = '
|
|
751
|
-
else if (
|
|
1142
|
+
else if (cLower === 'enter' || cLower === 'return')
|
|
1143
|
+
sendKeysCombo = '{ENTER}';
|
|
1144
|
+
else if (cLower === 'esc' || cLower === 'escape')
|
|
752
1145
|
sendKeysCombo = '{ESC}';
|
|
753
|
-
else if (
|
|
1146
|
+
else if (cLower === 'tab')
|
|
754
1147
|
sendKeysCombo = '{TAB}';
|
|
755
|
-
else if (
|
|
1148
|
+
else if (cLower === 'backspace')
|
|
756
1149
|
sendKeysCombo = '{BACKSPACE}';
|
|
757
|
-
else if (
|
|
1150
|
+
else if (cLower === 'space')
|
|
1151
|
+
sendKeysCombo = ' ';
|
|
1152
|
+
else if (cLower === 'up')
|
|
758
1153
|
sendKeysCombo = '{UP}';
|
|
759
|
-
else if (
|
|
1154
|
+
else if (cLower === 'down')
|
|
760
1155
|
sendKeysCombo = '{DOWN}';
|
|
761
|
-
else if (
|
|
1156
|
+
else if (cLower === 'left')
|
|
762
1157
|
sendKeysCombo = '{LEFT}';
|
|
763
|
-
else if (
|
|
1158
|
+
else if (cLower === 'right')
|
|
764
1159
|
sendKeysCombo = '{RIGHT}';
|
|
765
1160
|
else
|
|
766
1161
|
sendKeysCombo = combo;
|
|
767
1162
|
}
|
|
768
1163
|
const psScript = `
|
|
769
|
-
$sig = '
|
|
770
|
-
|
|
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
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
if (
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
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
|
-
|
|
880
|
-
$
|
|
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
|