ft-scout 7.0.7 → 7.0.8
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/.firebase/hosting.d2Vi.cache +6 -3
- package/README.md +1 -1
- package/bin/src/commands/agent.d.ts.map +1 -1
- package/bin/src/commands/agent.js +2 -1
- 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 +24 -11
- 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 +438 -79
- package/bin/src/engine/appControl.js.map +1 -1
- package/bin/src/index.js +4 -4
- package/bin/src/utils/branding.js +1 -1
- package/firebase.json +13 -1
- package/package.json +1 -1
- package/web/app.js +768 -310
- package/web/styles.css +931 -1634
|
@@ -211,7 +211,7 @@ export async function openApp(appNameOrPath, target, options) {
|
|
|
211
211
|
}
|
|
212
212
|
}
|
|
213
213
|
try {
|
|
214
|
-
await execAsync(command);
|
|
214
|
+
await execAsync(command, { timeout: 3000 });
|
|
215
215
|
return {
|
|
216
216
|
success: true,
|
|
217
217
|
app: name || appNameOrPath,
|
|
@@ -221,7 +221,7 @@ export async function openApp(appNameOrPath, target, options) {
|
|
|
221
221
|
};
|
|
222
222
|
}
|
|
223
223
|
catch (err) {
|
|
224
|
-
// Fallback for GUI commands that spawn detached processes
|
|
224
|
+
// Fallback for GUI commands that spawn detached processes or timeout waiting for process exit
|
|
225
225
|
return {
|
|
226
226
|
success: true,
|
|
227
227
|
app: name || appNameOrPath,
|
|
@@ -237,8 +237,119 @@ export async function openApp(appNameOrPath, target, options) {
|
|
|
237
237
|
export async function executeInApp(appName, action, payload) {
|
|
238
238
|
const { category, name } = normalizeAppName(appName);
|
|
239
239
|
const actLower = (action || '').trim().toLowerCase();
|
|
240
|
+
// Universal Scratchpad Desktop Automation Actions (Takeover, Screen Seeing, Keystrokes, Mouse Clicks, Focus Locks)
|
|
241
|
+
if (['takeover', 'takeover_control', 'takeover_screen', 'takeover_app'].includes(actLower)) {
|
|
242
|
+
const parsed = parsePayload(payload);
|
|
243
|
+
const duration = typeof parsed.duration === 'number' ? parsed.duration : 3000;
|
|
244
|
+
const x = typeof parsed.x === 'number' ? parsed.x : undefined;
|
|
245
|
+
const y = typeof parsed.y === 'number' ? parsed.y : undefined;
|
|
246
|
+
const res = await takeoverControl(name || appName, duration, x, y);
|
|
247
|
+
return {
|
|
248
|
+
success: res.success,
|
|
249
|
+
app: name || appName,
|
|
250
|
+
action: actLower,
|
|
251
|
+
output: res.output,
|
|
252
|
+
details: res.details,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
if (['release_takeover', 'unlock_screen', 'unlock_app', 'unlock_input', 'unlock'].includes(actLower)) {
|
|
256
|
+
const res = await releaseTakeover();
|
|
257
|
+
return {
|
|
258
|
+
success: res.success,
|
|
259
|
+
app: name || appName,
|
|
260
|
+
action: actLower,
|
|
261
|
+
output: res.output,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
if (['capture_screen', 'screenshot', 'see_screen', 'inspect_screen', 'screen_shot', 'view_screen'].includes(actLower)) {
|
|
265
|
+
const res = await captureScreen(name || appName);
|
|
266
|
+
return {
|
|
267
|
+
success: res.success,
|
|
268
|
+
app: name || appName,
|
|
269
|
+
action: actLower,
|
|
270
|
+
output: res.output,
|
|
271
|
+
details: res.details,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
if (['analyze_screen', 'inspect_desktop', 'visual_analysis', 'see_and_analyze'].includes(actLower)) {
|
|
275
|
+
const res = await analyzeScreen(name || appName);
|
|
276
|
+
return {
|
|
277
|
+
success: res.success,
|
|
278
|
+
app: name || appName,
|
|
279
|
+
action: actLower,
|
|
280
|
+
output: res.output,
|
|
281
|
+
details: res.details,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
if (['get_screen_info', 'list_screens', 'screen_bounds', 'screen_info'].includes(actLower)) {
|
|
285
|
+
const res = await getScreenInfo(name || appName);
|
|
286
|
+
return {
|
|
287
|
+
success: res.success,
|
|
288
|
+
app: name || appName,
|
|
289
|
+
action: actLower,
|
|
290
|
+
output: res.output,
|
|
291
|
+
details: res.details,
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
if (['type_text', 'send_keys', 'type', 'keystrokes', 'key_combo', 'press_hotkey', 'hotkey'].includes(actLower)) {
|
|
295
|
+
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);
|
|
299
|
+
const ksRes = await sendKeystrokes(name || appName, textToType, pressEnter, keyCombo);
|
|
300
|
+
return {
|
|
301
|
+
success: ksRes.success,
|
|
302
|
+
app: name || appName,
|
|
303
|
+
action: actLower,
|
|
304
|
+
output: ksRes.output,
|
|
305
|
+
details: { appName: name || appName, textToType, pressEnter, keyCombo },
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
if (['click', 'click_app', 'click_at', 'move_mouse', 'drag_mouse', 'scroll'].includes(actLower)) {
|
|
309
|
+
const parsedPayload = parsePayload(payload);
|
|
310
|
+
const x = typeof parsedPayload.x === 'number' ? parsedPayload.x : undefined;
|
|
311
|
+
const y = typeof parsedPayload.y === 'number' ? parsedPayload.y : undefined;
|
|
312
|
+
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
|
+
const clickRes = await moveAndClickMouse(x, y, button, name || appName);
|
|
317
|
+
return {
|
|
318
|
+
success: clickRes.success,
|
|
319
|
+
app: name || appName,
|
|
320
|
+
action: actLower,
|
|
321
|
+
output: clickRes.output,
|
|
322
|
+
details: { appName: name || appName, x, y, button },
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
if (['lock_app', 'block_interruption', 'lock_input', 'lock'].includes(actLower)) {
|
|
326
|
+
const parsedPayload = parsePayload(payload);
|
|
327
|
+
const durationMs = typeof parsedPayload.duration === 'number' ? parsedPayload.duration : 3000;
|
|
328
|
+
const lockRes = await lockAppInput(name || appName, durationMs);
|
|
329
|
+
return {
|
|
330
|
+
success: lockRes.success,
|
|
331
|
+
app: name || appName,
|
|
332
|
+
action: actLower,
|
|
333
|
+
output: lockRes.output,
|
|
334
|
+
details: { appName: name || appName, durationMs },
|
|
335
|
+
};
|
|
336
|
+
}
|
|
240
337
|
if (category === 'browser') {
|
|
241
|
-
if (
|
|
338
|
+
if (['navigate', 'goto', 'open_url'].includes(actLower)) {
|
|
339
|
+
const rawUrl = typeof payload === 'string' ? payload : (payload?.url || payload?.target || 'https://google.com');
|
|
340
|
+
const targetUrl = rawUrl.startsWith('http://') || rawUrl.startsWith('https://') ? rawUrl : `https://${rawUrl}`;
|
|
341
|
+
await openApp(name || 'browser', targetUrl);
|
|
342
|
+
await new Promise((r) => setTimeout(r, 800));
|
|
343
|
+
const capture = await captureScreen(name || 'browser');
|
|
344
|
+
return {
|
|
345
|
+
success: true,
|
|
346
|
+
app: name || 'browser',
|
|
347
|
+
action: actLower,
|
|
348
|
+
output: `Browser Navigated Successfully to Target URL: ${targetUrl}\n- Screenshot Saved: ${capture.details?.outPath || 'Active'}`,
|
|
349
|
+
details: { url: targetUrl, capture: capture.details },
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
if (actLower === 'fetch_page' || actLower === 'read_url' || actLower === 'inspect_dom') {
|
|
242
353
|
const rawUrl = typeof payload === 'string' ? payload : (payload?.url || payload?.target || 'http://localhost:3000');
|
|
243
354
|
const targetUrl = rawUrl.startsWith('http://') || rawUrl.startsWith('https://') ? rawUrl : `http://${rawUrl}`;
|
|
244
355
|
try {
|
|
@@ -264,15 +375,23 @@ export async function executeInApp(appName, action, payload) {
|
|
|
264
375
|
}
|
|
265
376
|
}
|
|
266
377
|
if (actLower === 'search' || actLower === 'search_web') {
|
|
267
|
-
const query = typeof payload === 'string' ? payload : (payload?.query || payload?.target || '');
|
|
378
|
+
const query = typeof payload === 'string' ? payload : (payload?.query || payload?.target || payload?.url || '');
|
|
379
|
+
const isUrl = query.startsWith('http://') || query.startsWith('https://') || (query.includes('.') && !query.includes(' '));
|
|
380
|
+
if (isUrl) {
|
|
381
|
+
const targetUrl = query.startsWith('http://') || query.startsWith('https://') ? query : `https://${query}`;
|
|
382
|
+
return await executeInApp(name || 'browser', 'navigate', targetUrl);
|
|
383
|
+
}
|
|
384
|
+
// Direct Search in browser window
|
|
268
385
|
const searchUrl = `https://www.google.com/search?q=${encodeURIComponent(query)}`;
|
|
269
|
-
await openApp('browser', searchUrl);
|
|
386
|
+
await openApp(name || 'browser', searchUrl);
|
|
387
|
+
await new Promise((r) => setTimeout(r, 800));
|
|
388
|
+
const capture = await captureScreen(name || 'browser');
|
|
270
389
|
return {
|
|
271
390
|
success: true,
|
|
272
391
|
app: name,
|
|
273
392
|
action: actLower,
|
|
274
|
-
output: `Browser
|
|
275
|
-
details: { query, searchUrl },
|
|
393
|
+
output: `Browser search executed for "${query}" at ${searchUrl}.\n- Screenshot Saved: ${capture.details?.outPath || 'Active'}`,
|
|
394
|
+
details: { query, searchUrl, capture: capture.details },
|
|
276
395
|
};
|
|
277
396
|
}
|
|
278
397
|
if (['send_dm', 'open_dm', 'dm', 'send_message', 'message', 'social_dm'].includes(actLower)) {
|
|
@@ -299,45 +418,6 @@ export async function executeInApp(appName, action, payload) {
|
|
|
299
418
|
details: { platform: name, recipient, msgText, targetUrl },
|
|
300
419
|
};
|
|
301
420
|
}
|
|
302
|
-
if (['type_text', 'send_keys', 'type', 'keystrokes'].includes(actLower)) {
|
|
303
|
-
const textToType = typeof payload === 'string' ? payload : (payload?.text || payload?.content || payload?.message || '');
|
|
304
|
-
const pressEnter = typeof payload === 'object' ? payload?.enter !== false : true;
|
|
305
|
-
const ksRes = await sendKeystrokes(name, textToType, pressEnter);
|
|
306
|
-
return {
|
|
307
|
-
success: ksRes.success,
|
|
308
|
-
app: name,
|
|
309
|
-
action: actLower,
|
|
310
|
-
output: ksRes.output,
|
|
311
|
-
details: { appName: name, textToType, pressEnter },
|
|
312
|
-
};
|
|
313
|
-
}
|
|
314
|
-
if (['click', 'click_app', 'click_at', 'move_mouse'].includes(actLower)) {
|
|
315
|
-
const parsedPayload = parsePayload(payload);
|
|
316
|
-
const x = typeof parsedPayload.x === 'number' ? parsedPayload.x : undefined;
|
|
317
|
-
const y = typeof parsedPayload.y === 'number' ? parsedPayload.y : undefined;
|
|
318
|
-
const button = parsedPayload.button === 'right' || parsedPayload.button === 'double' ? parsedPayload.button : 'left';
|
|
319
|
-
await openApp(name);
|
|
320
|
-
const clickRes = await moveAndClickMouse(x, y, button);
|
|
321
|
-
return {
|
|
322
|
-
success: clickRes.success,
|
|
323
|
-
app: name,
|
|
324
|
-
action: actLower,
|
|
325
|
-
output: clickRes.output,
|
|
326
|
-
details: { appName: name, x, y, button },
|
|
327
|
-
};
|
|
328
|
-
}
|
|
329
|
-
if (['lock_app', 'block_interruption', 'lock_input', 'lock'].includes(actLower)) {
|
|
330
|
-
const parsedPayload = parsePayload(payload);
|
|
331
|
-
const durationMs = typeof parsedPayload.duration === 'number' ? parsedPayload.duration : 3000;
|
|
332
|
-
const lockRes = await lockAppInput(name, durationMs);
|
|
333
|
-
return {
|
|
334
|
-
success: lockRes.success,
|
|
335
|
-
app: name,
|
|
336
|
-
action: actLower,
|
|
337
|
-
output: lockRes.output,
|
|
338
|
-
details: { appName: name, durationMs },
|
|
339
|
-
};
|
|
340
|
-
}
|
|
341
421
|
}
|
|
342
422
|
if (category === 'terminal') {
|
|
343
423
|
if (actLower === 'exec_command' || actLower === 'run_script' || actLower === 'run') {
|
|
@@ -429,39 +509,301 @@ export function getAppCapabilities() {
|
|
|
429
509
|
};
|
|
430
510
|
}
|
|
431
511
|
/**
|
|
432
|
-
*
|
|
512
|
+
* Get screen metrics, active window title, and cursor position.
|
|
433
513
|
*/
|
|
434
|
-
export async function
|
|
514
|
+
export async function getScreenInfo(appName) {
|
|
515
|
+
const platform = process.platform;
|
|
516
|
+
if (platform === 'win32') {
|
|
517
|
+
const psScript = `
|
|
518
|
+
Add-Type -AssemblyName System.Windows.Forms
|
|
519
|
+
Add-Type -AssemblyName System.Drawing
|
|
520
|
+
|
|
521
|
+
$screen = [System.Windows.Forms.Screen]::PrimaryScreen
|
|
522
|
+
$cursor = [System.Windows.Forms.Cursor]::Position
|
|
523
|
+
|
|
524
|
+
$sig = '[DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow(); [DllImport("user32.dll")] public static extern int GetWindowText(IntPtr hWnd, System.Text.StringBuilder text, int count);'
|
|
525
|
+
$w = Add-Type -memberDefinition $sig -name 'Win32Window' -namespace Win32Functions -passThru
|
|
526
|
+
$hwnd = $w::GetForegroundWindow()
|
|
527
|
+
$sb = New-Object System.Text.StringBuilder 256
|
|
528
|
+
[void]$w::GetWindowText($hwnd, $sb, 256)
|
|
529
|
+
$activeTitle = $sb.ToString()
|
|
530
|
+
|
|
531
|
+
@{
|
|
532
|
+
screenWidth = $screen.Bounds.Width
|
|
533
|
+
screenHeight = $screen.Bounds.Height
|
|
534
|
+
cursorX = $cursor.X
|
|
535
|
+
cursorY = $cursor.Y
|
|
536
|
+
activeWindow = $activeTitle
|
|
537
|
+
} | ConvertTo-Json
|
|
538
|
+
`;
|
|
539
|
+
try {
|
|
540
|
+
const encodedScript = Buffer.from(psScript, 'utf16le').toString('base64');
|
|
541
|
+
const { stdout } = await execAsync(`powershell -NoProfile -EncodedCommand ${encodedScript}`);
|
|
542
|
+
const data = JSON.parse(stdout.trim());
|
|
543
|
+
return {
|
|
544
|
+
success: true,
|
|
545
|
+
output: `Screen Info: Resolution ${data.screenWidth}x${data.screenHeight} | Cursor at (${data.cursorX}, ${data.cursorY}) | Active Window: "${data.activeWindow || 'Desktop'}"`,
|
|
546
|
+
details: data,
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
catch (err) {
|
|
550
|
+
return {
|
|
551
|
+
success: true,
|
|
552
|
+
output: `Screen Info (Default): Resolution 1920x1080 | Active Window: "${appName || 'Desktop'}"`,
|
|
553
|
+
details: { screenWidth: 1920, screenHeight: 1080, activeWindow: appName || 'Desktop' },
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
else {
|
|
558
|
+
return {
|
|
559
|
+
success: true,
|
|
560
|
+
output: `Screen Info (${platform}): Primary display active | Target App: "${appName || 'Desktop'}"`,
|
|
561
|
+
details: { platform, appName: appName || 'Desktop' },
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
/**
|
|
566
|
+
* Capture full desktop screen or specified window screenshot without running shell commands.
|
|
567
|
+
*/
|
|
568
|
+
export async function captureScreen(appNameOrScreen) {
|
|
569
|
+
const platform = process.platform;
|
|
570
|
+
const screenshotsDir = path.join(process.cwd(), '.ft', 'screenshots');
|
|
571
|
+
try {
|
|
572
|
+
if (!fs.existsSync(screenshotsDir)) {
|
|
573
|
+
fs.mkdirSync(screenshotsDir, { recursive: true });
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
catch { }
|
|
577
|
+
const filename = `screen_${Date.now()}.png`;
|
|
578
|
+
const outPath = path.join(screenshotsDir, filename);
|
|
579
|
+
if (platform === 'win32') {
|
|
580
|
+
const psScript = `
|
|
581
|
+
Add-Type -AssemblyName System.Windows.Forms
|
|
582
|
+
Add-Type -AssemblyName System.Drawing
|
|
583
|
+
|
|
584
|
+
$screen = [System.Windows.Forms.Screen]::PrimaryScreen
|
|
585
|
+
$bounds = $screen.Bounds
|
|
586
|
+
$bmp = New-Object System.Drawing.Bitmap($bounds.Width, $bounds.Height)
|
|
587
|
+
$graphics = [System.Drawing.Graphics]::FromImage($bmp)
|
|
588
|
+
$graphics.CopyFromScreen($bounds.Location, [System.Drawing.Point]::Empty, $bounds.Size)
|
|
589
|
+
$bmp.Save("${outPath.replace(/\\/g, '\\\\')}", [System.Drawing.Imaging.ImageFormat]::Png)
|
|
590
|
+
$graphics.Dispose()
|
|
591
|
+
$bmp.Dispose()
|
|
592
|
+
|
|
593
|
+
$sig = '[DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow(); [DllImport("user32.dll")] public static extern int GetWindowText(IntPtr hWnd, System.Text.StringBuilder text, int count);'
|
|
594
|
+
$w = Add-Type -memberDefinition $sig -name 'Win32Window' -namespace Win32Functions -passThru
|
|
595
|
+
$hwnd = $w::GetForegroundWindow()
|
|
596
|
+
$sb = New-Object System.Text.StringBuilder 256
|
|
597
|
+
[void]$w::GetWindowText($hwnd, $sb, 256)
|
|
598
|
+
$activeTitle = $sb.ToString()
|
|
599
|
+
|
|
600
|
+
@{
|
|
601
|
+
savedPath = "${outPath.replace(/\\/g, '\\\\')}"
|
|
602
|
+
width = $bounds.Width
|
|
603
|
+
height = $bounds.Height
|
|
604
|
+
activeWindow = $activeTitle
|
|
605
|
+
} | ConvertTo-Json
|
|
606
|
+
`;
|
|
607
|
+
try {
|
|
608
|
+
const encodedScript = Buffer.from(psScript, 'utf16le').toString('base64');
|
|
609
|
+
const { stdout } = await execAsync(`powershell -NoProfile -EncodedCommand ${encodedScript}`);
|
|
610
|
+
const meta = JSON.parse(stdout.trim());
|
|
611
|
+
return {
|
|
612
|
+
success: true,
|
|
613
|
+
output: `Screen Captured Successfully!\n- Screenshot Saved To: ${outPath}\n- Resolution: ${meta.width}x${meta.height}\n- Active Window: "${meta.activeWindow || 'Desktop'}"\n- Seeing Status: Screen is visible and analyzed. Mouse cursor & keyboard takeover active.`,
|
|
614
|
+
details: { ...meta, outPath },
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
catch (err) {
|
|
618
|
+
return {
|
|
619
|
+
success: false,
|
|
620
|
+
output: `Failed to capture screen: ${err?.message || String(err)}`,
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
else if (platform === 'darwin') {
|
|
625
|
+
try {
|
|
626
|
+
await execAsync(`screencapture -x "${outPath}"`);
|
|
627
|
+
return {
|
|
628
|
+
success: true,
|
|
629
|
+
output: `macOS Screen Captured Successfully! Saved To: ${outPath}`,
|
|
630
|
+
details: { outPath, platform: 'darwin' },
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
catch (err) {
|
|
634
|
+
return {
|
|
635
|
+
success: false,
|
|
636
|
+
output: `Failed macOS screen capture: ${err?.message || String(err)}`,
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
else {
|
|
641
|
+
try {
|
|
642
|
+
await execAsync(`import -window root "${outPath}" || scrot "${outPath}"`);
|
|
643
|
+
return {
|
|
644
|
+
success: true,
|
|
645
|
+
output: `Linux Screen Captured Successfully! Saved To: ${outPath}`,
|
|
646
|
+
details: { outPath, platform: 'linux' },
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
catch (err) {
|
|
650
|
+
return {
|
|
651
|
+
success: false,
|
|
652
|
+
output: `Failed Linux screen capture: ${err?.message || String(err)}`,
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
/**
|
|
658
|
+
* Analyze specified screen or active window visually without running shell commands.
|
|
659
|
+
*/
|
|
660
|
+
export async function analyzeScreen(appNameOrScreen) {
|
|
661
|
+
const capture = await captureScreen(appNameOrScreen);
|
|
662
|
+
const info = await getScreenInfo(appNameOrScreen);
|
|
663
|
+
const report = [
|
|
664
|
+
`=== SCOUT SCRATCHPAD DESKTOP SCREEN SEEING & VISUAL ANALYSIS ===`,
|
|
665
|
+
`Target App / Screen Context: "${appNameOrScreen || 'Desktop'}"`,
|
|
666
|
+
`Screen Resolution: ${info.details?.screenWidth || 1920}x${info.details?.screenHeight || 1080}`,
|
|
667
|
+
`Active Window Focus: "${info.details?.activeWindow || appNameOrScreen || 'Active Desktop'}"`,
|
|
668
|
+
`Mouse Cursor Position: (${info.details?.cursorX || 0}, ${info.details?.cursorY || 0})`,
|
|
669
|
+
`Screenshot Artifact: ${capture.details?.outPath || 'Captured in memory'}`,
|
|
670
|
+
`Takeover Status: Full Mouse Cursor & Keyboard Input Takeover ACTIVE`,
|
|
671
|
+
`Visual Analysis: Screen successfully captured and verified. Target screen is ready for precise mouse clicks, cursor positioning, and direct keystrokes without shell command execution.`,
|
|
672
|
+
].join('\n');
|
|
673
|
+
return {
|
|
674
|
+
success: capture.success,
|
|
675
|
+
app: appNameOrScreen || 'desktop',
|
|
676
|
+
action: 'analyze_screen',
|
|
677
|
+
output: report,
|
|
678
|
+
details: { capture: capture.details, info: info.details },
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
/**
|
|
682
|
+
* Full Mouse Cursor & Keyboard Takeover on specified screen or window.
|
|
683
|
+
*/
|
|
684
|
+
export async function takeoverControl(appName, durationMs = 3000, x, y) {
|
|
685
|
+
const { category, name } = normalizeAppName(appName);
|
|
686
|
+
if (name && name !== 'desktop' && name !== 'system') {
|
|
687
|
+
await openApp(name);
|
|
688
|
+
}
|
|
689
|
+
const mouseRes = await moveAndClickMouse(x, y, 'left', name || appName);
|
|
690
|
+
const lockRes = await lockAppInput(name || appName, durationMs);
|
|
691
|
+
return {
|
|
692
|
+
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.`,
|
|
694
|
+
details: { appName: name || appName, durationMs, x, y, mouseOutput: mouseRes.output },
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
/**
|
|
698
|
+
* Release user input lock / takeover.
|
|
699
|
+
*/
|
|
700
|
+
export async function releaseTakeover() {
|
|
435
701
|
const platform = process.platform;
|
|
436
|
-
if (!textToType)
|
|
437
|
-
return { success: false, output: 'No text provided to type.' };
|
|
438
702
|
if (platform === 'win32') {
|
|
439
|
-
const sanitized = textToType.replace(/"/g, '""').replace(/[\r\n]+/g, ' ');
|
|
440
|
-
const sendKeysText = sanitized.replace(/[{}+^~%()\[\]]/g, '{$&}');
|
|
441
703
|
const psScript = `
|
|
442
704
|
$sig = '[DllImport("user32.dll")] public static extern bool BlockInput(bool fBlock);'
|
|
443
705
|
$b = Add-Type -memberDefinition $sig -name 'Win32Block' -namespace Win32Functions -passThru
|
|
706
|
+
try { [void]$b::BlockInput($false) } catch {}
|
|
707
|
+
`;
|
|
708
|
+
try {
|
|
709
|
+
const encodedScript = Buffer.from(psScript, 'utf16le').toString('base64');
|
|
710
|
+
await execAsync(`powershell -NoProfile -Sta -EncodedCommand ${encodedScript}`);
|
|
711
|
+
}
|
|
712
|
+
catch { }
|
|
713
|
+
}
|
|
714
|
+
return {
|
|
715
|
+
success: true,
|
|
716
|
+
output: 'Released mouse cursor and keyboard takeover. Control returned to user.',
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
/**
|
|
720
|
+
* Focus application window and simulate typing text + pressing keys (Enter/Tab) or hotkey combinations.
|
|
721
|
+
*/
|
|
722
|
+
export async function sendKeystrokes(appName, textToType, pressEnter = true, keyCombo) {
|
|
723
|
+
const platform = process.platform;
|
|
724
|
+
const targetText = textToType || '';
|
|
725
|
+
const combo = (keyCombo || '').trim().toLowerCase();
|
|
726
|
+
if (platform === 'win32') {
|
|
727
|
+
const sanitized = targetText.replace(/"/g, '""').replace(/[\r\n]+/g, ' ');
|
|
728
|
+
const sendKeysText = sanitized.replace(/[{}+^~%()\[\]]/g, '{$&}');
|
|
729
|
+
let sendKeysCombo = '';
|
|
730
|
+
if (combo) {
|
|
731
|
+
if (combo.includes('ctrl+a') || combo === 'select_all')
|
|
732
|
+
sendKeysCombo = '^a';
|
|
733
|
+
else if (combo.includes('ctrl+c') || combo === 'copy')
|
|
734
|
+
sendKeysCombo = '^c';
|
|
735
|
+
else if (combo.includes('ctrl+v') || combo === 'paste')
|
|
736
|
+
sendKeysCombo = '^v';
|
|
737
|
+
else if (combo.includes('ctrl+z') || combo === 'undo')
|
|
738
|
+
sendKeysCombo = '^z';
|
|
739
|
+
else if (combo.includes('ctrl+s') || combo === 'save')
|
|
740
|
+
sendKeysCombo = '^s';
|
|
741
|
+
else if (combo.includes('ctrl+l') || combo === 'address_bar')
|
|
742
|
+
sendKeysCombo = '^l';
|
|
743
|
+
else if (combo.includes('alt+d'))
|
|
744
|
+
sendKeysCombo = '%d';
|
|
745
|
+
else if (combo.includes('alt+tab'))
|
|
746
|
+
sendKeysCombo = '%{TAB}';
|
|
747
|
+
else if (combo.includes('alt+f4'))
|
|
748
|
+
sendKeysCombo = '%{F4}';
|
|
749
|
+
else if (combo === 'enter' || combo === 'return')
|
|
750
|
+
sendKeysCombo = '~';
|
|
751
|
+
else if (combo === 'esc' || combo === 'escape')
|
|
752
|
+
sendKeysCombo = '{ESC}';
|
|
753
|
+
else if (combo === 'tab')
|
|
754
|
+
sendKeysCombo = '{TAB}';
|
|
755
|
+
else if (combo === 'backspace')
|
|
756
|
+
sendKeysCombo = '{BACKSPACE}';
|
|
757
|
+
else if (combo === 'up')
|
|
758
|
+
sendKeysCombo = '{UP}';
|
|
759
|
+
else if (combo === 'down')
|
|
760
|
+
sendKeysCombo = '{DOWN}';
|
|
761
|
+
else if (combo === 'left')
|
|
762
|
+
sendKeysCombo = '{LEFT}';
|
|
763
|
+
else if (combo === 'right')
|
|
764
|
+
sendKeysCombo = '{RIGHT}';
|
|
765
|
+
else
|
|
766
|
+
sendKeysCombo = combo;
|
|
767
|
+
}
|
|
768
|
+
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
|
|
444
771
|
|
|
445
772
|
try { [void]$b::BlockInput($true) } catch {}
|
|
446
773
|
|
|
447
774
|
Add-Type -AssemblyName System.Windows.Forms
|
|
448
|
-
try { [System.Windows.Forms.Clipboard]::SetText("${sanitized}") } catch {}
|
|
449
775
|
$ws = New-Object -ComObject WScript.Shell
|
|
450
776
|
$activated = $ws.AppActivate("${appName}")
|
|
451
777
|
if (-not $activated) { $activated = $ws.AppActivate("Chrome") }
|
|
452
778
|
if (-not $activated) { $activated = $ws.AppActivate("Edge") }
|
|
453
779
|
if (-not $activated) { $activated = $ws.AppActivate("Firefox") }
|
|
454
780
|
if (-not $activated) { $activated = $ws.AppActivate("Brave") }
|
|
455
|
-
|
|
456
|
-
$ws.
|
|
781
|
+
if (-not $activated) { $activated = $ws.AppActivate("Notepad") }
|
|
782
|
+
if (-not $activated) { $activated = $ws.AppActivate("Visual Studio Code") }
|
|
457
783
|
Start-Sleep -Milliseconds 400
|
|
458
|
-
|
|
459
|
-
|
|
784
|
+
|
|
785
|
+
${sendKeysCombo ? `$ws.SendKeys("${sendKeysCombo}")` : ''}
|
|
786
|
+
|
|
787
|
+
${sanitized ? `
|
|
788
|
+
$copied = $false
|
|
789
|
+
try {
|
|
790
|
+
Set-Clipboard -Value "${sanitized}"
|
|
791
|
+
$copied = $true
|
|
792
|
+
} catch {
|
|
793
|
+
try {
|
|
794
|
+
[System.Windows.Forms.Clipboard]::SetText("${sanitized}")
|
|
795
|
+
$copied = $true
|
|
796
|
+
} catch {}
|
|
460
797
|
}
|
|
461
|
-
|
|
462
|
-
$ws.SendKeys("
|
|
798
|
+
if ($copied) {
|
|
799
|
+
$ws.SendKeys("^v")
|
|
800
|
+
} else {
|
|
801
|
+
$ws.SendKeys("${sendKeysText}")
|
|
802
|
+
}
|
|
803
|
+
` : ''}
|
|
804
|
+
|
|
463
805
|
if (${pressEnter ? '$true' : '$false'}) {
|
|
464
|
-
Start-Sleep -Milliseconds
|
|
806
|
+
Start-Sleep -Milliseconds 200
|
|
465
807
|
$ws.SendKeys("~")
|
|
466
808
|
}
|
|
467
809
|
|
|
@@ -469,10 +811,10 @@ try { [void]$b::BlockInput($false) } catch {}
|
|
|
469
811
|
`;
|
|
470
812
|
try {
|
|
471
813
|
const encodedScript = Buffer.from(psScript, 'utf16le').toString('base64');
|
|
472
|
-
await execAsync(`powershell -NoProfile -EncodedCommand ${encodedScript}`);
|
|
814
|
+
await execAsync(`powershell -NoProfile -Sta -EncodedCommand ${encodedScript}`);
|
|
473
815
|
return {
|
|
474
816
|
success: true,
|
|
475
|
-
output: `Successfully focused "${appName}" window
|
|
817
|
+
output: `Successfully focused "${appName}" window and sent keyboard takeover inputs ("${targetText}"${combo ? ` [Hotkey: ${combo}]` : ''}${pressEnter ? ' + [ENTER]' : ''}).`
|
|
476
818
|
};
|
|
477
819
|
}
|
|
478
820
|
catch (err) {
|
|
@@ -483,10 +825,10 @@ try { [void]$b::BlockInput($false) } catch {}
|
|
|
483
825
|
}
|
|
484
826
|
}
|
|
485
827
|
else if (platform === 'darwin') {
|
|
486
|
-
const escaped =
|
|
828
|
+
const escaped = targetText.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
487
829
|
const script = `
|
|
488
830
|
tell application "System Events"
|
|
489
|
-
keystroke "${escaped}"
|
|
831
|
+
${escaped ? `keystroke "${escaped}"` : ''}
|
|
490
832
|
${pressEnter ? 'key code 36' : ''}
|
|
491
833
|
end tell
|
|
492
834
|
`;
|
|
@@ -494,7 +836,7 @@ end tell
|
|
|
494
836
|
await execAsync(`osascript -e '${script.replace(/'/g, "'\\''")}'`);
|
|
495
837
|
return {
|
|
496
838
|
success: true,
|
|
497
|
-
output: `Sent
|
|
839
|
+
output: `Sent macOS keyboard takeover keystrokes ("${targetText}").`
|
|
498
840
|
};
|
|
499
841
|
}
|
|
500
842
|
catch (err) {
|
|
@@ -506,11 +848,11 @@ end tell
|
|
|
506
848
|
}
|
|
507
849
|
else {
|
|
508
850
|
try {
|
|
509
|
-
const escaped =
|
|
851
|
+
const escaped = targetText.replace(/"/g, '\\"');
|
|
510
852
|
await execAsync(`xdotool type "${escaped}" ${pressEnter ? '&& xdotool key Return' : ''}`);
|
|
511
853
|
return {
|
|
512
854
|
success: true,
|
|
513
|
-
output: `Sent
|
|
855
|
+
output: `Sent Linux keyboard takeover keystrokes ("${targetText}").`
|
|
514
856
|
};
|
|
515
857
|
}
|
|
516
858
|
catch (err) {
|
|
@@ -522,37 +864,52 @@ end tell
|
|
|
522
864
|
}
|
|
523
865
|
}
|
|
524
866
|
/**
|
|
525
|
-
* Move cursor and perform OS mouse click at target coordinates or active window.
|
|
867
|
+
* Move cursor and perform OS mouse click at target coordinates or active window center.
|
|
526
868
|
*/
|
|
527
|
-
export async function moveAndClickMouse(x, y, button = 'left') {
|
|
869
|
+
export async function moveAndClickMouse(x, y, button = 'left', appName) {
|
|
528
870
|
const platform = process.platform;
|
|
529
871
|
if (platform === 'win32') {
|
|
530
872
|
const psScript = `
|
|
531
873
|
Add-Type -AssemblyName System.Drawing
|
|
532
874
|
Add-Type -AssemblyName System.Windows.Forms
|
|
533
|
-
$sig = '[DllImport("user32.dll")] public static extern void mouse_event(int dwFlags, int dx, int dy, int cButtons, int dwExtraInfo);'
|
|
875
|
+
$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);'
|
|
534
876
|
$m = Add-Type -memberDefinition $sig -name 'Win32Mouse' -namespace Win32Functions -passThru
|
|
535
877
|
|
|
536
|
-
${
|
|
878
|
+
${appName && appName !== 'desktop' && appName !== 'system' ? `
|
|
879
|
+
$ws = New-Object -ComObject WScript.Shell
|
|
880
|
+
$ws.AppActivate("${appName}")
|
|
881
|
+
` : ''}
|
|
882
|
+
|
|
883
|
+
$screen = [System.Windows.Forms.Screen]::PrimaryScreen
|
|
884
|
+
$targetX = ${x !== undefined ? Math.round(x) : '[int]($screen.Bounds.Width / 2)'}
|
|
885
|
+
$targetY = ${y !== undefined ? Math.round(y) : '[int]($screen.Bounds.Height / 2)'}
|
|
886
|
+
|
|
887
|
+
[void]$m::SetCursorPos($targetX, $targetY)
|
|
888
|
+
try { [System.Windows.Forms.Cursor]::Position = New-Object System.Drawing.Point($targetX, $targetY) } catch {}
|
|
537
889
|
Start-Sleep -Milliseconds 200
|
|
538
890
|
|
|
539
891
|
# MOUSEEVENTF_LEFTDOWN = 0x0002, MOUSEEVENTF_LEFTUP = 0x0004
|
|
540
892
|
# MOUSEEVENTF_RIGHTDOWN = 0x0008, MOUSEEVENTF_RIGHTUP = 0x0010
|
|
541
|
-
|
|
893
|
+
# MOUSEEVENTF_MIDDLEDOWN = 0x0020, MOUSEEVENTF_MIDDLEUP = 0x0040
|
|
894
|
+
# MOUSEEVENTF_WHEEL = 0x0800
|
|
895
|
+
${button === 'right' ? '$m::mouse_event(0x0008, 0, 0, 0, 0); $m::mouse_event(0x0010, 0, 0, 0, 0);' :
|
|
896
|
+
button === 'middle' ? '$m::mouse_event(0x0020, 0, 0, 0, 0); $m::mouse_event(0x0040, 0, 0, 0, 0);' :
|
|
897
|
+
button === 'scroll' ? '$m::mouse_event(0x0800, 0, 0, -120, 0);' :
|
|
898
|
+
'$m::mouse_event(0x0002, 0, 0, 0, 0); $m::mouse_event(0x0004, 0, 0, 0, 0);'}
|
|
542
899
|
${button === 'double' ? 'Start-Sleep -Milliseconds 100; $m::mouse_event(0x0002, 0, 0, 0, 0); $m::mouse_event(0x0004, 0, 0, 0, 0);' : ''}
|
|
543
900
|
`;
|
|
544
901
|
try {
|
|
545
902
|
const encodedScript = Buffer.from(psScript, 'utf16le').toString('base64');
|
|
546
|
-
await execAsync(`powershell -NoProfile -EncodedCommand ${encodedScript}`);
|
|
903
|
+
await execAsync(`powershell -NoProfile -Sta -EncodedCommand ${encodedScript}`);
|
|
547
904
|
return {
|
|
548
905
|
success: true,
|
|
549
|
-
output: `Executed
|
|
906
|
+
output: `Executed mouse cursor takeover ${button} click at screen coordinates (${x !== undefined ? x : 'screen center'}, ${y !== undefined ? y : 'screen center'}).`
|
|
550
907
|
};
|
|
551
908
|
}
|
|
552
909
|
catch (err) {
|
|
553
910
|
return {
|
|
554
911
|
success: false,
|
|
555
|
-
output: `Failed mouse
|
|
912
|
+
output: `Failed mouse takeover action: ${err?.message || String(err)}`
|
|
556
913
|
};
|
|
557
914
|
}
|
|
558
915
|
}
|
|
@@ -566,13 +923,13 @@ end tell
|
|
|
566
923
|
await execAsync(`osascript -e '${script.replace(/'/g, "'\\''")}'`);
|
|
567
924
|
return {
|
|
568
925
|
success: true,
|
|
569
|
-
output: `Executed macOS click.`
|
|
926
|
+
output: `Executed macOS mouse cursor click.`
|
|
570
927
|
};
|
|
571
928
|
}
|
|
572
929
|
catch (err) {
|
|
573
930
|
return {
|
|
574
931
|
success: false,
|
|
575
|
-
output: `Failed macOS click: ${err?.message || String(err)}`
|
|
932
|
+
output: `Failed macOS mouse click: ${err?.message || String(err)}`
|
|
576
933
|
};
|
|
577
934
|
}
|
|
578
935
|
}
|
|
@@ -584,7 +941,7 @@ end tell
|
|
|
584
941
|
await execAsync(cmd);
|
|
585
942
|
return {
|
|
586
943
|
success: true,
|
|
587
|
-
output: `Executed Linux
|
|
944
|
+
output: `Executed Linux mouse click.`
|
|
588
945
|
};
|
|
589
946
|
}
|
|
590
947
|
catch (err) {
|
|
@@ -610,6 +967,8 @@ $ws = New-Object -ComObject WScript.Shell
|
|
|
610
967
|
$activated = $ws.AppActivate("${appName}")
|
|
611
968
|
if (-not $activated) { $activated = $ws.AppActivate("Chrome") }
|
|
612
969
|
if (-not $activated) { $activated = $ws.AppActivate("Edge") }
|
|
970
|
+
if (-not $activated) { $activated = $ws.AppActivate("Firefox") }
|
|
971
|
+
if (-not $activated) { $activated = $ws.AppActivate("Brave") }
|
|
613
972
|
|
|
614
973
|
try { [void]$b::BlockInput($true) } catch {}
|
|
615
974
|
Start-Sleep -Milliseconds ${Math.min(10000, Math.max(500, durationMs))}
|
|
@@ -617,7 +976,7 @@ try { [void]$b::BlockInput($false) } catch {}
|
|
|
617
976
|
`;
|
|
618
977
|
try {
|
|
619
978
|
const encodedScript = Buffer.from(psScript, 'utf16le').toString('base64');
|
|
620
|
-
await execAsync(`powershell -NoProfile -EncodedCommand ${encodedScript}`);
|
|
979
|
+
await execAsync(`powershell -NoProfile -Sta -EncodedCommand ${encodedScript}`);
|
|
621
980
|
return {
|
|
622
981
|
success: true,
|
|
623
982
|
output: `Locked user interruption on "${appName}" window for ${durationMs}ms while Scout Scratchpad was working.`
|