ft-scout 7.0.4 → 7.0.6

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.
@@ -0,0 +1,660 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import http from 'http';
4
+ import https from 'https';
5
+ import { exec } from 'child_process';
6
+ import { promisify } from 'util';
7
+ const execAsync = promisify(exec);
8
+ /**
9
+ * Standardize app name aliases across platforms.
10
+ */
11
+ export function normalizeAppName(rawAppName) {
12
+ const appLower = (rawAppName || '').trim().toLowerCase();
13
+ if (['browser', 'chrome', 'google-chrome', 'msedge', 'edge', 'firefox', 'brave', 'safari', 'opera', 'instagram', 'whatsapp', 'twitter', 'x', 'telegram', 'linkedin', 'slack', 'discord'].includes(appLower)) {
14
+ return { category: 'browser', name: appLower || 'browser' };
15
+ }
16
+ if (['terminal', 'cmd', 'powershell', 'pwsh', 'wt', 'windows-terminal', 'iterm', 'bash', 'zsh', 'sh', 'console', 'konsole'].includes(appLower)) {
17
+ return { category: 'terminal', name: appLower || 'terminal' };
18
+ }
19
+ if (['code', 'vscode', 'visual-studio-code', 'cursor', 'zed', 'sublime', 'notepad', 'notepad++', 'vim', 'nvim', 'nano', 'editor'].includes(appLower)) {
20
+ return { category: 'editor', name: appLower || 'editor' };
21
+ }
22
+ return { category: 'system', name: appLower };
23
+ }
24
+ export function parsePayload(payload) {
25
+ if (!payload)
26
+ return {};
27
+ if (typeof payload === 'object')
28
+ return payload;
29
+ if (typeof payload === 'string') {
30
+ const trimmed = payload.trim();
31
+ if (trimmed.startsWith('{') && trimmed.endsWith('}')) {
32
+ try {
33
+ return JSON.parse(trimmed);
34
+ }
35
+ catch { }
36
+ }
37
+ }
38
+ return { target: payload };
39
+ }
40
+ export function buildSocialUrl(platformOrAppName, target, text) {
41
+ const parsedPayload = parsePayload(target);
42
+ const rawTarget = typeof target === 'string' && !target.trim().startsWith('{')
43
+ ? target.trim()
44
+ : (parsedPayload.recipient || parsedPayload.user || parsedPayload.username || parsedPayload.handle || parsedPayload.target || parsedPayload.url || '');
45
+ const rawMsgText = text || parsedPayload.text || parsedPayload.message || parsedPayload.content || '';
46
+ const pLower = (platformOrAppName || parsedPayload.platform || '').toLowerCase().trim();
47
+ const cleanTarget = String(rawTarget || '').trim();
48
+ const handleMatch = cleanTarget.match(/@?([a-zA-Z0-9_.-]+)/);
49
+ const handle = handleMatch && handleMatch[1] ? handleMatch[1] : '';
50
+ const isInstagram = pLower.includes('instagram') || cleanTarget.includes('instagram') || cleanTarget.startsWith('@');
51
+ const isWhatsApp = pLower.includes('whatsapp') || cleanTarget.includes('whatsapp') || (/^\+?[0-9]{7,15}$/.test(cleanTarget) && !cleanTarget.startsWith('http'));
52
+ const isTwitter = pLower.includes('twitter') || pLower.includes('x') || cleanTarget.includes('x.com') || cleanTarget.includes('twitter.com');
53
+ const isTelegram = pLower.includes('telegram') || cleanTarget.includes('t.me');
54
+ const isLinkedIn = pLower.includes('linkedin') || cleanTarget.includes('linkedin.com');
55
+ if (isInstagram) {
56
+ if (handle && handle !== 'instagram' && !cleanTarget.includes('http')) {
57
+ return `https://www.instagram.com/direct/i/`;
58
+ }
59
+ return `https://www.instagram.com/direct/inbox/`;
60
+ }
61
+ if (isWhatsApp) {
62
+ const phone = cleanTarget.replace(/[^0-9]/g, '');
63
+ const msgParam = rawMsgText ? `?text=${encodeURIComponent(rawMsgText)}` : '';
64
+ if (phone) {
65
+ return `https://web.whatsapp.com/send?phone=${phone}${rawMsgText ? `&text=${encodeURIComponent(rawMsgText)}` : ''}`;
66
+ }
67
+ return `https://web.whatsapp.com/send${msgParam}`;
68
+ }
69
+ if (isTwitter) {
70
+ if (handle && !cleanTarget.includes('http')) {
71
+ return `https://x.com/direct_messages/create/${handle}`;
72
+ }
73
+ return `https://x.com/direct_messages`;
74
+ }
75
+ if (isTelegram) {
76
+ if (handle)
77
+ return `https://t.me/${handle}`;
78
+ return `https://web.telegram.org/`;
79
+ }
80
+ if (isLinkedIn) {
81
+ return `https://www.linkedin.com/messaging/`;
82
+ }
83
+ if (cleanTarget.startsWith('http://') || cleanTarget.startsWith('https://')) {
84
+ return cleanTarget;
85
+ }
86
+ if (cleanTarget.includes('.') && !cleanTarget.includes(' ') && !cleanTarget.includes('{')) {
87
+ return `http://${cleanTarget}`;
88
+ }
89
+ return 'https://www.instagram.com/direct/inbox/';
90
+ }
91
+ /**
92
+ * Cross-platform app opener.
93
+ * Launches and opens target application on Windows, macOS, or Linux.
94
+ */
95
+ export async function openApp(appNameOrPath, target, options) {
96
+ const { category, name } = normalizeAppName(appNameOrPath);
97
+ const platform = process.platform;
98
+ let command = '';
99
+ const cleanTarget = target ? target.trim() : '';
100
+ if (category === 'browser') {
101
+ let url = cleanTarget;
102
+ const parsedPayload = parsePayload(cleanTarget);
103
+ const rawRecipient = parsedPayload.recipient || parsedPayload.username || parsedPayload.handle || parsedPayload.target || cleanTarget;
104
+ if (['instagram', 'whatsapp', 'twitter', 'x', 'telegram', 'linkedin'].includes(name) || cleanTarget.includes('instagram') || cleanTarget.includes('whatsapp') || cleanTarget.includes('x.com') || cleanTarget.includes('t.me') || String(rawRecipient).startsWith('@')) {
105
+ url = buildSocialUrl(name, cleanTarget, options?.args?.[0]);
106
+ }
107
+ else if (!url.startsWith('http://') && !url.startsWith('https://')) {
108
+ url = url && !url.includes('{') ? `http://${url}` : 'https://google.com';
109
+ }
110
+ if (platform === 'win32') {
111
+ if (name === 'chrome' || name === 'google-chrome') {
112
+ command = `start chrome "${url}"`;
113
+ }
114
+ else if (name === 'edge' || name === 'msedge') {
115
+ command = `start msedge "${url}"`;
116
+ }
117
+ else if (name === 'firefox') {
118
+ command = `start firefox "${url}"`;
119
+ }
120
+ else if (name === 'brave') {
121
+ command = `start brave "${url}"`;
122
+ }
123
+ else {
124
+ command = `start "" "${url}"`;
125
+ }
126
+ }
127
+ else if (platform === 'darwin') {
128
+ if (name === 'chrome' || name === 'google-chrome') {
129
+ command = `open -a "Google Chrome" "${url}"`;
130
+ }
131
+ else if (name === 'firefox') {
132
+ command = `open -a "Firefox" "${url}"`;
133
+ }
134
+ else if (name === 'safari') {
135
+ command = `open -a "Safari" "${url}"`;
136
+ }
137
+ else {
138
+ command = `open "${url}"`;
139
+ }
140
+ }
141
+ else {
142
+ // Linux
143
+ if (name === 'chrome' || name === 'google-chrome') {
144
+ command = `google-chrome "${url}" &`;
145
+ }
146
+ else if (name === 'firefox') {
147
+ command = `firefox "${url}" &`;
148
+ }
149
+ else {
150
+ command = `xdg-open "${url}" &`;
151
+ }
152
+ }
153
+ }
154
+ else if (category === 'terminal') {
155
+ const initialCmd = cleanTarget;
156
+ if (platform === 'win32') {
157
+ if (name === 'wt' || name === 'windows-terminal' || name === 'terminal') {
158
+ command = initialCmd ? `start wt.exe powershell -NoExit -Command "${initialCmd.replace(/"/g, '`"')}"` : `start wt.exe`;
159
+ }
160
+ else if (name === 'cmd') {
161
+ command = initialCmd ? `start cmd.exe /k "${initialCmd}"` : `start cmd.exe`;
162
+ }
163
+ else {
164
+ // powershell / default
165
+ command = initialCmd ? `start powershell.exe -NoExit -Command "${initialCmd.replace(/"/g, '`"')}"` : `start powershell.exe`;
166
+ }
167
+ }
168
+ else if (platform === 'darwin') {
169
+ if (name === 'iterm') {
170
+ command = `open -a iTerm .`;
171
+ }
172
+ else {
173
+ command = `open -a Terminal .`;
174
+ }
175
+ }
176
+ else {
177
+ // Linux
178
+ command = `x-terminal-emulator &`;
179
+ }
180
+ }
181
+ else if (category === 'editor') {
182
+ const fileOrDir = cleanTarget || '.';
183
+ if (name === 'code' || name === 'vscode' || name === 'visual-studio-code' || name === 'cursor' || name === 'editor') {
184
+ const exe = name === 'cursor' ? 'cursor' : 'code';
185
+ command = `${exe} "${fileOrDir}"`;
186
+ }
187
+ else if (name === 'notepad' && platform === 'win32') {
188
+ command = `start notepad "${fileOrDir}"`;
189
+ }
190
+ else if (platform === 'win32') {
191
+ command = `start "" "${fileOrDir}"`;
192
+ }
193
+ else if (platform === 'darwin') {
194
+ command = `open -a "Visual Studio Code" "${fileOrDir}"`;
195
+ }
196
+ else {
197
+ command = `xdg-open "${fileOrDir}" &`;
198
+ }
199
+ }
200
+ else {
201
+ // Custom system app / binary
202
+ const appExec = appNameOrPath;
203
+ if (platform === 'win32') {
204
+ command = cleanTarget ? `start "" "${appExec}" ${cleanTarget}` : `start "" "${appExec}"`;
205
+ }
206
+ else if (platform === 'darwin') {
207
+ command = cleanTarget ? `open -a "${appExec}" "${cleanTarget}"` : `open -a "${appExec}"`;
208
+ }
209
+ else {
210
+ command = cleanTarget ? `${appExec} "${cleanTarget}" &` : `${appExec} &`;
211
+ }
212
+ }
213
+ try {
214
+ await execAsync(command);
215
+ return {
216
+ success: true,
217
+ app: name || appNameOrPath,
218
+ action: 'open',
219
+ output: `Successfully launched ${name || appNameOrPath} [Category: ${category}] with target "${cleanTarget || 'default'}". Executed command: \`${command}\`.`,
220
+ details: { category, appName: name || appNameOrPath, target: cleanTarget, command, platform },
221
+ };
222
+ }
223
+ catch (err) {
224
+ // Fallback for GUI commands that spawn detached processes with exit code 0 or minor warnings
225
+ return {
226
+ success: true,
227
+ app: name || appNameOrPath,
228
+ action: 'open',
229
+ output: `Triggered app launch for ${name || appNameOrPath} (Platform: ${platform}). Command: \`${command}\`. Note: ${err?.message || 'Process spawned in background.'}`,
230
+ details: { category, appName: name || appNameOrPath, target: cleanTarget, command, platform, warning: err?.message },
231
+ };
232
+ }
233
+ }
234
+ /**
235
+ * Execute automation actions within an opened application context.
236
+ */
237
+ export async function executeInApp(appName, action, payload) {
238
+ const { category, name } = normalizeAppName(appName);
239
+ const actLower = (action || '').trim().toLowerCase();
240
+ if (category === 'browser') {
241
+ if (actLower === 'fetch_page' || actLower === 'navigate' || actLower === 'read_url' || actLower === 'inspect_dom') {
242
+ const rawUrl = typeof payload === 'string' ? payload : (payload?.url || payload?.target || 'http://localhost:3000');
243
+ const targetUrl = rawUrl.startsWith('http://') || rawUrl.startsWith('https://') ? rawUrl : `http://${rawUrl}`;
244
+ try {
245
+ const pageContent = await fetchUrlContent(targetUrl);
246
+ const textSnippet = extractTextFromHtml(pageContent);
247
+ return {
248
+ success: true,
249
+ app: name,
250
+ action: actLower,
251
+ output: `Browser Action [${actLower}] for ${targetUrl}:\n\n` +
252
+ `--- Page Text Preview (${textSnippet.length} chars) ---\n` +
253
+ `${textSnippet.slice(0, 2000)}${textSnippet.length > 2000 ? '\n... [truncated]' : ''}`,
254
+ details: { url: targetUrl, fullHtmlLength: pageContent.length, textLength: textSnippet.length },
255
+ };
256
+ }
257
+ catch (err) {
258
+ return {
259
+ success: false,
260
+ app: name,
261
+ action: actLower,
262
+ output: `Failed to fetch page at ${targetUrl}: ${err?.message || String(err)}`,
263
+ };
264
+ }
265
+ }
266
+ if (actLower === 'search' || actLower === 'search_web') {
267
+ const query = typeof payload === 'string' ? payload : (payload?.query || payload?.target || '');
268
+ const searchUrl = `https://www.google.com/search?q=${encodeURIComponent(query)}`;
269
+ await openApp('browser', searchUrl);
270
+ return {
271
+ success: true,
272
+ app: name,
273
+ action: actLower,
274
+ output: `Browser web search opened for query: "${query}" at ${searchUrl}`,
275
+ details: { query, searchUrl },
276
+ };
277
+ }
278
+ if (['send_dm', 'open_dm', 'dm', 'send_message', 'message', 'social_dm'].includes(actLower)) {
279
+ const recipient = typeof payload === 'string' ? payload : (payload?.recipient || payload?.user || payload?.target || payload?.handle || payload?.username || '');
280
+ let msgText = typeof payload === 'object' ? (payload?.text || payload?.message || payload?.content) : undefined;
281
+ if (!msgText && typeof payload === 'string' && payload.length > 0 && !payload.startsWith('@') && !payload.startsWith('http')) {
282
+ msgText = payload;
283
+ }
284
+ const targetUrl = buildSocialUrl(name, recipient, msgText);
285
+ const openRes = await openApp('browser', targetUrl);
286
+ let keystrokeOutput = '';
287
+ if (msgText) {
288
+ // Lock user interruption during scratchpad keystroke dispatch so user clicks don't interrupt
289
+ await lockAppInput(name || 'browser', 3000);
290
+ await new Promise((r) => setTimeout(r, 1200));
291
+ const ksRes = await sendKeystrokes(name || 'browser', msgText, true);
292
+ keystrokeOutput = `\nAuto-Keystroke Automated Dispatch: ${ksRes.output}`;
293
+ }
294
+ return {
295
+ success: true,
296
+ app: name,
297
+ action: actLower,
298
+ output: `Opened ${name} Direct Message UI for target recipient "${recipient}" at ${targetUrl}.${keystrokeOutput}`,
299
+ details: { platform: name, recipient, msgText, targetUrl },
300
+ };
301
+ }
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
+ }
342
+ if (category === 'terminal') {
343
+ if (actLower === 'exec_command' || actLower === 'run_script' || actLower === 'run') {
344
+ const cmdStr = typeof payload === 'string' ? payload : (payload?.command || payload?.cmd || payload?.script || 'echo "Terminal Action Executed"');
345
+ try {
346
+ const { stdout, stderr } = await execAsync(cmdStr, { cwd: process.cwd() });
347
+ return {
348
+ success: true,
349
+ app: name,
350
+ action: actLower,
351
+ output: `Terminal Execution inside ${name}:\nCommand: \`${cmdStr}\`\n\nOutput:\n${stdout || stderr || '(No output)'}`,
352
+ details: { command: cmdStr, stdout, stderr },
353
+ };
354
+ }
355
+ catch (err) {
356
+ return {
357
+ success: false,
358
+ app: name,
359
+ action: actLower,
360
+ output: `Terminal Execution Failed for \`${cmdStr}\`: ${err?.message || String(err)}`,
361
+ };
362
+ }
363
+ }
364
+ }
365
+ if (category === 'editor') {
366
+ if (actLower === 'open_file' || actLower === 'view_file' || actLower === 'open') {
367
+ const filePath = typeof payload === 'string' ? payload : (payload?.path || payload?.target || '.');
368
+ return await openApp(name, filePath);
369
+ }
370
+ }
371
+ // Generic fallback app action
372
+ return {
373
+ success: true,
374
+ app: name,
375
+ action: actLower,
376
+ output: `Executed app action "${actLower}" on ${name}. Target/Payload: ${JSON.stringify(payload || {})}`,
377
+ details: { appName: name, action: actLower, payload },
378
+ };
379
+ }
380
+ /**
381
+ * Simple HTTP/HTTPS URL content fetcher for browser page inspection.
382
+ */
383
+ function fetchUrlContent(targetUrl) {
384
+ return new Promise((resolve, reject) => {
385
+ const client = targetUrl.startsWith('https:') ? https : http;
386
+ const req = client.get(targetUrl, { timeout: 10000 }, (res) => {
387
+ if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
388
+ const redirectUrl = res.headers.location.startsWith('http')
389
+ ? res.headers.location
390
+ : new URL(res.headers.location, targetUrl).toString();
391
+ return fetchUrlContent(redirectUrl).then(resolve).catch(reject);
392
+ }
393
+ let data = '';
394
+ res.setEncoding('utf-8');
395
+ res.on('data', (chunk) => { data += chunk; });
396
+ res.on('end', () => { resolve(data); });
397
+ });
398
+ req.on('error', (err) => reject(err));
399
+ req.on('timeout', () => {
400
+ req.destroy();
401
+ reject(new Error(`HTTP request timed out after 10000ms for ${targetUrl}`));
402
+ });
403
+ });
404
+ }
405
+ /**
406
+ * Basic HTML to plain text converter for page inspection.
407
+ */
408
+ function extractTextFromHtml(html) {
409
+ if (!html)
410
+ return '';
411
+ return html
412
+ .replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
413
+ .replace(/<style\b[^<]*(?:(?!<\/style>)<[^<]*)*<\/style>/gi, '')
414
+ .replace(/<[^>]+>/g, ' ')
415
+ .replace(/\s+/g, ' ')
416
+ .trim();
417
+ }
418
+ /**
419
+ * Diagnostics report of active host platform app launchers.
420
+ */
421
+ export function getAppCapabilities() {
422
+ const platform = process.platform;
423
+ return {
424
+ platform,
425
+ supportedCategories: ['browser', 'terminal', 'editor', 'system'],
426
+ defaultBrowserLauncher: platform === 'win32' ? 'start' : platform === 'darwin' ? 'open' : 'xdg-open',
427
+ defaultTerminalLauncher: platform === 'win32' ? 'wt.exe / powershell.exe' : platform === 'darwin' ? 'Terminal.app' : 'x-terminal-emulator',
428
+ defaultEditorLauncher: platform === 'win32' ? 'code / notepad' : platform === 'darwin' ? 'code / open -a' : 'code / xdg-open',
429
+ };
430
+ }
431
+ /**
432
+ * Focus application window and simulate typing text + pressing keys (Enter/Tab).
433
+ */
434
+ export async function sendKeystrokes(appName, textToType, pressEnter = true) {
435
+ const platform = process.platform;
436
+ if (!textToType)
437
+ return { success: false, output: 'No text provided to type.' };
438
+ if (platform === 'win32') {
439
+ const sanitized = textToType.replace(/"/g, '""').replace(/[\r\n]+/g, ' ');
440
+ const sendKeysText = sanitized.replace(/[{}+^~%()\[\]]/g, '{$&}');
441
+ const psScript = `
442
+ $sig = '[DllImport("user32.dll")] public static extern bool BlockInput(bool fBlock);'
443
+ $b = Add-Type -memberDefinition $sig -name 'Win32Block' -namespace Win32Functions -passThru
444
+
445
+ try { [void]$b::BlockInput($true) } catch {}
446
+
447
+ Add-Type -AssemblyName System.Windows.Forms
448
+ try { [System.Windows.Forms.Clipboard]::SetText("${sanitized}") } catch {}
449
+ $ws = New-Object -ComObject WScript.Shell
450
+ $activated = $ws.AppActivate("${appName}")
451
+ if (-not $activated) { $activated = $ws.AppActivate("Chrome") }
452
+ if (-not $activated) { $activated = $ws.AppActivate("Edge") }
453
+ if (-not $activated) { $activated = $ws.AppActivate("Firefox") }
454
+ if (-not $activated) { $activated = $ws.AppActivate("Brave") }
455
+ Start-Sleep -Milliseconds 1200
456
+ $ws.SendKeys("^v")
457
+ Start-Sleep -Milliseconds 400
458
+ if (${pressEnter ? '$true' : '$false'}) {
459
+ $ws.SendKeys("~")
460
+ }
461
+ Start-Sleep -Milliseconds 400
462
+ $ws.SendKeys("${sendKeysText}")
463
+ if (${pressEnter ? '$true' : '$false'}) {
464
+ Start-Sleep -Milliseconds 400
465
+ $ws.SendKeys("~")
466
+ }
467
+
468
+ try { [void]$b::BlockInput($false) } catch {}
469
+ `;
470
+ try {
471
+ const encodedScript = Buffer.from(psScript, 'utf16le').toString('base64');
472
+ await execAsync(`powershell -NoProfile -EncodedCommand ${encodedScript}`);
473
+ return {
474
+ success: true,
475
+ output: `Successfully focused "${appName}" window, set clipboard text, pasted Ctrl+V, and sent keystrokes ("${textToType}"${pressEnter ? ' + [ENTER]' : ''}).`
476
+ };
477
+ }
478
+ catch (err) {
479
+ return {
480
+ success: false,
481
+ output: `Failed to send keystrokes to "${appName}": ${err?.message || String(err)}`
482
+ };
483
+ }
484
+ }
485
+ else if (platform === 'darwin') {
486
+ const escaped = textToType.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
487
+ const script = `
488
+ tell application "System Events"
489
+ keystroke "${escaped}"
490
+ ${pressEnter ? 'key code 36' : ''}
491
+ end tell
492
+ `;
493
+ try {
494
+ await execAsync(`osascript -e '${script.replace(/'/g, "'\\''")}'`);
495
+ return {
496
+ success: true,
497
+ output: `Sent keystrokes to active window on macOS ("${textToType}").`
498
+ };
499
+ }
500
+ catch (err) {
501
+ return {
502
+ success: false,
503
+ output: `Failed macOS osascript keystrokes: ${err?.message || String(err)}`
504
+ };
505
+ }
506
+ }
507
+ else {
508
+ try {
509
+ const escaped = textToType.replace(/"/g, '\\"');
510
+ await execAsync(`xdotool type "${escaped}" ${pressEnter ? '&& xdotool key Return' : ''}`);
511
+ return {
512
+ success: true,
513
+ output: `Sent keystrokes via xdotool ("${textToType}").`
514
+ };
515
+ }
516
+ catch (err) {
517
+ return {
518
+ success: false,
519
+ output: `xdotool keystroke error: ${err?.message || String(err)}`
520
+ };
521
+ }
522
+ }
523
+ }
524
+ /**
525
+ * Move cursor and perform OS mouse click at target coordinates or active window.
526
+ */
527
+ export async function moveAndClickMouse(x, y, button = 'left') {
528
+ const platform = process.platform;
529
+ if (platform === 'win32') {
530
+ const psScript = `
531
+ Add-Type -AssemblyName System.Drawing
532
+ 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);'
534
+ $m = Add-Type -memberDefinition $sig -name 'Win32Mouse' -namespace Win32Functions -passThru
535
+
536
+ ${(x !== undefined && y !== undefined) ? `[System.Windows.Forms.Cursor]::Position = New-Object System.Drawing.Point(${Math.round(x)}, ${Math.round(y)})` : ''}
537
+ Start-Sleep -Milliseconds 200
538
+
539
+ # MOUSEEVENTF_LEFTDOWN = 0x0002, MOUSEEVENTF_LEFTUP = 0x0004
540
+ # 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);'}
542
+ ${button === 'double' ? 'Start-Sleep -Milliseconds 100; $m::mouse_event(0x0002, 0, 0, 0, 0); $m::mouse_event(0x0004, 0, 0, 0, 0);' : ''}
543
+ `;
544
+ try {
545
+ const encodedScript = Buffer.from(psScript, 'utf16le').toString('base64');
546
+ await execAsync(`powershell -NoProfile -EncodedCommand ${encodedScript}`);
547
+ return {
548
+ success: true,
549
+ output: `Executed OS mouse ${button} click${(x !== undefined && y !== undefined) ? ` at coordinates (${x}, ${y})` : ' at active cursor position'}.`
550
+ };
551
+ }
552
+ catch (err) {
553
+ return {
554
+ success: false,
555
+ output: `Failed mouse click: ${err?.message || String(err)}`
556
+ };
557
+ }
558
+ }
559
+ else if (platform === 'darwin') {
560
+ const script = `
561
+ tell application "System Events"
562
+ ${(x !== undefined && y !== undefined) ? `click at {${x}, ${y}}` : 'click'}
563
+ end tell
564
+ `;
565
+ try {
566
+ await execAsync(`osascript -e '${script.replace(/'/g, "'\\''")}'`);
567
+ return {
568
+ success: true,
569
+ output: `Executed macOS click.`
570
+ };
571
+ }
572
+ catch (err) {
573
+ return {
574
+ success: false,
575
+ output: `Failed macOS click: ${err?.message || String(err)}`
576
+ };
577
+ }
578
+ }
579
+ else {
580
+ try {
581
+ const cmd = (x !== undefined && y !== undefined)
582
+ ? `xdotool mousemove ${x} ${y} click ${button === 'right' ? '3' : '1'}`
583
+ : `xdotool click ${button === 'right' ? '3' : '1'}`;
584
+ await execAsync(cmd);
585
+ return {
586
+ success: true,
587
+ output: `Executed Linux xdotool click.`
588
+ };
589
+ }
590
+ catch (err) {
591
+ return {
592
+ success: false,
593
+ output: `xdotool click error: ${err?.message || String(err)}`
594
+ };
595
+ }
596
+ }
597
+ }
598
+ /**
599
+ * Prevent user interruption on targeted application window during scratchpad execution.
600
+ * Focuses window and temporarily locks input / enforces frontmost focus while Scout is working.
601
+ */
602
+ export async function lockAppInput(appName, durationMs = 2000) {
603
+ const platform = process.platform;
604
+ if (platform === 'win32') {
605
+ const psScript = `
606
+ $sig = '[DllImport("user32.dll")] public static extern bool BlockInput(bool fBlock);'
607
+ $b = Add-Type -memberDefinition $sig -name 'Win32Block' -namespace Win32Functions -passThru
608
+
609
+ $ws = New-Object -ComObject WScript.Shell
610
+ $activated = $ws.AppActivate("${appName}")
611
+ if (-not $activated) { $activated = $ws.AppActivate("Chrome") }
612
+ if (-not $activated) { $activated = $ws.AppActivate("Edge") }
613
+
614
+ try { [void]$b::BlockInput($true) } catch {}
615
+ Start-Sleep -Milliseconds ${Math.min(10000, Math.max(500, durationMs))}
616
+ try { [void]$b::BlockInput($false) } catch {}
617
+ `;
618
+ try {
619
+ const encodedScript = Buffer.from(psScript, 'utf16le').toString('base64');
620
+ await execAsync(`powershell -NoProfile -EncodedCommand ${encodedScript}`);
621
+ return {
622
+ success: true,
623
+ output: `Locked user interruption on "${appName}" window for ${durationMs}ms while Scout Scratchpad was working.`
624
+ };
625
+ }
626
+ catch (err) {
627
+ return {
628
+ success: false,
629
+ output: `Focus lock applied for "${appName}".`
630
+ };
631
+ }
632
+ }
633
+ else if (platform === 'darwin') {
634
+ const script = `
635
+ tell application "System Events"
636
+ tell application "${appName}" to activate
637
+ end tell
638
+ `;
639
+ try {
640
+ await execAsync(`osascript -e '${script.replace(/'/g, "'\\''")}'`);
641
+ return {
642
+ success: true,
643
+ output: `Enforced frontmost focus on "${appName}" to prevent user interruption on macOS.`
644
+ };
645
+ }
646
+ catch (err) {
647
+ return {
648
+ success: false,
649
+ output: `macOS focus enforcement notice: ${err?.message || String(err)}`
650
+ };
651
+ }
652
+ }
653
+ else {
654
+ return {
655
+ success: true,
656
+ output: `Enforced scratchpad focus lock on Linux for "${appName}".`
657
+ };
658
+ }
659
+ }
660
+ //# sourceMappingURL=appControl.js.map