ucu-mcp 0.4.0 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/CHANGELOG.md +50 -4
  2. package/dist/bin/ucu-mcp.js +1 -1
  3. package/dist/src/index.d.ts +2 -2
  4. package/dist/src/index.js +2 -2
  5. package/dist/src/mcp/server.js +1 -1
  6. package/dist/src/mcp/tools/app-tools.d.ts +2 -0
  7. package/dist/src/mcp/tools/app-tools.js +220 -0
  8. package/dist/src/mcp/tools/element-tools.d.ts +23 -0
  9. package/dist/src/mcp/tools/element-tools.js +59 -0
  10. package/dist/src/mcp/tools/helpers.d.ts +82 -0
  11. package/dist/src/mcp/tools/helpers.js +243 -0
  12. package/dist/src/mcp/tools/index.d.ts +19 -0
  13. package/dist/src/mcp/tools/index.js +54 -0
  14. package/dist/src/mcp/tools/input-tools.d.ts +2 -0
  15. package/dist/src/mcp/tools/input-tools.js +66 -0
  16. package/dist/src/mcp/tools/keyboard-tools.d.ts +2 -0
  17. package/dist/src/mcp/tools/keyboard-tools.js +35 -0
  18. package/dist/src/mcp/tools/screen-tools.d.ts +2 -0
  19. package/dist/src/mcp/tools/screen-tools.js +69 -0
  20. package/dist/src/mcp/tools.d.ts +9 -0
  21. package/dist/src/mcp/tools.js +87 -23
  22. package/dist/src/platform/base.d.ts +3 -0
  23. package/dist/src/platform/jxa-helpers.d.ts +11 -0
  24. package/dist/src/platform/jxa-helpers.js +206 -0
  25. package/dist/src/platform/macos/ax-tree.d.ts +4 -0
  26. package/dist/src/platform/macos/ax-tree.js +462 -0
  27. package/dist/src/platform/macos/base.d.ts +57 -0
  28. package/dist/src/platform/macos/base.js +92 -0
  29. package/dist/src/platform/macos/clipboard.d.ts +3 -0
  30. package/dist/src/platform/macos/clipboard.js +20 -0
  31. package/dist/src/platform/macos/element.d.ts +4 -0
  32. package/dist/src/platform/macos/element.js +212 -0
  33. package/dist/src/platform/macos/focus.d.ts +3 -0
  34. package/dist/src/platform/macos/focus.js +33 -0
  35. package/dist/src/platform/macos/helpers.d.ts +35 -0
  36. package/dist/src/platform/macos/helpers.js +54 -0
  37. package/dist/src/platform/macos/index.d.ts +2 -0
  38. package/dist/src/platform/macos/index.js +1 -0
  39. package/dist/src/platform/macos/input.d.ts +9 -0
  40. package/dist/src/platform/macos/input.js +62 -0
  41. package/dist/src/platform/macos/screen.d.ts +7 -0
  42. package/dist/src/platform/macos/screen.js +197 -0
  43. package/dist/src/platform/macos/window.d.ts +6 -0
  44. package/dist/src/platform/macos/window.js +251 -0
  45. package/dist/src/platform/macos.js +71 -563
  46. package/dist/src/util/errors.d.ts +7 -2
  47. package/dist/src/util/errors.js +7 -3
  48. package/native/cgevent/cgevent-helper +0 -0
  49. package/native/ocr/ocr-helper +0 -0
  50. package/native/windowlist/windowlist-helper +0 -0
  51. package/package.json +1 -1
@@ -4,9 +4,11 @@ import { promisify } from "node:util";
4
4
  import { captureFullScreen, captureRegion } from "../utils/screenshot.js";
5
5
  import { click as inputClick, doubleClick as inputDoubleClick, move as inputMove, drag as inputDrag, scroll as inputScroll, typeText, pressShortcut } from "../utils/input.js";
6
6
  import { CaptureError, ElementNotFoundError, InputSynthesisError, PermissionError, PlatformError, TargetStaleError, UcuError, WindowNotFoundError } from "../util/errors.js";
7
+ import { logger } from "../util/logger.js";
7
8
  import { existsSync } from "node:fs";
8
9
  import { join, dirname } from "node:path";
9
10
  import { fileURLToPath } from "node:url";
11
+ import { jxaChildElements, jxaGetBounds, jxaIsVisible, jxaElementActionHelpers } from "./jxa-helpers.js";
10
12
  const __macosDirname = dirname(fileURLToPath(import.meta.url));
11
13
  const execFileAsync = promisify(execFile);
12
14
  function errorMessage(error) {
@@ -149,8 +151,9 @@ export class MacOSPlatform {
149
151
  return;
150
152
  try {
151
153
  const { appName } = this.savedFocus;
154
+ const appNameLiteral = JSON.stringify(appName);
152
155
  execFileSync("osascript", [
153
- "-e", `tell application "${appName}" to activate`,
156
+ "-e", `tell application ${appNameLiteral} to activate`,
154
157
  ], { timeout: 5000 });
155
158
  }
156
159
  catch {
@@ -195,8 +198,9 @@ export class MacOSPlatform {
195
198
  ], { encoding: "utf-8", timeout: 5000 }).trim();
196
199
  return JSON.parse(out);
197
200
  }
198
- catch {
199
- return { width: 1920, height: 1080, scaleFactor: 2 };
201
+ catch (error) {
202
+ logger.warn("getScreenSize failed, using fallback", { error: errorMessage(error) });
203
+ return { width: 1920, height: 1080, scaleFactor: 2, estimated: true };
200
204
  }
201
205
  }
202
206
  isScreenLocked() {
@@ -208,7 +212,8 @@ export class MacOSPlatform {
208
212
  return /"IOConsoleLocked"\s*=\s*Yes/.test(out);
209
213
  }
210
214
  catch {
211
- return false;
215
+ // Fail-closed: if we can't determine lock state, assume locked
216
+ return true;
212
217
  }
213
218
  }
214
219
  // ── Window Management ───────────────────────────────────────────────────
@@ -235,14 +240,19 @@ export class MacOSPlatform {
235
240
  }
236
241
  JSON.stringify(result);
237
242
  `;
238
- const out = execFileSync("osascript", [
239
- "-l", "JavaScript",
240
- "-e", jxaScript,
241
- ], { encoding: "utf-8", timeout: 10000 }).trim();
242
- return JSON.parse(out);
243
+ try {
244
+ const out = execFileSync("osascript", [
245
+ "-l", "JavaScript",
246
+ "-e", jxaScript,
247
+ ], { encoding: "utf-8", timeout: 10000 }).trim();
248
+ return JSON.parse(out);
249
+ }
250
+ catch (error) {
251
+ rethrowAccessibilityError(error, "list_apps");
252
+ }
243
253
  }
244
254
  async focusApp(app) {
245
- const escapedApp = app.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
255
+ const appLiteral = JSON.stringify(app);
246
256
  this.windowCache = undefined;
247
257
  // NOTE: We intentionally do NOT call AppleScript "activate" here.
248
258
  // focus_app sets the internal target context so subsequent operations
@@ -269,16 +279,14 @@ export class MacOSPlatform {
269
279
  // bare WindowNotFoundError("CC Switch") was indistinguishable from
270
280
  // "the app is not running", which led models to retry forever.
271
281
  this.activeTarget = undefined; // Clear stale target on focus failure
272
- const err = new WindowNotFoundError(app);
273
- err.hint =
274
- "list_windows returned no match for this app. If the app is running, " +
282
+ const err = new WindowNotFoundError(app, { hint: "list_windows returned no match for this app. If the app is running, " +
275
283
  "the most likely cause is that it is an Electron app whose AX tree is " +
276
284
  "not exposed to System Events (System Settings > Privacy & Security > " +
277
285
  "Accessibility must be granted to the Electron process itself, not just " +
278
286
  "to the host terminal). Pixel-level workaround: call screenshot to " +
279
287
  "capture the screen, then ocr to locate UI text and get its bounding " +
280
288
  "box coordinates, then click(x, y) at those screen coordinates. " +
281
- "Alternatively, modify the app's config file or database directly.";
289
+ "Alternatively, modify the app's config file or database directly." });
282
290
  throw err;
283
291
  }
284
292
  this.activeTarget = {
@@ -308,10 +316,10 @@ export class MacOSPlatform {
308
316
  ].some((name) => normalized.includes(name));
309
317
  if (!knownBrowser)
310
318
  return undefined;
311
- const escapedApp = appName.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
319
+ const appLiteral = JSON.stringify(appName);
312
320
  const jxaScript = `
313
321
  function run() {
314
- var appName = "${escapedApp}";
322
+ var appName = ${appLiteral};
315
323
  try {
316
324
  var app = Application(appName);
317
325
  var url = "";
@@ -468,11 +476,16 @@ export class MacOSPlatform {
468
476
  }
469
477
  JSON.stringify(result);
470
478
  `;
471
- const jxaOut = execFileSync("osascript", [
472
- "-l", "JavaScript",
473
- "-e", jxaScript
474
- ], { encoding: "utf-8", timeout: 15000 });
475
- return JSON.parse(jxaOut.trim());
479
+ try {
480
+ const jxaOut = execFileSync("osascript", [
481
+ "-l", "JavaScript",
482
+ "-e", jxaScript
483
+ ], { encoding: "utf-8", timeout: 15000 });
484
+ return JSON.parse(jxaOut.trim());
485
+ }
486
+ catch (error) {
487
+ rethrowAccessibilityError(error, "list_windows_jxa");
488
+ }
476
489
  }
477
490
  async getWindowState(windowId, depth, includeBounds = true) {
478
491
  if (!windowId || windowId === this.activeTarget?.windowId) {
@@ -484,18 +497,14 @@ export class MacOSPlatform {
484
497
  }
485
498
  const maxDepth = Math.min(depth || 3, 10);
486
499
  const maxElements = 50;
487
- const escapedWindowId = resolvedWindowId.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/`/g, '\\`').replace(/\$/g, '\\$');
500
+ const windowIdLiteral = JSON.stringify(resolvedWindowId);
488
501
  const targetWindow = (await this.listWindows(true)).find((w) => w.id === resolvedWindowId);
489
502
  const targetJson = JSON.stringify(targetWindow ?? null);
490
503
  try {
491
504
  const jxaScript = `
492
505
  ObjC.import('AppKit');
493
506
  var se = Application('System Events');
494
- function childElements(elem) {
495
- try { return elem.uiElements(); } catch(e1) {
496
- try { return elem.elements(); } catch(e2) { return []; }
497
- }
498
- }
507
+ ${jxaChildElements()}
499
508
  var result = {window: null, focusedElement: null, tree: null, error: null};
500
509
  var target = ${targetJson};
501
510
  var includeBounds = ${includeBounds ? "true" : "false"};
@@ -506,7 +515,7 @@ export class MacOSPlatform {
506
515
 
507
516
  function windowMatches(win, proc) {
508
517
  if (!target) {
509
- try { return String(win.id()) === String("${escapedWindowId}"); } catch(e) { return false; }
518
+ try { return String(win.id()) === String(${windowIdLiteral}); } catch(e) { return false; }
510
519
  }
511
520
  try {
512
521
  if (target.pid && proc.unixId && proc.unixId() !== target.pid) return false;
@@ -526,7 +535,7 @@ export class MacOSPlatform {
526
535
  closeEnough(size[1], b.height, 24);
527
536
  } catch(e) {}
528
537
 
529
- try { return String(win.id()) === String("${escapedWindowId}"); } catch(e) {}
538
+ try { return String(win.id()) === String(${windowIdLiteral}); } catch(e) {}
530
539
  return false;
531
540
  }
532
541
 
@@ -534,7 +543,7 @@ export class MacOSPlatform {
534
543
  var foundProc = null;
535
544
 
536
545
  // Fast path: resolve "ProcessName/winN" format directly
537
- var idParts = "${escapedWindowId}".split('/');
546
+ var idParts = ${windowIdLiteral}.split('/');
538
547
  if (idParts.length >= 2 && idParts[0]) {
539
548
  var procName = idParts[0];
540
549
  var winIdx = 0;
@@ -575,7 +584,7 @@ export class MacOSPlatform {
575
584
  var winPos = foundWin.position();
576
585
  var winSize = foundWin.size();
577
586
  result.window = {
578
- id: String("${escapedWindowId}"),
587
+ id: String(${windowIdLiteral}),
579
588
  title: foundWin.name() || '',
580
589
  processName: foundProc.name() || '',
581
590
  pid: foundProc.unixId ? foundProc.unixId() : 0,
@@ -822,7 +831,7 @@ export class MacOSPlatform {
822
831
  }
823
832
  }
824
833
  async ocrJxa(tmpPath, screenSize, scaleFactor, region, buf) {
825
- const escapedPath = tmpPath.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/`/g, "\\`").replace(/$/g, "\\$");
834
+ const pathLiteral = JSON.stringify(tmpPath);
826
835
  const jxaScript = `
827
836
  function run() {
828
837
  ObjC.import('Vision');
@@ -830,7 +839,7 @@ export class MacOSPlatform {
830
839
  ObjC.import('Foundation');
831
840
  var app = Application.currentApplication();
832
841
  app.includeStandardAdditions = true;
833
- var path = "${escapedPath}";
842
+ var path = ${pathLiteral};
834
843
  var url = $.NSURL.fileURLWithPath(path);
835
844
  var image = $.NSImage.alloc.initWithContentsOfURL(url);
836
845
  if (!image || !image.isValid) {
@@ -914,10 +923,10 @@ export class MacOSPlatform {
914
923
  const effectiveApp = app || this.activeTarget?.appName;
915
924
  const maxDepth = Math.min(depth || 5, 10);
916
925
  const maxResults = Math.min(Math.max(options.maxResults ?? 50, 1), 200);
917
- const escapedApp = (effectiveApp || "").replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/`/g, '\\`').replace(/\$/g, '\\$');
918
- const escapedText = text ? text.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/`/g, '\\`').replace(/\$/g, '\\$') : "";
919
- const escapedRole = role ? role.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/`/g, '\\`').replace(/\$/g, '\\$') : "";
920
- const escapedValue = value ? value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/`/g, '\\`').replace(/\$/g, '\\$') : "";
926
+ const appLiteral = JSON.stringify(effectiveApp || "");
927
+ const textLiteral = text ? JSON.stringify(text) : "null";
928
+ const roleLiteral = role ? JSON.stringify(role) : "null";
929
+ const valueLiteral = value ? JSON.stringify(value) : "null";
921
930
  // Pre-compile regex on TS side to validate syntax before passing to JXA
922
931
  if (text && textMode === "regex") {
923
932
  try {
@@ -941,11 +950,7 @@ export class MacOSPlatform {
941
950
  const startTime = Date.now();
942
951
  const jxaScript = `
943
952
  var se = Application('System Events');
944
- function childElements(elem) {
945
- try { return elem.uiElements(); } catch(e1) {
946
- try { return elem.elements(); } catch(e2) { return []; }
947
- }
948
- }
953
+ ${jxaChildElements()}
949
954
  var results = [];
950
955
  var scannedCount = 0;
951
956
  var matchedCount = 0;
@@ -953,22 +958,13 @@ export class MacOSPlatform {
953
958
  var maxResults = ${maxResults};
954
959
  var includeBounds = ${includeBounds ? "true" : "false"};
955
960
  var visibleOnly = ${visibleOnly ? "true" : "false"};
956
- var textMode = "${textMode}";
961
+ var textMode = ${JSON.stringify(textMode)};
957
962
 
958
- var textFilter = ${text ? `"${escapedText}"` : "null"};
959
- var roleFilter = ${role ? `"${escapedRole}"` : "null"};
960
- var valueFilter = ${value ? `"${escapedValue}"` : "null"};
963
+ var textFilter = ${textLiteral};
964
+ var roleFilter = ${roleLiteral};
965
+ var valueFilter = ${valueLiteral};
961
966
 
962
- function isVisible(elem) {
963
- try {
964
- var pos = elem.position();
965
- var sz = elem.size();
966
- if (!pos || !sz) return false;
967
- return sz[0] > 0 && sz[1] > 0 && pos[0] > -10000 && pos[1] > -10000;
968
- } catch(e) {
969
- return false;
970
- }
971
- }
967
+ ${jxaIsVisible()}
972
968
 
973
969
  // Shared filter helper. textMatches and valueMatches used to be near
974
970
  // copies of the same three-branch dispatch (contains / exact / regex);
@@ -1035,15 +1031,7 @@ export class MacOSPlatform {
1035
1031
  return true;
1036
1032
  }
1037
1033
 
1038
- function getBounds(elem) {
1039
- try {
1040
- var pos = elem.position();
1041
- var sz = elem.size();
1042
- return {x: pos[0] || 0, y: pos[1] || 0, width: sz[0] || 0, height: sz[1] || 0};
1043
- } catch(e) {
1044
- return {x: 0, y: 0, width: 0, height: 0};
1045
- }
1046
- }
1034
+ ${jxaGetBounds()}
1047
1035
 
1048
1036
  function traverse(elem, path, currentDepth) {
1049
1037
  if (resultCount[0] >= maxResults) return;
@@ -1094,11 +1082,11 @@ export class MacOSPlatform {
1094
1082
  }
1095
1083
 
1096
1084
  try {
1097
- if ("${escapedApp}") {
1098
- var proc = se.processes["${escapedApp}"]();
1085
+ if (${appLiteral}) {
1086
+ var proc = se.processes[${appLiteral}]();
1099
1087
  var wins = proc.windows();
1100
1088
  for (var w = 0; w < wins.length && resultCount[0] < maxResults; w++) {
1101
- traverse(wins[w], "${escapedApp}/win" + w, 0);
1089
+ traverse(wins[w], ${appLiteral} + "/win" + w, 0);
1102
1090
  }
1103
1091
  } else {
1104
1092
  var procs = se.processes();
@@ -1183,9 +1171,9 @@ export class MacOSPlatform {
1183
1171
  }
1184
1172
  async clickElement(elementId, app) {
1185
1173
  this.evictExpiredCacheEntries();
1186
- const escapedElementId = elementId.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/`/g, '\\`').replace(/\$/g, '\\$');
1174
+ const elementIdLiteral = JSON.stringify(elementId);
1187
1175
  const effectiveApp = app || this.activeTarget?.appName;
1188
- const escapedApp = (effectiveApp || "").replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/`/g, '\\`').replace(/\$/g, '\\$');
1176
+ const appLiteral = JSON.stringify(effectiveApp || "");
1189
1177
  const cached = this.elementCache.get(elementId);
1190
1178
  if (cached && this.isCacheEntryExpired(cached)) {
1191
1179
  this.elementCache.delete(elementId);
@@ -1194,173 +1182,12 @@ export class MacOSPlatform {
1194
1182
  const jxaScript = `
1195
1183
  var se = Application('System Events');
1196
1184
  var _result = null;
1197
- function childElements(elem) {
1198
- try { return elem.uiElements(); } catch(e1) {
1199
- try { return elem.elements(); } catch(e2) { return []; }
1200
- }
1201
- }
1202
- var elemPath = "${escapedElementId}";
1203
- var appName = "${escapedApp}";
1185
+ ${jxaElementActionHelpers()}
1186
+ var elemPath = ${elementIdLiteral};
1187
+ var appName = ${appLiteral};
1204
1188
  var cached = ${cachedJson};
1205
1189
 
1206
- function resolveElementByFullPath(path) {
1207
- var parts = path.split('/');
1208
- if (parts.length < 2) return null;
1209
-
1210
- var procName = parts[0];
1211
- var winPart = parts[1];
1212
- var winIdx = 0;
1213
- var match = winPart.match(/^win(\\\\d+)$/);
1214
- if (match) {
1215
- winIdx = parseInt(match[1]);
1216
- }
1217
-
1218
- try {
1219
- var proc = se.processes[procName]();
1220
- var wins = proc.windows();
1221
- if (winIdx >= wins.length) return null;
1222
- var current = wins[winIdx];
1223
-
1224
- for (var i = 2; i < parts.length; i++) {
1225
- var idx = parseInt(parts[i]);
1226
- if (isNaN(idx)) return null;
1227
- try {
1228
- var kids = childElements(current);
1229
- if (idx >= kids.length) return null;
1230
- current = kids[idx];
1231
- } catch(e) { return null; }
1232
- }
1233
- return current;
1234
- } catch(e) { return null; }
1235
- }
1236
-
1237
- function elemString(elem, getter) {
1238
- try {
1239
- var value = getter(elem);
1240
- return value === undefined || value === null ? '' : String(value);
1241
- } catch(e) {
1242
- return '';
1243
- }
1244
- }
1245
-
1246
- function getBounds(elem) {
1247
- try {
1248
- var pos = elem.position();
1249
- var sz = elem.size();
1250
- return {x: pos[0] || 0, y: pos[1] || 0, width: sz[0] || 0, height: sz[1] || 0};
1251
- } catch(e) {
1252
- return {x: 0, y: 0, width: 0, height: 0};
1253
- }
1254
- }
1255
-
1256
- function descriptorMatches(elem) {
1257
- if (!cached) return true;
1258
- var role = elemString(elem, function(e) { return e.role(); });
1259
- var name = elemString(elem, function(e) { return e.name(); });
1260
- var desc = elemString(elem, function(e) { return e.description(); });
1261
- var value = elemString(elem, function(e) { return e.value(); });
1262
- if (cached.role && role && role !== cached.role) return false;
1263
- if (cached.name && name && name !== cached.name) return false;
1264
- if (cached.value && value && value !== cached.value) return false;
1265
- if (cached.description && desc && desc !== cached.description) return false;
1266
- return true;
1267
- }
1268
-
1269
- function scoreEquivalent(elem) {
1270
- if (!cached) return -1;
1271
- var score = 0;
1272
- var role = elemString(elem, function(e) { return e.role(); });
1273
- var name = elemString(elem, function(e) { return e.name(); });
1274
- var desc = elemString(elem, function(e) { return e.description(); });
1275
- var value = elemString(elem, function(e) { return e.value(); });
1276
- var subrole = elemString(elem, function(e) { return e.subrole(); });
1277
- var identifier = elemString(elem, function(e) { return e.identifier(); });
1278
- if (cached.role && role === cached.role) score += 4;
1279
- if (cached.name && name === cached.name) score += 4;
1280
- if (cached.value && value === cached.value) score += 3;
1281
- if (cached.description && desc === cached.description) score += 2;
1282
- if (cached.subrole && subrole === cached.subrole) score += 2;
1283
- if (cached.identifier && identifier === cached.identifier) score += 3;
1284
- var b = getBounds(elem);
1285
- if (cached.bounds) {
1286
- var cx = b.x + b.width / 2;
1287
- var cy = b.y + b.height / 2;
1288
- var ocx = cached.bounds.x + cached.bounds.width / 2;
1289
- var ocy = cached.bounds.y + cached.bounds.height / 2;
1290
- var distance = Math.sqrt(Math.pow(cx - ocx, 2) + Math.pow(cy - ocy, 2));
1291
- if (distance < 8) score += 4;
1292
- else if (distance < 40) score += 2;
1293
- else if (distance < 120) score += 1;
1294
- }
1295
- return score;
1296
- }
1297
-
1298
- function refetchEquivalent() {
1299
- if (!cached) return null;
1300
- var targetApp = appName || cached.appName || '';
1301
- var best = null;
1302
- var bestScore = 0;
1303
- var visited = [0];
1304
- function visit(elem, depth) {
1305
- if (visited[0] > 350 || depth > 10) return;
1306
- visited[0]++;
1307
- var score = scoreEquivalent(elem);
1308
- if (score > bestScore) {
1309
- best = elem;
1310
- bestScore = score;
1311
- }
1312
- try {
1313
- var kids = childElements(elem);
1314
- for (var i = 0; i < kids.length; i++) visit(kids[i], depth + 1);
1315
- } catch(e) {}
1316
- }
1317
- try {
1318
- if (targetApp) {
1319
- var proc = se.processes[targetApp]();
1320
- var wins = proc.windows();
1321
- for (var w = 0; w < wins.length; w++) visit(wins[w], 0);
1322
- } else {
1323
- var procs = se.processes();
1324
- for (var p = 0; p < procs.length; p++) {
1325
- try {
1326
- var wins2 = procs[p].windows();
1327
- for (var w2 = 0; w2 < wins2.length; w2++) visit(wins2[w2], 0);
1328
- } catch(e2) {}
1329
- }
1330
- }
1331
- } catch(e) {}
1332
- return bestScore >= 6 ? best : null;
1333
- }
1334
-
1335
- var elem = null;
1336
-
1337
- if (appName) {
1338
- try {
1339
- var proc = se.processes[appName]();
1340
- var wins = proc.windows();
1341
- var parts = elemPath.split('/');
1342
- var winIdx = 0;
1343
- var match = parts[0].match(/^win(\\\\d+)$/);
1344
- if (match) winIdx = parseInt(match[1]);
1345
- if (winIdx < wins.length) {
1346
- var current = wins[winIdx];
1347
- for (var i = 1; i < parts.length; i++) {
1348
- var idx = parseInt(parts[i]);
1349
- if (isNaN(idx)) break;
1350
- try {
1351
- var kids = childElements(current);
1352
- if (idx >= kids.length) break;
1353
- current = kids[idx];
1354
- } catch(e) { break; }
1355
- }
1356
- elem = current;
1357
- }
1358
- } catch(e) {}
1359
- }
1360
-
1361
- if (!elem) {
1362
- elem = resolveElementByFullPath(elemPath);
1363
- }
1190
+ var elem = resolveElementInApp(elemPath, appName) || resolveElementByFullPath(elemPath);
1364
1191
 
1365
1192
  if (elem && !descriptorMatches(elem)) {
1366
1193
  elem = refetchEquivalent() || elem;
@@ -1415,10 +1242,10 @@ export class MacOSPlatform {
1415
1242
  }
1416
1243
  async typeInElement(elementId, text, app, clearFirst) {
1417
1244
  this.evictExpiredCacheEntries();
1418
- const escapedText = text.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/`/g, '\\`').replace(/\$/g, '\\$');
1245
+ const textLiteral = JSON.stringify(text);
1419
1246
  const effectiveApp = app || this.activeTarget?.appName;
1420
- const escapedApp = (effectiveApp || "").replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/`/g, '\\`').replace(/\$/g, '\\$');
1421
- const escapedElementId = elementId.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/`/g, '\\`').replace(/\$/g, '\\$');
1247
+ const appLiteral = JSON.stringify(effectiveApp || "");
1248
+ const elementIdLiteral = JSON.stringify(elementId);
1422
1249
  const cached = this.elementCache.get(elementId);
1423
1250
  if (cached && this.isCacheEntryExpired(cached)) {
1424
1251
  this.elementCache.delete(elementId);
@@ -1427,175 +1254,14 @@ export class MacOSPlatform {
1427
1254
  const jxaScript = `
1428
1255
  var se = Application('System Events');
1429
1256
  var _result = null;
1430
- function childElements(elem) {
1431
- try { return elem.uiElements(); } catch(e1) {
1432
- try { return elem.elements(); } catch(e2) { return []; }
1433
- }
1434
- }
1435
- var elemPath = "${escapedElementId}";
1436
- var appName = "${escapedApp}";
1437
- var textToType = "${escapedText}";
1257
+ ${jxaElementActionHelpers()}
1258
+ var elemPath = ${elementIdLiteral};
1259
+ var appName = ${appLiteral};
1260
+ var textToType = ${textLiteral};
1438
1261
  var shouldClear = ${clearFirst ? "true" : "false"};
1439
1262
  var cached = ${cachedJson};
1440
1263
 
1441
- function resolveElementByFullPath(path) {
1442
- var parts = path.split('/');
1443
- if (parts.length < 2) return null;
1444
-
1445
- var procName = parts[0];
1446
- var winPart = parts[1];
1447
- var winIdx = 0;
1448
- var match = winPart.match(/^win(\\\\d+)$/);
1449
- if (match) {
1450
- winIdx = parseInt(match[1]);
1451
- }
1452
-
1453
- try {
1454
- var proc = se.processes[procName]();
1455
- var wins = proc.windows();
1456
- if (winIdx >= wins.length) return null;
1457
- var current = wins[winIdx];
1458
-
1459
- for (var i = 2; i < parts.length; i++) {
1460
- var idx = parseInt(parts[i]);
1461
- if (isNaN(idx)) return null;
1462
- try {
1463
- var kids = childElements(current);
1464
- if (idx >= kids.length) return null;
1465
- current = kids[idx];
1466
- } catch(e) { return null; }
1467
- }
1468
- return current;
1469
- } catch(e) { return null; }
1470
- }
1471
-
1472
- function elemString(elem, getter) {
1473
- try {
1474
- var value = getter(elem);
1475
- return value === undefined || value === null ? '' : String(value);
1476
- } catch(e) {
1477
- return '';
1478
- }
1479
- }
1480
-
1481
- function getBounds(elem) {
1482
- try {
1483
- var pos = elem.position();
1484
- var sz = elem.size();
1485
- return {x: pos[0] || 0, y: pos[1] || 0, width: sz[0] || 0, height: sz[1] || 0};
1486
- } catch(e) {
1487
- return {x: 0, y: 0, width: 0, height: 0};
1488
- }
1489
- }
1490
-
1491
- function descriptorMatches(elem) {
1492
- if (!cached) return true;
1493
- var role = elemString(elem, function(e) { return e.role(); });
1494
- var name = elemString(elem, function(e) { return e.name(); });
1495
- var desc = elemString(elem, function(e) { return e.description(); });
1496
- var value = elemString(elem, function(e) { return e.value(); });
1497
- if (cached.role && role && role !== cached.role) return false;
1498
- if (cached.name && name && name !== cached.name) return false;
1499
- if (cached.value && value && value !== cached.value) return false;
1500
- if (cached.description && desc && desc !== cached.description) return false;
1501
- return true;
1502
- }
1503
-
1504
- function scoreEquivalent(elem) {
1505
- if (!cached) return -1;
1506
- var score = 0;
1507
- var role = elemString(elem, function(e) { return e.role(); });
1508
- var name = elemString(elem, function(e) { return e.name(); });
1509
- var desc = elemString(elem, function(e) { return e.description(); });
1510
- var value = elemString(elem, function(e) { return e.value(); });
1511
- var subrole = elemString(elem, function(e) { return e.subrole(); });
1512
- var identifier = elemString(elem, function(e) { return e.identifier(); });
1513
- if (cached.role && role === cached.role) score += 4;
1514
- if (cached.name && name === cached.name) score += 4;
1515
- if (cached.value && value === cached.value) score += 3;
1516
- if (cached.description && desc === cached.description) score += 2;
1517
- if (cached.subrole && subrole === cached.subrole) score += 2;
1518
- if (cached.identifier && identifier === cached.identifier) score += 3;
1519
- var b = getBounds(elem);
1520
- if (cached.bounds) {
1521
- var cx = b.x + b.width / 2;
1522
- var cy = b.y + b.height / 2;
1523
- var ocx = cached.bounds.x + cached.bounds.width / 2;
1524
- var ocy = cached.bounds.y + cached.bounds.height / 2;
1525
- var distance = Math.sqrt(Math.pow(cx - ocx, 2) + Math.pow(cy - ocy, 2));
1526
- if (distance < 8) score += 4;
1527
- else if (distance < 40) score += 2;
1528
- else if (distance < 120) score += 1;
1529
- }
1530
- return score;
1531
- }
1532
-
1533
- function refetchEquivalent() {
1534
- if (!cached) return null;
1535
- var targetApp = appName || cached.appName || '';
1536
- var best = null;
1537
- var bestScore = 0;
1538
- var visited = [0];
1539
- function visit(elem, depth) {
1540
- if (visited[0] > 350 || depth > 10) return;
1541
- visited[0]++;
1542
- var score = scoreEquivalent(elem);
1543
- if (score > bestScore) {
1544
- best = elem;
1545
- bestScore = score;
1546
- }
1547
- try {
1548
- var kids = childElements(elem);
1549
- for (var i = 0; i < kids.length; i++) visit(kids[i], depth + 1);
1550
- } catch(e) {}
1551
- }
1552
- try {
1553
- if (targetApp) {
1554
- var proc = se.processes[targetApp]();
1555
- var wins = proc.windows();
1556
- for (var w = 0; w < wins.length; w++) visit(wins[w], 0);
1557
- } else {
1558
- var procs = se.processes();
1559
- for (var p = 0; p < procs.length; p++) {
1560
- try {
1561
- var wins2 = procs[p].windows();
1562
- for (var w2 = 0; w2 < wins2.length; w2++) visit(wins2[w2], 0);
1563
- } catch(e2) {}
1564
- }
1565
- }
1566
- } catch(e) {}
1567
- return bestScore >= 6 ? best : null;
1568
- }
1569
-
1570
- var elem = null;
1571
-
1572
- if (appName) {
1573
- try {
1574
- var proc = se.processes[appName]();
1575
- var wins = proc.windows();
1576
- var parts = elemPath.split('/');
1577
- var winIdx = 0;
1578
- var match = parts[0].match(/^win(\\\\d+)$/);
1579
- if (match) winIdx = parseInt(match[1]);
1580
- if (winIdx < wins.length) {
1581
- var current = wins[winIdx];
1582
- for (var i = 1; i < parts.length; i++) {
1583
- var idx = parseInt(parts[i]);
1584
- if (isNaN(idx)) break;
1585
- try {
1586
- var kids = childElements(current);
1587
- if (idx >= kids.length) break;
1588
- current = kids[idx];
1589
- } catch(e) { break; }
1590
- }
1591
- elem = current;
1592
- }
1593
- } catch(e) {}
1594
- }
1595
-
1596
- if (!elem) {
1597
- elem = resolveElementByFullPath(elemPath);
1598
- }
1264
+ var elem = resolveElementInApp(elemPath, appName) || resolveElementByFullPath(elemPath);
1599
1265
 
1600
1266
  if (elem && !descriptorMatches(elem)) {
1601
1267
  elem = refetchEquivalent() || elem;
@@ -1691,170 +1357,12 @@ export class MacOSPlatform {
1691
1357
  const jxaScript = `
1692
1358
  var se = Application('System Events');
1693
1359
  var _result = null;
1694
- function childElements(elem) {
1695
- try { return elem.uiElements(); } catch(e1) {
1696
- try { return elem.elements(); } catch(e2) { return []; }
1697
- }
1698
- }
1360
+ ${jxaElementActionHelpers()}
1699
1361
  var elemPath = ${elementIdLiteral};
1700
1362
  var appName = ${appLiteral};
1701
1363
  var valueToSet = ${valueLiteral};
1702
1364
  var cached = ${cachedJson};
1703
1365
 
1704
- function resolveElementByFullPath(path) {
1705
- var parts = path.split('/');
1706
- if (parts.length < 2) return null;
1707
-
1708
- var procName = parts[0];
1709
- var winPart = parts[1];
1710
- var winIdx = 0;
1711
- var match = winPart.match(/^win(\\\\d+)$/);
1712
- if (match) winIdx = parseInt(match[1]);
1713
-
1714
- try {
1715
- var proc = se.processes[procName]();
1716
- var wins = proc.windows();
1717
- if (winIdx >= wins.length) return null;
1718
- var current = wins[winIdx];
1719
-
1720
- for (var i = 2; i < parts.length; i++) {
1721
- var idx = parseInt(parts[i]);
1722
- if (isNaN(idx)) return null;
1723
- try {
1724
- var kids = childElements(current);
1725
- if (idx >= kids.length) return null;
1726
- current = kids[idx];
1727
- } catch(e) { return null; }
1728
- }
1729
- return current;
1730
- } catch(e) { return null; }
1731
- }
1732
-
1733
- function resolveElementInApp(path, targetApp) {
1734
- if (!targetApp) return null;
1735
- var parts = path.split('/');
1736
- var start = parts[0] === targetApp ? 1 : 0;
1737
- var winPart = parts[start] || 'win0';
1738
- var winIdx = 0;
1739
- var match = winPart.match(/^win(\\\\d+)$/);
1740
- if (match) winIdx = parseInt(match[1]);
1741
-
1742
- try {
1743
- var proc = se.processes[targetApp]();
1744
- var wins = proc.windows();
1745
- if (winIdx >= wins.length) return null;
1746
- var current = wins[winIdx];
1747
- for (var i = start + 1; i < parts.length; i++) {
1748
- var idx = parseInt(parts[i]);
1749
- if (isNaN(idx)) return null;
1750
- try {
1751
- var kids = childElements(current);
1752
- if (idx >= kids.length) return null;
1753
- current = kids[idx];
1754
- } catch(e) { return null; }
1755
- }
1756
- return current;
1757
- } catch(e) { return null; }
1758
- }
1759
-
1760
- function elemString(elem, getter) {
1761
- try {
1762
- var value = getter(elem);
1763
- return value === undefined || value === null ? '' : String(value);
1764
- } catch(e) {
1765
- return '';
1766
- }
1767
- }
1768
-
1769
- function getBounds(elem) {
1770
- try {
1771
- var pos = elem.position();
1772
- var sz = elem.size();
1773
- return {x: pos[0] || 0, y: pos[1] || 0, width: sz[0] || 0, height: sz[1] || 0};
1774
- } catch(e) {
1775
- return {x: 0, y: 0, width: 0, height: 0};
1776
- }
1777
- }
1778
-
1779
- function descriptorMatches(elem) {
1780
- if (!cached) return true;
1781
- var role = elemString(elem, function(e) { return e.role(); });
1782
- var name = elemString(elem, function(e) { return e.name(); });
1783
- var desc = elemString(elem, function(e) { return e.description(); });
1784
- var value = elemString(elem, function(e) { return e.value(); });
1785
- if (cached.role && role && role !== cached.role) return false;
1786
- if (cached.name && name && name !== cached.name) return false;
1787
- if (cached.value && value && value !== cached.value) return false;
1788
- if (cached.description && desc && desc !== cached.description) return false;
1789
- return true;
1790
- }
1791
-
1792
- function scoreEquivalent(elem) {
1793
- if (!cached) return -1;
1794
- var score = 0;
1795
- var role = elemString(elem, function(e) { return e.role(); });
1796
- var name = elemString(elem, function(e) { return e.name(); });
1797
- var desc = elemString(elem, function(e) { return e.description(); });
1798
- var value = elemString(elem, function(e) { return e.value(); });
1799
- var subrole = elemString(elem, function(e) { return e.subrole(); });
1800
- var identifier = elemString(elem, function(e) { return e.identifier(); });
1801
- if (cached.role && role === cached.role) score += 4;
1802
- if (cached.name && name === cached.name) score += 4;
1803
- if (cached.value && value === cached.value) score += 3;
1804
- if (cached.description && desc === cached.description) score += 2;
1805
- if (cached.subrole && subrole === cached.subrole) score += 2;
1806
- if (cached.identifier && identifier === cached.identifier) score += 3;
1807
- var b = getBounds(elem);
1808
- if (cached.bounds) {
1809
- var cx = b.x + b.width / 2;
1810
- var cy = b.y + b.height / 2;
1811
- var ocx = cached.bounds.x + cached.bounds.width / 2;
1812
- var ocy = cached.bounds.y + cached.bounds.height / 2;
1813
- var distance = Math.sqrt(Math.pow(cx - ocx, 2) + Math.pow(cy - ocy, 2));
1814
- if (distance < 8) score += 4;
1815
- else if (distance < 40) score += 2;
1816
- else if (distance < 120) score += 1;
1817
- }
1818
- return score;
1819
- }
1820
-
1821
- function refetchEquivalent() {
1822
- if (!cached) return null;
1823
- var targetApp = appName || cached.appName || '';
1824
- var best = null;
1825
- var bestScore = 0;
1826
- var visited = [0];
1827
- function visit(elem, depth) {
1828
- if (visited[0] > 350 || depth > 10) return;
1829
- visited[0]++;
1830
- var score = scoreEquivalent(elem);
1831
- if (score > bestScore) {
1832
- best = elem;
1833
- bestScore = score;
1834
- }
1835
- try {
1836
- var kids = childElements(elem);
1837
- for (var i = 0; i < kids.length; i++) visit(kids[i], depth + 1);
1838
- } catch(e) {}
1839
- }
1840
- try {
1841
- if (targetApp) {
1842
- var proc = se.processes[targetApp]();
1843
- var wins = proc.windows();
1844
- for (var w = 0; w < wins.length; w++) visit(wins[w], 0);
1845
- } else {
1846
- var procs = se.processes();
1847
- for (var p = 0; p < procs.length; p++) {
1848
- try {
1849
- var wins2 = procs[p].windows();
1850
- for (var w2 = 0; w2 < wins2.length; w2++) visit(wins2[w2], 0);
1851
- } catch(e2) {}
1852
- }
1853
- }
1854
- } catch(e) {}
1855
- return bestScore >= 6 ? best : null;
1856
- }
1857
-
1858
1366
  var elem = resolveElementInApp(elemPath, appName) || resolveElementByFullPath(elemPath);
1859
1367
  if (elem && !descriptorMatches(elem)) {
1860
1368
  elem = refetchEquivalent() || elem;