ft-scout 8.0.3 → 8.0.5

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.
Files changed (48) hide show
  1. package/.firebase/hosting.d2Vi.cache +6 -6
  2. package/README.md +29 -13
  3. package/bin/src/commands/agent.d.ts.map +1 -1
  4. package/bin/src/commands/agent.js +48 -18
  5. package/bin/src/commands/agent.js.map +1 -1
  6. package/bin/src/commands/audit.d.ts.map +1 -1
  7. package/bin/src/commands/audit.js +8 -2
  8. package/bin/src/commands/audit.js.map +1 -1
  9. package/bin/src/commands/dashboard.d.ts.map +1 -1
  10. package/bin/src/commands/dashboard.js +9 -7
  11. package/bin/src/commands/dashboard.js.map +1 -1
  12. package/bin/src/commands/plan.d.ts.map +1 -0
  13. package/bin/src/commands/plan.js +70 -0
  14. package/bin/src/commands/plan.js.map +1 -0
  15. package/bin/src/commands/quota.d.ts.map +1 -1
  16. package/bin/src/commands/quota.js +4 -20
  17. package/bin/src/commands/quota.js.map +1 -1
  18. package/bin/src/commands/risky.d.ts.map +1 -1
  19. package/bin/src/commands/risky.js +8 -2
  20. package/bin/src/commands/risky.js.map +1 -1
  21. package/bin/src/engine/agentEngine.d.ts.map +1 -1
  22. package/bin/src/engine/agentEngine.js +16 -4
  23. package/bin/src/engine/agentEngine.js.map +1 -1
  24. package/bin/src/engine/appControl.d.ts.map +1 -1
  25. package/bin/src/engine/appControl.js +287 -17
  26. package/bin/src/engine/appControl.js.map +1 -1
  27. package/bin/src/engine/liveScreenEngine.d.ts.map +1 -0
  28. package/bin/src/engine/liveScreenEngine.js +390 -0
  29. package/bin/src/engine/liveScreenEngine.js.map +1 -0
  30. package/bin/src/engine/voiceEngine.d.ts.map +1 -1
  31. package/bin/src/engine/voiceEngine.js +1 -0
  32. package/bin/src/engine/voiceEngine.js.map +1 -1
  33. package/bin/src/index.js +18 -7
  34. package/bin/src/index.js.map +1 -1
  35. package/bin/src/server/server.d.ts.map +1 -1
  36. package/bin/src/server/server.js +57 -0
  37. package/bin/src/server/server.js.map +1 -1
  38. package/bin/src/utils/auth.d.ts.map +1 -1
  39. package/bin/src/utils/auth.js +382 -7
  40. package/bin/src/utils/auth.js.map +1 -1
  41. package/bin/src/utils/branding.js +1 -1
  42. package/bin/src/utils/payment.d.ts.map +1 -0
  43. package/bin/src/utils/payment.js +251 -0
  44. package/bin/src/utils/payment.js.map +1 -0
  45. package/firebase.json +20 -6
  46. package/package.json +1 -1
  47. package/web/app.js +97 -16
  48. package/web/styles.css +96 -1
@@ -2,11 +2,12 @@ import fs from 'fs';
2
2
  import path from 'path';
3
3
  import http from 'http';
4
4
  import https from 'https';
5
- import { exec } from 'child_process';
5
+ import { exec, spawn, ChildProcess } from 'child_process';
6
6
  import { promisify } from 'util';
7
7
  import OpenAI from 'openai';
8
8
  import dotenv from 'dotenv';
9
9
  import chalk from 'chalk';
10
+ import { startLiveScreenShare, stopLiveScreenShare, isLiveScreenShareActive, getLiveScreenFrame, analyzeLiveScreen, callVisionAiWithFrame, setLiveScreenTargetApp, getLiveScreenTargetApp, } from './liveScreenEngine.js';
10
11
  dotenv.config({ quiet: true });
11
12
  const execAsync = promisify(exec);
12
13
  /**
@@ -183,6 +184,9 @@ export async function findInstalledApp(query) {
183
184
  */
184
185
  export async function openApp(appNameOrPath, target, options) {
185
186
  const { category, name } = normalizeAppName(appNameOrPath);
187
+ if (name && name !== 'desktop' && name !== 'system') {
188
+ setLiveScreenTargetApp(name);
189
+ }
186
190
  const platform = process.platform;
187
191
  let command = '';
188
192
  const cleanTarget = target ? target.trim() : '';
@@ -352,6 +356,10 @@ export async function openApp(appNameOrPath, target, options) {
352
356
  export async function executeInApp(appName, action, payload) {
353
357
  const { category, name } = normalizeAppName(appName);
354
358
  const actLower = (action || '').trim().toLowerCase();
359
+ // Focus live screen sharing on the active app being operated
360
+ if (name && name !== 'desktop' && name !== 'system') {
361
+ setLiveScreenTargetApp(name);
362
+ }
355
363
  // Universal Scratchpad Desktop Automation Actions (Takeover, Screen Seeing, Keystrokes, Mouse Clicks, Focus Locks)
356
364
  if (['takeover', 'takeover_control', 'takeover_screen', 'takeover_app'].includes(actLower)) {
357
365
  const parsed = parsePayload(payload);
@@ -376,7 +384,38 @@ export async function executeInApp(appName, action, payload) {
376
384
  output: res.output,
377
385
  };
378
386
  }
387
+ if (['start_screen_share', 'live_screen_share', 'screen_share', 'start_live_stream'].includes(actLower)) {
388
+ const targetApp = (name && name !== 'desktop' && name !== 'system') ? name : undefined;
389
+ const res = await startLiveScreenShare({ targetApp });
390
+ return {
391
+ success: res.success,
392
+ app: name || appName,
393
+ action: actLower,
394
+ output: res.message,
395
+ details: { active: true, targetApp },
396
+ };
397
+ }
398
+ if (['stop_screen_share', 'end_screen_share', 'stop_live_stream'].includes(actLower)) {
399
+ const res = stopLiveScreenShare();
400
+ return {
401
+ success: res.success,
402
+ app: name || appName,
403
+ action: actLower,
404
+ output: res.summary,
405
+ details: { active: false },
406
+ };
407
+ }
379
408
  if (['capture_screen', 'screenshot', 'screen_shot'].includes(actLower)) {
409
+ if (isLiveScreenShareActive()) {
410
+ const liveRes = await analyzeLiveScreen();
411
+ return {
412
+ success: liveRes.success,
413
+ app: name || appName,
414
+ action: actLower,
415
+ output: liveRes.output,
416
+ details: liveRes.details,
417
+ };
418
+ }
380
419
  const res = await captureScreen(name || appName);
381
420
  return {
382
421
  success: res.success,
@@ -386,9 +425,9 @@ export async function executeInApp(appName, action, payload) {
386
425
  details: res.details,
387
426
  };
388
427
  }
389
- if (['see_screen', 'inspect_screen', 'view_screen', 'analyze_screen', 'inspect_desktop', 'visual_analysis', 'see_and_analyze'].includes(actLower)) {
428
+ if (['see_screen', 'inspect_screen', 'view_screen', 'analyze_screen', 'inspect_desktop', 'visual_analysis', 'see_and_analyze', 'live_screen_stream', 'screen_stream'].includes(actLower)) {
390
429
  const query = typeof payload === 'string' ? payload : (payload?.query || payload?.target || payload?.text || undefined);
391
- const res = await analyzeScreen(name || appName, query);
430
+ const res = await analyzeLiveScreen(query);
392
431
  return {
393
432
  success: res.success,
394
433
  app: name || appName,
@@ -1030,12 +1069,208 @@ export async function getScreenInfo(appName) {
1030
1069
  };
1031
1070
  }
1032
1071
  }
1072
+ let persistentAuraProcess = null;
1073
+ let persistentAuraFlagPath = null;
1074
+ /**
1075
+ * Start persistent futuristic sky-blue aura overlay on screen borders and corners.
1076
+ * Stays continuously active and animating as long as live screen sharing is running.
1077
+ */
1078
+ export async function startScreenAuraOverlay(message = "Live Screen Share Active • Streaming to Model") {
1079
+ const platform = process.platform;
1080
+ if (platform !== 'win32')
1081
+ return;
1082
+ // Stop any existing overlay first
1083
+ stopScreenAuraOverlay();
1084
+ try {
1085
+ const scriptsDir = path.join(process.cwd(), '.ft', 'scripts');
1086
+ if (!fs.existsSync(scriptsDir)) {
1087
+ fs.mkdirSync(scriptsDir, { recursive: true });
1088
+ }
1089
+ const psFile = path.join(scriptsDir, 'scout_aura_persistent.ps1');
1090
+ const flagFile = path.join(scriptsDir, 'aura_active.flag');
1091
+ fs.writeFileSync(flagFile, String(Date.now()), 'utf8');
1092
+ persistentAuraFlagPath = flagFile;
1093
+ const subtitleText = " Perception Active";
1094
+ const parentPid = process.pid;
1095
+ const overlayPs = `
1096
+ Add-Type -AssemblyName PresentationFramework, PresentationCore, WindowsBase
1097
+
1098
+ [xml]$xaml = @"
1099
+ <Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
1100
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
1101
+ Title="Scout Aura Overlay"
1102
+ WindowStyle="None"
1103
+ AllowsTransparency="True"
1104
+ Background="Transparent"
1105
+ Topmost="True"
1106
+ ShowInTaskbar="False"
1107
+ ShowActivated="False"
1108
+ IsHitTestVisible="False">
1109
+ <Window.Resources>
1110
+ <Storyboard x:Key="AuraPulse" RepeatBehavior="Forever" AutoReverse="True">
1111
+ <DoubleAnimation Storyboard.TargetName="AuraBorder" Storyboard.TargetProperty="Opacity"
1112
+ From="0.4" To="1.0" Duration="0:0:0.6"/>
1113
+ <DoubleAnimation Storyboard.TargetName="CornerTL" Storyboard.TargetProperty="Opacity"
1114
+ From="0.5" To="1.0" Duration="0:0:0.6"/>
1115
+ <DoubleAnimation Storyboard.TargetName="CornerTR" Storyboard.TargetProperty="Opacity"
1116
+ From="0.5" To="1.0" Duration="0:0:0.6"/>
1117
+ <DoubleAnimation Storyboard.TargetName="CornerBL" Storyboard.TargetProperty="Opacity"
1118
+ From="0.5" To="1.0" Duration="0:0:0.6"/>
1119
+ <DoubleAnimation Storyboard.TargetName="CornerBR" Storyboard.TargetProperty="Opacity"
1120
+ From="0.5" To="1.0" Duration="0:0:0.6"/>
1121
+ <DoubleAnimation Storyboard.TargetName="AuraGlow" Storyboard.TargetProperty="Opacity"
1122
+ From="0.25" To="0.9" Duration="0:0:0.6"/>
1123
+ </Storyboard>
1124
+ </Window.Resources>
1125
+
1126
+ <Grid IsHitTestVisible="False">
1127
+ <!-- Outer Glowing Sky Blue Aura Border -->
1128
+ <Border x:Name="AuraGlow" BorderThickness="6" BorderBrush="#00d8ff" Margin="0">
1129
+ <Border.Effect>
1130
+ <DropShadowEffect Color="#00e5ff" BlurRadius="45" ShadowDepth="0" Opacity="0.95"/>
1131
+ </Border.Effect>
1132
+ </Border>
1133
+ <Border x:Name="AuraBorder" BorderThickness="3" BorderBrush="#38bdf8" Margin="0">
1134
+ <Border.Effect>
1135
+ <DropShadowEffect Color="#00e5ff" BlurRadius="25" ShadowDepth="0" Opacity="1"/>
1136
+ </Border.Effect>
1137
+ </Border>
1138
+
1139
+ <!-- Sky Blue Corner Brackets with Aura Glow -->
1140
+ <!-- Top Left Corner -->
1141
+ <Canvas HorizontalAlignment="Left" VerticalAlignment="Top" Width="120" Height="120" Margin="6,6,0,0">
1142
+ <Path x:Name="CornerTL" Data="M 0,90 L 0,0 L 90,0" Stroke="#00e5ff" StrokeThickness="6">
1143
+ <Path.Effect>
1144
+ <DropShadowEffect Color="#00e5ff" BlurRadius="30" ShadowDepth="0" Opacity="1"/>
1145
+ </Path.Effect>
1146
+ </Path>
1147
+ </Canvas>
1148
+
1149
+ <!-- Top Right Corner -->
1150
+ <Canvas HorizontalAlignment="Right" VerticalAlignment="Top" Width="120" Height="120" Margin="0,6,6,0">
1151
+ <Path x:Name="CornerTR" Data="M 30,0 L 120,0 L 120,90" Stroke="#00e5ff" StrokeThickness="6">
1152
+ <Path.Effect>
1153
+ <DropShadowEffect Color="#00e5ff" BlurRadius="30" ShadowDepth="0" Opacity="1"/>
1154
+ </Path.Effect>
1155
+ </Path>
1156
+ </Canvas>
1157
+
1158
+ <!-- Bottom Left Corner -->
1159
+ <Canvas HorizontalAlignment="Left" VerticalAlignment="Bottom" Width="120" Height="120" Margin="6,0,0,6">
1160
+ <Path x:Name="CornerBL" Data="M 0,30 L 0,120 L 90,120" Stroke="#00e5ff" StrokeThickness="6">
1161
+ <Path.Effect>
1162
+ <DropShadowEffect Color="#00e5ff" BlurRadius="30" ShadowDepth="0" Opacity="1"/>
1163
+ </Path.Effect>
1164
+ </Path>
1165
+ </Canvas>
1166
+
1167
+ <!-- Bottom Right Corner -->
1168
+ <Canvas HorizontalAlignment="Right" VerticalAlignment="Bottom" Width="120" Height="120" Margin="0,0,6,6">
1169
+ <Path x:Name="CornerBR" Data="M 30,120 L 120,120 L 120,30" Stroke="#00e5ff" StrokeThickness="6">
1170
+ <Path.Effect>
1171
+ <DropShadowEffect Color="#00e5ff" BlurRadius="30" ShadowDepth="0" Opacity="1"/>
1172
+ </Path.Effect>
1173
+ </Path>
1174
+ </Canvas>
1175
+
1176
+ <!-- Top Floating Status Badge -->
1177
+ <Border HorizontalAlignment="Center" VerticalAlignment="Top" Margin="0,22,0,0"
1178
+ Background="#EE03121F" BorderBrush="#00e5ff" BorderThickness="1.8" CornerRadius="24" Padding="26,10">
1179
+ <Border.Effect>
1180
+ <DropShadowEffect Color="#00e5ff" BlurRadius="25" ShadowDepth="0" Opacity="0.95"/>
1181
+ </Border.Effect>
1182
+ <StackPanel Orientation="Horizontal" VerticalAlignment="Center">
1183
+ <Ellipse Width="12" Height="12" Fill="#00e5ff" Margin="0,0,12,0">
1184
+ <Ellipse.Effect>
1185
+ <DropShadowEffect Color="#00e5ff" BlurRadius="12" ShadowDepth="0"/>
1186
+ </Ellipse.Effect>
1187
+ </Ellipse>
1188
+ <TextBlock Text="${message}" Foreground="#ffffff" FontSize="16" FontWeight="Bold" VerticalAlignment="Center"/>
1189
+ <TextBlock Text="${subtitleText}" Foreground="#38bdf8" FontSize="13.5" FontWeight="SemiBold" Margin="10,0,0,0" VerticalAlignment="Center"/>
1190
+ </StackPanel>
1191
+ </Border>
1192
+ </Grid>
1193
+ </Window>
1194
+ "@
1195
+
1196
+ $reader = (New-Object System.Xml.XmlNodeReader $xaml)
1197
+ $window = [System.Windows.Markup.XamlReader]::Load($reader)
1198
+
1199
+ $window.Left = [System.Windows.SystemParameters]::VirtualScreenLeft
1200
+ $window.Top = [System.Windows.SystemParameters]::VirtualScreenTop
1201
+ $window.Width = [System.Windows.SystemParameters]::VirtualScreenWidth
1202
+ $window.Height = [System.Windows.SystemParameters]::VirtualScreenHeight
1203
+ $window.Topmost = $true
1204
+ $window.ShowActivated = $false
1205
+
1206
+ $storyboard = $window.Resources["AuraPulse"]
1207
+ $window.Add_Loaded({
1208
+ $storyboard.Begin($window)
1209
+ })
1210
+
1211
+ $flagPath = "${flagFile.replace(/\\/g, '\\\\')}"
1212
+ $parentPid = ${parentPid}
1213
+
1214
+ # Check if screen sharing is active or parent exited every 350ms
1215
+ $timer = New-Object System.Windows.Threading.DispatcherTimer
1216
+ $timer.Interval = [TimeSpan]::FromMilliseconds(350)
1217
+ $timer.Add_Tick({
1218
+ $shouldClose = $false
1219
+ if (-not (Test-Path $flagPath)) {
1220
+ $shouldClose = $true
1221
+ } else {
1222
+ $proc = Get-Process -Id $parentPid -ErrorAction SilentlyContinue
1223
+ if (-not $proc) {
1224
+ $shouldClose = $true
1225
+ }
1226
+ }
1227
+ if ($shouldClose) {
1228
+ $timer.Stop()
1229
+ $window.Close()
1230
+ }
1231
+ })
1232
+ $timer.Start()
1233
+
1234
+ [void]$window.ShowDialog()
1235
+ `;
1236
+ fs.writeFileSync(psFile, overlayPs, 'utf8');
1237
+ const child = spawn('powershell', ['-WindowStyle', 'Hidden', '-NoProfile', '-Sta', '-ExecutionPolicy', 'Bypass', '-File', psFile], {
1238
+ detached: false,
1239
+ stdio: 'ignore',
1240
+ });
1241
+ child.unref();
1242
+ persistentAuraProcess = child;
1243
+ }
1244
+ catch { }
1245
+ }
1246
+ /**
1247
+ * Stop persistent screen aura animation when live screen sharing stops.
1248
+ */
1249
+ export function stopScreenAuraOverlay() {
1250
+ if (persistentAuraFlagPath && fs.existsSync(persistentAuraFlagPath)) {
1251
+ try {
1252
+ fs.unlinkSync(persistentAuraFlagPath);
1253
+ }
1254
+ catch { }
1255
+ persistentAuraFlagPath = null;
1256
+ }
1257
+ if (persistentAuraProcess) {
1258
+ try {
1259
+ persistentAuraProcess.kill();
1260
+ }
1261
+ catch { }
1262
+ persistentAuraProcess = null;
1263
+ }
1264
+ }
1033
1265
  /**
1034
1266
  * Display a futuristic sky-blue aura animation on screen borders and corners,
1035
- * show the warning "Scout is on the screen.", and block external inputs
1036
- * during live screen capture and automation.
1267
+ * show the warning "Scout is on the screen.", and optionally block external inputs.
1268
+ * If durationMs <= 0, aura stays on continuously until stopScreenAuraOverlay() is called.
1037
1269
  */
1038
1270
  export async function showScreenAuraOverlay(durationMs = 2200, message = "Scout is viewing the screen.", blockInput = false) {
1271
+ if (durationMs <= 0) {
1272
+ return await startScreenAuraOverlay(message);
1273
+ }
1039
1274
  const platform = process.platform;
1040
1275
  if (platform !== 'win32')
1041
1276
  return;
@@ -1053,7 +1288,7 @@ try { [void]$b::BlockInput($true) } catch {}
1053
1288
  const unblockSnippet = blockInput ? `
1054
1289
  try { [void]$b::BlockInput($false) } catch {}
1055
1290
  ` : '';
1056
- const subtitleText = blockInput ? " • External Input Locked" : " • Perception Active";
1291
+ const subtitleText = blockInput ? " • External Input Locked" : " Perception Active";
1057
1292
  const overlayPs = `
1058
1293
  Add-Type -AssemblyName PresentationFramework, PresentationCore, WindowsBase
1059
1294
  ${blockInputSnippet}
@@ -1457,12 +1692,18 @@ export async function analyzeScreen(appNameOrScreen, targetQuery) {
1457
1692
  */
1458
1693
  export async function takeoverControl(appName, durationMs = 3000, x, y) {
1459
1694
  const { category, name } = normalizeAppName(appName);
1695
+ const targetApp = (name && name !== 'desktop' && name !== 'system') ? name : undefined;
1696
+ if (targetApp) {
1697
+ setLiveScreenTargetApp(targetApp);
1698
+ }
1460
1699
  if (name && name !== 'desktop' && name !== 'system' && category !== 'browser') {
1461
1700
  await openApp(name);
1462
1701
  }
1463
1702
  // Trigger visual sky-blue aura overlay on screen borders & corners with warning badge & input block
1464
1703
  try {
1465
- await showScreenAuraOverlay(Math.min(5000, Math.max(800, durationMs)), "Scout is on the screen.", true);
1704
+ const auraLabel = targetApp ? `Scout is controlling ${targetApp}...` : "Scout is on the screen.";
1705
+ await showScreenAuraOverlay(Math.min(5000, Math.max(800, durationMs)), auraLabel, true);
1706
+ startLiveScreenShare({ targetApp }).catch(() => { });
1466
1707
  }
1467
1708
  catch { }
1468
1709
  const mouseRes = await moveAndClickMouse(x, y, 'left', name || appName);
@@ -1946,23 +2187,22 @@ $m::mouse_event(0x0800, 0, 0, ${delta}, 0)
1946
2187
  * Visually ground and click a target UI element by name or visual description.
1947
2188
  */
1948
2189
  export async function findAndClickElement(elementQuery, appName, button = 'left') {
1949
- const capture = await captureScreen(appName);
1950
- if (!capture.success || !capture.details?.outPath) {
1951
- return { success: false, output: `Could not capture screen to locate element: ${capture.output}` };
1952
- }
1953
- const screenWidth = capture.details.width || 1920;
1954
- const screenHeight = capture.details.height || 1080;
1955
- const outPath = capture.details.outPath;
2190
+ let screenWidth = 1920;
2191
+ let screenHeight = 1080;
1956
2192
  let targetX = undefined;
1957
2193
  let targetY = undefined;
1958
- if (fs.existsSync(outPath)) {
2194
+ // Use in-memory live screen frame first without writing screenshot files to disk
2195
+ const liveFrame = await getLiveScreenFrame();
2196
+ if (liveFrame) {
2197
+ screenWidth = liveFrame.width;
2198
+ screenHeight = liveFrame.height;
1959
2199
  try {
1960
- const prompt = `Look at this screenshot and locate the UI element: "${elementQuery}".
2200
+ const prompt = `Look at this screen and locate the UI element: "${elementQuery}".
1961
2201
  Return ONLY a valid JSON object with the center point of this element in 0-1000 normalized coordinates:
1962
2202
  {"point": [y, x]}
1963
2203
  Where y is between 0 and 1000 (top to bottom) and x is between 0 and 1000 (left to right).
1964
2204
  If not found, return {"point": null}. Output nothing else.`;
1965
- const reply = await callVisionAi(outPath, prompt, 80);
2205
+ const reply = await callVisionAiWithFrame(liveFrame, prompt, 80);
1966
2206
  if (reply) {
1967
2207
  const match = reply.match(/\{[\s\S]*"point"[\s\S]*\}/);
1968
2208
  if (match) {
@@ -1980,6 +2220,36 @@ If not found, return {"point": null}. Output nothing else.`;
1980
2220
  }
1981
2221
  catch { }
1982
2222
  }
2223
+ else {
2224
+ const capture = await captureScreen(appName);
2225
+ if (capture.success && capture.details?.outPath && fs.existsSync(capture.details.outPath)) {
2226
+ screenWidth = capture.details.width || 1920;
2227
+ screenHeight = capture.details.height || 1080;
2228
+ try {
2229
+ const prompt = `Look at this screenshot and locate the UI element: "${elementQuery}".
2230
+ Return ONLY a valid JSON object with the center point of this element in 0-1000 normalized coordinates:
2231
+ {"point": [y, x]}
2232
+ Where y is between 0 and 1000 (top to bottom) and x is between 0 and 1000 (left to right).
2233
+ If not found, return {"point": null}. Output nothing else.`;
2234
+ const reply = await callVisionAi(capture.details.outPath, prompt, 80);
2235
+ if (reply) {
2236
+ const match = reply.match(/\{[\s\S]*"point"[\s\S]*\}/);
2237
+ if (match) {
2238
+ const parsed = JSON.parse(match[0]);
2239
+ if (Array.isArray(parsed.point) && parsed.point.length === 2) {
2240
+ const normY = Number(parsed.point[0]);
2241
+ const normX = Number(parsed.point[1]);
2242
+ if (!isNaN(normX) && !isNaN(normY)) {
2243
+ targetX = Math.round((normX / 1000) * screenWidth);
2244
+ targetY = Math.round((normY / 1000) * screenHeight);
2245
+ }
2246
+ }
2247
+ }
2248
+ }
2249
+ }
2250
+ catch { }
2251
+ }
2252
+ }
1983
2253
  if ((targetX === undefined || targetY === undefined) && process.platform === 'win32') {
1984
2254
  const psScript = `
1985
2255
  Add-Type -AssemblyName UIAutomationClient