ucu-mcp 0.4.0 → 0.4.3

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 +225 -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 +84 -0
  11. package/dist/src/mcp/tools/helpers.js +247 -0
  12. package/dist/src/mcp/tools/index.d.ts +19 -0
  13. package/dist/src/mcp/tools/index.js +55 -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
@@ -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;
@@ -304,7 +343,6 @@ export function registerTools(server) {
304
343
  if (windows.length === 0) {
305
344
  let accessibility = "unknown";
306
345
  try {
307
- const { checkPermission } = await import("../safety/permissions.js");
308
346
  const { granted } = await checkPermission("accessibility");
309
347
  accessibility = granted ? "granted" : "denied";
310
348
  }
@@ -347,7 +385,8 @@ export function registerTools(server) {
347
385
  ...captureAfterFields,
348
386
  }, async (params) => {
349
387
  const pt = await resolvePoint(params.x, params.y, params.windowId);
350
- 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) });
351
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);
352
391
  });
353
392
  registry.register("click");
@@ -358,7 +397,8 @@ export function registerTools(server) {
358
397
  ...captureAfterFields,
359
398
  }, async (params) => {
360
399
  const pt = await resolvePoint(params.x, params.y, params.windowId);
361
- 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) });
362
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);
363
403
  });
364
404
  registry.register("double_click");
@@ -369,7 +409,8 @@ export function registerTools(server) {
369
409
  }, async (params) => {
370
410
  if (params.windowId)
371
411
  throw new UnsupportedParameterError("windowId-targeted keyboard typing is not implemented");
372
- 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) });
373
414
  return actionResponse("type_text", { typed: true, charCount: params.text.length }, {}, params.captureAfter, params.captureFormat, params.captureMaxWidth);
374
415
  });
375
416
  registry.register("type_text");
@@ -388,7 +429,8 @@ export function registerTools(server) {
388
429
  ];
389
430
  if (keys.length === 0)
390
431
  throw new UnsupportedParameterError("press_key requires at least one key");
391
- 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) });
392
434
  return actionResponse("press_key", { pressed: true, keys }, {}, params.captureAfter, params.captureFormat, params.captureMaxWidth);
393
435
  });
394
436
  registry.register("press_key");
@@ -400,7 +442,8 @@ export function registerTools(server) {
400
442
  }, async (params) => {
401
443
  const pt = await resolvePoint(params.x, params.y, params.windowId);
402
444
  const deltaX = params.deltaX ?? 0;
403
- 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) });
404
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);
405
448
  });
406
449
  registry.register("scroll");
@@ -414,7 +457,8 @@ export function registerTools(server) {
414
457
  }, async (params) => {
415
458
  const start = await resolvePoint(params.startX, params.startY, params.windowId);
416
459
  const end = await resolvePoint(params.endX, params.endY, params.windowId);
417
- 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) });
418
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);
419
463
  });
420
464
  registry.register("drag");
@@ -479,9 +523,9 @@ export function registerTools(server) {
479
523
  const ocr = resolveHelperPath(["native", "ocr", "ocr-helper"]);
480
524
  const windowlist = resolveHelperPath(["native", "windowlist", "windowlist-helper"]);
481
525
  nativeHelpers = {
482
- cgevent: { ok: cgevent.path !== null, path: cgevent.path, tried: cgevent.tried.slice(0, 3) },
483
- ocr: { ok: ocr.path !== null, path: ocr.path, tried: ocr.tried.slice(0, 3) },
484
- 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 },
485
529
  };
486
530
  }
487
531
  let readiness = "ready";
@@ -658,22 +702,40 @@ export function registerTools(server) {
658
702
  ...captureAfterFields,
659
703
  }, async (params) => {
660
704
  const pt = await resolvePoint(params.x, params.y, params.windowId);
661
- 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) });
662
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);
663
708
  });
664
709
  registry.register("move");
665
710
  registerTool("find_element", "Find accessibility elements by text, role, or value. Supports value/index/near selectors.", findElementInputSchema, async (params) => {
666
711
  const effectiveApp = params.app || getActiveTarget()?.appName;
667
- 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,
668
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 }) });
669
- 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) }] };
670
731
  });
671
732
  registry.register("find_element");
672
733
  registerTool("click_element", "Click an accessibility element by its ID", {
673
734
  elementId: z.string().describe("AX element identifier"), app: z.string().optional().describe("Target app"), ...captureAfterFields,
674
735
  }, async (params) => {
675
736
  const effectiveApp = params.app || getActiveTarget()?.appName;
676
- 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) });
677
739
  return actionResponse("click_element", { clicked: true, elementId: params.elementId }, { elementId: params.elementId, app: effectiveApp }, params.captureAfter, params.captureFormat, params.captureMaxWidth);
678
740
  });
679
741
  registry.register("click_element");
@@ -681,7 +743,8 @@ export function registerTools(server) {
681
743
  elementId: z.string().describe("AX element identifier"), value: z.string().describe("Value to set"), app: z.string().optional().describe("Target app"), ...captureAfterFields,
682
744
  }, async (params) => {
683
745
  const effectiveApp = params.app || getActiveTarget()?.appName;
684
- 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) });
685
748
  return actionResponse("set_value", { setValue: true, elementId: params.elementId }, { elementId: params.elementId, app: effectiveApp }, params.captureAfter, params.captureFormat, params.captureMaxWidth);
686
749
  });
687
750
  registry.register("set_value");
@@ -690,7 +753,8 @@ export function registerTools(server) {
690
753
  app: z.string().optional().describe("Target app"), clearFirst: z.boolean().optional().describe("Clear existing text before typing"), ...captureAfterFields,
691
754
  }, async (params) => {
692
755
  const effectiveApp = params.app || getActiveTarget()?.appName;
693
- 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) });
694
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);
695
759
  });
696
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>;