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.
@@ -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 with exit code 0 or minor warnings
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 (actLower === 'fetch_page' || actLower === 'navigate' || actLower === 'read_url' || actLower === 'inspect_dom') {
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 web search opened for query: "${query}" at ${searchUrl}`,
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
- * Focus application window and simulate typing text + pressing keys (Enter/Tab).
512
+ * Get screen metrics, active window title, and cursor position.
433
513
  */
434
- export async function sendKeystrokes(appName, textToType, pressEnter = true) {
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
- Start-Sleep -Milliseconds 1200
456
- $ws.SendKeys("^v")
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
- if (${pressEnter ? '$true' : '$false'}) {
459
- $ws.SendKeys("~")
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
- Start-Sleep -Milliseconds 400
462
- $ws.SendKeys("${sendKeysText}")
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 400
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, set clipboard text, pasted Ctrl+V, and sent keystrokes ("${textToType}"${pressEnter ? ' + [ENTER]' : ''}).`
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 = textToType.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
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 keystrokes to active window on macOS ("${textToType}").`
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 = textToType.replace(/"/g, '\\"');
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 keystrokes via xdotool ("${textToType}").`
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
- ${(x !== undefined && y !== undefined) ? `[System.Windows.Forms.Cursor]::Position = New-Object System.Drawing.Point(${Math.round(x)}, ${Math.round(y)})` : ''}
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
- ${button === 'right' ? '$m::mouse_event(0x0008, 0, 0, 0, 0); $m::mouse_event(0x0010, 0, 0, 0, 0);' : '$m::mouse_event(0x0002, 0, 0, 0, 0); $m::mouse_event(0x0004, 0, 0, 0, 0);'}
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 OS mouse ${button} click${(x !== undefined && y !== undefined) ? ` at coordinates (${x}, ${y})` : ' at active cursor position'}.`
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 click: ${err?.message || String(err)}`
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 xdotool click.`
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.`