ucu-mcp 0.3.9 → 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 (53) hide show
  1. package/CHANGELOG.md +67 -3
  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 +96 -25
  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.d.ts +1 -0
  46. package/dist/src/platform/macos.js +114 -583
  47. package/dist/src/safety/guard.js +1 -1
  48. package/dist/src/util/errors.d.ts +7 -2
  49. package/dist/src/util/errors.js +7 -3
  50. package/native/cgevent/cgevent-helper +0 -0
  51. package/native/ocr/ocr-helper +0 -0
  52. package/native/windowlist/windowlist-helper +0 -0
  53. package/package.json +1 -1
@@ -37,11 +37,15 @@ const captureAfterFields = {
37
37
  captureMaxWidth: z.number().default(1280).describe("Maximum width for the post-action screenshot"),
38
38
  captureFormat: z.enum(["png", "jpeg"]).default("jpeg").describe("Format for the post-action screenshot"),
39
39
  };
40
- // Exported so unit tests can pin the schema constraint directly instead
41
- // of going through the McpServer wrapper (which `handler()` calls
42
- // bypass). (Herschel review Major: 0.3.5's value='' test was a
43
- // tautology because it re-created a local zod schema instead of
44
- // asserting against this one.)
40
+ /**
41
+ * Exported so unit tests can pin the schema constraint directly instead
42
+ * of going through the McpServer wrapper (which `handler()` calls
43
+ * bypass). (Herschel review Major: 0.3.5's value='' test was a
44
+ * tautology because it re-created a local zod schema instead of
45
+ * asserting against this one.)
46
+ *
47
+ * @internal Not part of the public API — may change without a semver bump.
48
+ */
45
49
  export const findElementInputSchema = {
46
50
  text: z.string().optional().describe("Text to search"),
47
51
  role: z.string().optional().describe("AX role"),
@@ -63,6 +67,41 @@ async function resolvePoint(x, y, windowId) {
63
67
  throw new WindowNotFoundError(windowId);
64
68
  return { x: win.bounds.x + x, y: win.bounds.y + y };
65
69
  }
70
+ /**
71
+ * Build safety context (window title + URL) for the current active target.
72
+ *
73
+ * The SafetyGuard's window-skip and URL blocklist checks are dead code unless
74
+ * tool handlers pass `windowTitle` / `url` in the `params` object. This helper
75
+ * resolves both from the active target so each action tool can spread the
76
+ * result into its `withSafety` call with minimal overhead.
77
+ */
78
+ async function getSafetyContext(windowId) {
79
+ const target = activeTargetContext;
80
+ const effectiveWindowId = windowId ?? target?.windowId;
81
+ let windowTitle;
82
+ if (effectiveWindowId) {
83
+ try {
84
+ const windows = await getPlatform().listWindows();
85
+ const win = windows.find(w => w.id === effectiveWindowId);
86
+ windowTitle = win?.title;
87
+ }
88
+ catch { /* best effort */ }
89
+ }
90
+ if (!windowTitle && target?.title) {
91
+ windowTitle = target.title;
92
+ }
93
+ let url;
94
+ const platform = getPlatform();
95
+ if (platform.getActiveBrowserContext) {
96
+ try {
97
+ const appName = target?.appName;
98
+ const ctx = await platform.getActiveBrowserContext(appName);
99
+ url = ctx?.url;
100
+ }
101
+ catch { /* best effort */ }
102
+ }
103
+ return { windowTitle, url };
104
+ }
66
105
  function jsonText(value) {
67
106
  return { type: "text", text: JSON.stringify(value, null, 2) };
68
107
  }
@@ -97,7 +136,7 @@ function errorDetails(error) {
97
136
  // Some platform errors carry an inline `hint` field (added by macos.ts focusApp
98
137
  // for the Electron AX case, etc.). Surface it under `hint` so the model can
99
138
  // see remediation without parsing the message string.
100
- const inlineHint = err.hint;
139
+ const inlineHint = err instanceof UcuError ? err.hint : undefined;
101
140
  const details = {
102
141
  name: err.name,
103
142
  code,
@@ -105,7 +144,7 @@ function errorDetails(error) {
105
144
  message: err.message,
106
145
  recovery: recoveryHint(code),
107
146
  };
108
- if (typeof inlineHint === "string" && inlineHint.length > 0) {
147
+ if (inlineHint) {
109
148
  details.hint = inlineHint;
110
149
  }
111
150
  return details;
@@ -208,7 +247,13 @@ async function withSafety(sa) {
208
247
  }
209
248
  if (sa.dryRun)
210
249
  return `[DRY-RUN] ${await sa.dryRun()}`;
211
- const shouldManageFocus = sa.requiresAccessibility && !["screenshot", "list_windows", "list_apps", "get_window_state", "get_cursor_position", "get_screen_size", "ocr", "doctor", "wait", "wait_for_element", "find_element", "focus_app"].includes(sa.action);
250
+ // Focus management is disabled by default: CGEvent input injection works
251
+ // at the HID level without requiring the target app to be frontmost, and
252
+ // AX operations target processes by name/PID via System Events. The user
253
+ // should remain in their current app while the agent works in the background.
254
+ // Re-enable saveFocus/restoreFocus only if a specific AX operation truly
255
+ // requires the target app to be frontmost (rare).
256
+ const shouldManageFocus = false;
212
257
  if (shouldManageFocus)
213
258
  await platform.saveFocus?.();
214
259
  const start = Date.now();
@@ -298,7 +343,6 @@ export function registerTools(server) {
298
343
  if (windows.length === 0) {
299
344
  let accessibility = "unknown";
300
345
  try {
301
- const { checkPermission } = await import("../safety/permissions.js");
302
346
  const { granted } = await checkPermission("accessibility");
303
347
  accessibility = granted ? "granted" : "denied";
304
348
  }
@@ -341,7 +385,8 @@ export function registerTools(server) {
341
385
  ...captureAfterFields,
342
386
  }, async (params) => {
343
387
  const pt = await resolvePoint(params.x, params.y, params.windowId);
344
- await withSafety({ action: "click", params: { x: pt.x, y: pt.y }, requiresAccessibility: true, execute: () => getPlatform().click(pt.x, pt.y, params.button) });
388
+ const safetyCtx = await getSafetyContext(params.windowId);
389
+ await withSafety({ action: "click", params: { x: pt.x, y: pt.y, ...safetyCtx }, requiresAccessibility: true, execute: () => getPlatform().click(pt.x, pt.y, params.button) });
345
390
  return actionResponse("click", { clicked: true, x: pt.x, y: pt.y }, { x: pt.x, y: pt.y, windowId: params.windowId }, params.captureAfter, params.captureFormat, params.captureMaxWidth);
346
391
  });
347
392
  registry.register("click");
@@ -352,7 +397,8 @@ export function registerTools(server) {
352
397
  ...captureAfterFields,
353
398
  }, async (params) => {
354
399
  const pt = await resolvePoint(params.x, params.y, params.windowId);
355
- await withSafety({ action: "click", params: { x: pt.x, y: pt.y, doubleClick: true }, requiresAccessibility: true, execute: () => getPlatform().click(pt.x, pt.y, params.button, true) });
400
+ const safetyCtx = await getSafetyContext(params.windowId);
401
+ await withSafety({ action: "click", params: { x: pt.x, y: pt.y, doubleClick: true, ...safetyCtx }, requiresAccessibility: true, execute: () => getPlatform().click(pt.x, pt.y, params.button, true) });
356
402
  return actionResponse("double_click", { doubleClicked: true, x: pt.x, y: pt.y }, { x: pt.x, y: pt.y, windowId: params.windowId }, params.captureAfter, params.captureFormat, params.captureMaxWidth);
357
403
  });
358
404
  registry.register("double_click");
@@ -363,7 +409,8 @@ export function registerTools(server) {
363
409
  }, async (params) => {
364
410
  if (params.windowId)
365
411
  throw new UnsupportedParameterError("windowId-targeted keyboard typing is not implemented");
366
- await withSafety({ action: "type_text", params: { text: params.text }, requiresAccessibility: true, execute: () => getPlatform().type(params.text, params.delay) });
412
+ const safetyCtx = await getSafetyContext();
413
+ await withSafety({ action: "type_text", params: { text: params.text, ...safetyCtx }, requiresAccessibility: true, execute: () => getPlatform().type(params.text, params.delay) });
367
414
  return actionResponse("type_text", { typed: true, charCount: params.text.length }, {}, params.captureAfter, params.captureFormat, params.captureMaxWidth);
368
415
  });
369
416
  registry.register("type_text");
@@ -382,7 +429,8 @@ export function registerTools(server) {
382
429
  ];
383
430
  if (keys.length === 0)
384
431
  throw new UnsupportedParameterError("press_key requires at least one key");
385
- await withSafety({ action: "press_key", params: { keys }, requiresAccessibility: true, execute: () => getPlatform().key(keys) });
432
+ const safetyCtx = await getSafetyContext();
433
+ await withSafety({ action: "press_key", params: { keys, ...safetyCtx }, requiresAccessibility: true, execute: () => getPlatform().key(keys) });
386
434
  return actionResponse("press_key", { pressed: true, keys }, {}, params.captureAfter, params.captureFormat, params.captureMaxWidth);
387
435
  });
388
436
  registry.register("press_key");
@@ -394,7 +442,8 @@ export function registerTools(server) {
394
442
  }, async (params) => {
395
443
  const pt = await resolvePoint(params.x, params.y, params.windowId);
396
444
  const deltaX = params.deltaX ?? 0;
397
- await withSafety({ action: "scroll", params: { x: pt.x, y: pt.y }, requiresAccessibility: true, execute: () => getPlatform().scroll(pt.x, pt.y, deltaX, params.deltaY) });
445
+ const safetyCtx = await getSafetyContext(params.windowId);
446
+ await withSafety({ action: "scroll", params: { x: pt.x, y: pt.y, ...safetyCtx }, requiresAccessibility: true, execute: () => getPlatform().scroll(pt.x, pt.y, deltaX, params.deltaY) });
398
447
  return actionResponse("scroll", { scrolled: true, x: pt.x, y: pt.y }, { x: pt.x, y: pt.y, windowId: params.windowId }, params.captureAfter, params.captureFormat, params.captureMaxWidth);
399
448
  });
400
449
  registry.register("scroll");
@@ -408,7 +457,8 @@ export function registerTools(server) {
408
457
  }, async (params) => {
409
458
  const start = await resolvePoint(params.startX, params.startY, params.windowId);
410
459
  const end = await resolvePoint(params.endX, params.endY, params.windowId);
411
- await withSafety({ action: "drag", params: { startX: start.x, startY: start.y, endX: end.x, endY: end.y }, requiresAccessibility: true, execute: () => getPlatform().drag(start.x, start.y, end.x, end.y, params.button, params.duration) });
460
+ const safetyCtx = await getSafetyContext(params.windowId);
461
+ await withSafety({ action: "drag", params: { startX: start.x, startY: start.y, endX: end.x, endY: end.y, ...safetyCtx }, requiresAccessibility: true, execute: () => getPlatform().drag(start.x, start.y, end.x, end.y, params.button, params.duration) });
412
462
  return actionResponse("drag", { dragged: true, startX: start.x, startY: start.y, endX: end.x, endY: end.y }, { startX: start.x, startY: start.y, endX: end.x, endY: end.y, windowId: params.windowId }, params.captureAfter, params.captureFormat, params.captureMaxWidth);
413
463
  });
414
464
  registry.register("drag");
@@ -473,9 +523,9 @@ export function registerTools(server) {
473
523
  const ocr = resolveHelperPath(["native", "ocr", "ocr-helper"]);
474
524
  const windowlist = resolveHelperPath(["native", "windowlist", "windowlist-helper"]);
475
525
  nativeHelpers = {
476
- cgevent: { ok: cgevent.path !== null, path: cgevent.path, tried: cgevent.tried.slice(0, 3) },
477
- ocr: { ok: ocr.path !== null, path: ocr.path, tried: ocr.tried.slice(0, 3) },
478
- windowlist: { ok: windowlist.path !== null, path: windowlist.path, tried: windowlist.tried.slice(0, 3) },
526
+ cgevent: { ok: cgevent.path !== null, path: cgevent.path, tried: cgevent.tried },
527
+ ocr: { ok: ocr.path !== null, path: ocr.path, tried: ocr.tried },
528
+ windowlist: { ok: windowlist.path !== null, path: windowlist.path, tried: windowlist.tried },
479
529
  };
480
530
  }
481
531
  let readiness = "ready";
@@ -634,7 +684,8 @@ export function registerTools(server) {
634
684
  registerTool("get_screen_size", "Get screen dimensions and scale factor", {
635
685
  display: z.number().optional().describe("Display index"),
636
686
  }, async (params) => {
637
- return { content: [{ type: "text", text: JSON.stringify(getPlatform().getScreenSize(params.display), null, 2) }] };
687
+ const result = await withSafety({ action: "get_screen_size", params: {}, execute: () => Promise.resolve(getPlatform().getScreenSize(params.display)) });
688
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
638
689
  });
639
690
  registry.register("get_screen_size");
640
691
  registerTool("ocr", "Perform OCR on screen region", {
@@ -651,22 +702,40 @@ export function registerTools(server) {
651
702
  ...captureAfterFields,
652
703
  }, async (params) => {
653
704
  const pt = await resolvePoint(params.x, params.y, params.windowId);
654
- await withSafety({ action: "move", params: { x: pt.x, y: pt.y }, requiresAccessibility: true, execute: () => getPlatform().move(pt.x, pt.y) });
705
+ const safetyCtx = await getSafetyContext(params.windowId);
706
+ await withSafety({ action: "move", params: { x: pt.x, y: pt.y, ...safetyCtx }, requiresAccessibility: true, execute: () => getPlatform().move(pt.x, pt.y) });
655
707
  return actionResponse("move", { moved: true, x: pt.x, y: pt.y }, { x: pt.x, y: pt.y, windowId: params.windowId }, params.captureAfter, params.captureFormat, params.captureMaxWidth);
656
708
  });
657
709
  registry.register("move");
658
710
  registerTool("find_element", "Find accessibility elements by text, role, or value. Supports value/index/near selectors.", findElementInputSchema, async (params) => {
659
711
  const effectiveApp = params.app || getActiveTarget()?.appName;
660
- const response = await withSafety({ action: "find_element", params: {}, requiresAccessibility: true,
712
+ const safetyCtx = await getSafetyContext(params.windowId);
713
+ const response = await withSafety({ action: "find_element", params: { ...safetyCtx }, requiresAccessibility: true,
661
714
  execute: () => getPlatform().findElement({ text: params.text, role: params.role, app: effectiveApp, depth: params.depth, includeBounds: params.includeBounds, maxResults: params.maxResults, textMode: params.textMode, visibleOnly: params.visibleOnly, value: params.value, index: params.index, near: params.near }) });
662
- return { content: [{ type: "text", text: JSON.stringify({ results: response.results, metrics: response.metrics }, null, 2) }] };
715
+ const payload = { results: response.results, metrics: response.metrics };
716
+ // When find_element returns 0 results AND scannedCount is 0 for a specific
717
+ // app, the AX tree is empty — the most common cause is Electron / Chromium
718
+ // apps whose AX tree is not exposed to System Events. Attach a pixel-level
719
+ // fallback hint so the model can proceed via screenshot + ocr + click
720
+ // instead of retrying find_element forever.
721
+ if (response.results.length === 0 && effectiveApp && response.metrics.scannedCount === 0) {
722
+ payload.hint =
723
+ `${effectiveApp} returned 0 AX elements (scannedCount=0, meaning the AX tree is empty). ` +
724
+ "This is typical for Electron/Chromium apps whose AX tree is not exposed to System Events. " +
725
+ "Pixel-level workaround: call screenshot to capture the screen, then ocr to locate " +
726
+ "the target UI text and get its bounding box coordinates, then click(x, y) at those " +
727
+ "screen coordinates. Alternatively, use type_text or press_key for keyboard-based " +
728
+ "interaction, or modify the app's config file or database directly.";
729
+ }
730
+ return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
663
731
  });
664
732
  registry.register("find_element");
665
733
  registerTool("click_element", "Click an accessibility element by its ID", {
666
734
  elementId: z.string().describe("AX element identifier"), app: z.string().optional().describe("Target app"), ...captureAfterFields,
667
735
  }, async (params) => {
668
736
  const effectiveApp = params.app || getActiveTarget()?.appName;
669
- await withSafety({ action: "click_element", params: {}, requiresAccessibility: true, execute: () => getPlatform().clickElement(params.elementId, effectiveApp) });
737
+ const safetyCtx = await getSafetyContext();
738
+ await withSafety({ action: "click_element", params: { ...safetyCtx }, requiresAccessibility: true, execute: () => getPlatform().clickElement(params.elementId, effectiveApp) });
670
739
  return actionResponse("click_element", { clicked: true, elementId: params.elementId }, { elementId: params.elementId, app: effectiveApp }, params.captureAfter, params.captureFormat, params.captureMaxWidth);
671
740
  });
672
741
  registry.register("click_element");
@@ -674,7 +743,8 @@ export function registerTools(server) {
674
743
  elementId: z.string().describe("AX element identifier"), value: z.string().describe("Value to set"), app: z.string().optional().describe("Target app"), ...captureAfterFields,
675
744
  }, async (params) => {
676
745
  const effectiveApp = params.app || getActiveTarget()?.appName;
677
- await withSafety({ action: "set_value", params: { value: params.value }, requiresAccessibility: true, execute: () => getPlatform().setElementValue(params.elementId, params.value, effectiveApp) });
746
+ const safetyCtx = await getSafetyContext();
747
+ await withSafety({ action: "set_value", params: { value: params.value, ...safetyCtx }, requiresAccessibility: true, execute: () => getPlatform().setElementValue(params.elementId, params.value, effectiveApp) });
678
748
  return actionResponse("set_value", { setValue: true, elementId: params.elementId }, { elementId: params.elementId, app: effectiveApp }, params.captureAfter, params.captureFormat, params.captureMaxWidth);
679
749
  });
680
750
  registry.register("set_value");
@@ -683,7 +753,8 @@ export function registerTools(server) {
683
753
  app: z.string().optional().describe("Target app"), clearFirst: z.boolean().optional().describe("Clear existing text before typing"), ...captureAfterFields,
684
754
  }, async (params) => {
685
755
  const effectiveApp = params.app || getActiveTarget()?.appName;
686
- await withSafety({ action: "type_in_element", params: { text: params.text }, requiresAccessibility: true, execute: () => getPlatform().typeInElement(params.elementId, params.text, effectiveApp, params.clearFirst) });
756
+ const safetyCtx = await getSafetyContext();
757
+ await withSafety({ action: "type_in_element", params: { text: params.text, ...safetyCtx }, requiresAccessibility: true, execute: () => getPlatform().typeInElement(params.elementId, params.text, effectiveApp, params.clearFirst) });
687
758
  return actionResponse("type_in_element", { typed: true, elementId: params.elementId, charCount: params.text.length }, { elementId: params.elementId, app: effectiveApp }, params.captureAfter, params.captureFormat, params.captureMaxWidth);
688
759
  });
689
760
  registry.register("type_in_element");
@@ -8,6 +8,7 @@ export interface ScreenSize {
8
8
  width: number;
9
9
  height: number;
10
10
  scaleFactor?: number;
11
+ estimated?: boolean;
11
12
  }
12
13
  export interface ScreenshotOptions {
13
14
  format?: "png" | "jpeg";
@@ -100,6 +101,8 @@ export interface FindElementResult {
100
101
  role: string;
101
102
  name: string;
102
103
  value?: string;
104
+ subrole?: string;
105
+ identifier?: string;
103
106
  bounds?: {
104
107
  x: number;
105
108
  y: number;
@@ -0,0 +1,11 @@
1
+ export declare function jxaChildElements(): string;
2
+ export declare function jxaResolveElementByFullPath(): string;
3
+ export declare function jxaResolveElementInApp(): string;
4
+ export declare function jxaElemString(): string;
5
+ export declare function jxaGetBounds(): string;
6
+ export declare function jxaIsVisible(): string;
7
+ export declare function jxaDescriptorMatches(): string;
8
+ export declare function jxaScoreEquivalent(): string;
9
+ export declare function jxaRefetchEquivalent(): string;
10
+ /** Common helper set used by clickElement, typeInElement, setElementValue */
11
+ export declare function jxaElementActionHelpers(): string;
@@ -0,0 +1,206 @@
1
+ // JXA helper function templates for macOS accessibility scripts.
2
+ // Each function returns a JXA code string that can be embedded in osascript calls.
3
+ export function jxaChildElements() {
4
+ return `
5
+ function childElements(elem) {
6
+ try { return elem.uiElements(); } catch(e1) {
7
+ try { return elem.elements(); } catch(e2) { return []; }
8
+ }
9
+ }`;
10
+ }
11
+ export function jxaResolveElementByFullPath() {
12
+ return String.raw `
13
+ function resolveElementByFullPath(path) {
14
+ var parts = path.split('/');
15
+ if (parts.length < 2) return null;
16
+
17
+ var procName = parts[0];
18
+ var winPart = parts[1];
19
+ var winIdx = 0;
20
+ var match = winPart.match(/^win(\d+)$/);
21
+ if (match) {
22
+ winIdx = parseInt(match[1]);
23
+ }
24
+
25
+ try {
26
+ var proc = se.processes[procName]();
27
+ var wins = proc.windows();
28
+ if (winIdx >= wins.length) return null;
29
+ var current = wins[winIdx];
30
+
31
+ for (var i = 2; i < parts.length; i++) {
32
+ var idx = parseInt(parts[i]);
33
+ if (isNaN(idx)) return null;
34
+ try {
35
+ var kids = childElements(current);
36
+ if (idx >= kids.length) return null;
37
+ current = kids[idx];
38
+ } catch(e) { return null; }
39
+ }
40
+ return current;
41
+ } catch(e) { return null; }
42
+ }`;
43
+ }
44
+ export function jxaResolveElementInApp() {
45
+ return String.raw `
46
+ function resolveElementInApp(path, targetApp) {
47
+ if (!targetApp) return null;
48
+ var parts = path.split('/');
49
+ var start = parts[0] === targetApp ? 1 : 0;
50
+ var winPart = parts[start] || 'win0';
51
+ var winIdx = 0;
52
+ var match = winPart.match(/^win(\d+)$/);
53
+ if (match) winIdx = parseInt(match[1]);
54
+
55
+ try {
56
+ var proc = se.processes[targetApp]();
57
+ var wins = proc.windows();
58
+ if (winIdx >= wins.length) return null;
59
+ var current = wins[winIdx];
60
+ for (var i = start + 1; i < parts.length; i++) {
61
+ var idx = parseInt(parts[i]);
62
+ if (isNaN(idx)) return null;
63
+ try {
64
+ var kids = childElements(current);
65
+ if (idx >= kids.length) return null;
66
+ current = kids[idx];
67
+ } catch(e) { return null; }
68
+ }
69
+ return current;
70
+ } catch(e) { return null; }
71
+ }`;
72
+ }
73
+ export function jxaElemString() {
74
+ return `
75
+ function elemString(elem, getter) {
76
+ try {
77
+ var value = getter(elem);
78
+ return value === undefined || value === null ? '' : String(value);
79
+ } catch(e) {
80
+ return '';
81
+ }
82
+ }`;
83
+ }
84
+ export function jxaGetBounds() {
85
+ return `
86
+ function getBounds(elem) {
87
+ try {
88
+ var pos = elem.position();
89
+ var sz = elem.size();
90
+ return {x: pos[0] || 0, y: pos[1] || 0, width: sz[0] || 0, height: sz[1] || 0};
91
+ } catch(e) {
92
+ return {x: 0, y: 0, width: 0, height: 0};
93
+ }
94
+ }`;
95
+ }
96
+ export function jxaIsVisible() {
97
+ return `
98
+ function isVisible(elem) {
99
+ try {
100
+ var pos = elem.position();
101
+ var sz = elem.size();
102
+ if (!pos || !sz) return false;
103
+ return sz[0] > 0 && sz[1] > 0 && pos[0] > -10000 && pos[1] > -10000;
104
+ } catch(e) {
105
+ return false;
106
+ }
107
+ }`;
108
+ }
109
+ export function jxaDescriptorMatches() {
110
+ return `
111
+ function descriptorMatches(elem) {
112
+ if (!cached) return true;
113
+ var role = elemString(elem, function(e) { return e.role(); });
114
+ var name = elemString(elem, function(e) { return e.name(); });
115
+ var desc = elemString(elem, function(e) { return e.description(); });
116
+ var value = elemString(elem, function(e) { return e.value(); });
117
+ if (cached.role && role && role !== cached.role) return false;
118
+ if (cached.name && name && name !== cached.name) return false;
119
+ if (cached.value && value && value !== cached.value) return false;
120
+ if (cached.description && desc && desc !== cached.description) return false;
121
+ return true;
122
+ }`;
123
+ }
124
+ export function jxaScoreEquivalent() {
125
+ return `
126
+ function scoreEquivalent(elem) {
127
+ if (!cached) return -1;
128
+ var score = 0;
129
+ var role = elemString(elem, function(e) { return e.role(); });
130
+ var name = elemString(elem, function(e) { return e.name(); });
131
+ var desc = elemString(elem, function(e) { return e.description(); });
132
+ var value = elemString(elem, function(e) { return e.value(); });
133
+ var subrole = elemString(elem, function(e) { return e.subrole(); });
134
+ var identifier = elemString(elem, function(e) { return e.identifier(); });
135
+ if (cached.role && role === cached.role) score += 4;
136
+ if (cached.name && name === cached.name) score += 4;
137
+ if (cached.value && value === cached.value) score += 3;
138
+ if (cached.description && desc === cached.description) score += 2;
139
+ if (cached.subrole && subrole === cached.subrole) score += 2;
140
+ if (cached.identifier && identifier === cached.identifier) score += 3;
141
+ var b = getBounds(elem);
142
+ if (cached.bounds) {
143
+ var cx = b.x + b.width / 2;
144
+ var cy = b.y + b.height / 2;
145
+ var ocx = cached.bounds.x + cached.bounds.width / 2;
146
+ var ocy = cached.bounds.y + cached.bounds.height / 2;
147
+ var distance = Math.sqrt(Math.pow(cx - ocx, 2) + Math.pow(cy - ocy, 2));
148
+ if (distance < 8) score += 4;
149
+ else if (distance < 40) score += 2;
150
+ else if (distance < 120) score += 1;
151
+ }
152
+ return score;
153
+ }`;
154
+ }
155
+ export function jxaRefetchEquivalent() {
156
+ return `
157
+ function refetchEquivalent() {
158
+ if (!cached) return null;
159
+ var targetApp = appName || cached.appName || '';
160
+ var best = null;
161
+ var bestScore = 0;
162
+ var visited = [0];
163
+ function visit(elem, depth) {
164
+ if (visited[0] > 350 || depth > 10) return;
165
+ visited[0]++;
166
+ var score = scoreEquivalent(elem);
167
+ if (score > bestScore) {
168
+ best = elem;
169
+ bestScore = score;
170
+ }
171
+ try {
172
+ var kids = childElements(elem);
173
+ for (var i = 0; i < kids.length; i++) visit(kids[i], depth + 1);
174
+ } catch(e) {}
175
+ }
176
+ try {
177
+ if (targetApp) {
178
+ var proc = se.processes[targetApp]();
179
+ var wins = proc.windows();
180
+ for (var w = 0; w < wins.length; w++) visit(wins[w], 0);
181
+ } else {
182
+ var procs = se.processes();
183
+ for (var p = 0; p < procs.length; p++) {
184
+ try {
185
+ var wins2 = procs[p].windows();
186
+ for (var w2 = 0; w2 < wins2.length; w2++) visit(wins2[w2], 0);
187
+ } catch(e2) {}
188
+ }
189
+ }
190
+ } catch(e) {}
191
+ return bestScore >= 6 ? best : null;
192
+ }`;
193
+ }
194
+ /** Common helper set used by clickElement, typeInElement, setElementValue */
195
+ export function jxaElementActionHelpers() {
196
+ return [
197
+ jxaChildElements(),
198
+ jxaResolveElementByFullPath(),
199
+ jxaResolveElementInApp(),
200
+ jxaElemString(),
201
+ jxaGetBounds(),
202
+ jxaDescriptorMatches(),
203
+ jxaScoreEquivalent(),
204
+ jxaRefetchEquivalent(),
205
+ ].join("\n");
206
+ }
@@ -0,0 +1,4 @@
1
+ import type { MacOSPlatform } from "./base.js";
2
+ import type { WindowState, FindElementOptions, FindElementResponse } from "../base.js";
3
+ export declare function getWindowState(this: MacOSPlatform, windowId?: string, depth?: number, includeBounds?: boolean): Promise<WindowState>;
4
+ export declare function findElement(this: MacOSPlatform, options: FindElementOptions): Promise<FindElementResponse>;