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.
- package/CHANGELOG.md +67 -3
- package/dist/bin/ucu-mcp.js +1 -1
- package/dist/src/index.d.ts +2 -2
- package/dist/src/index.js +2 -2
- package/dist/src/mcp/server.js +1 -1
- package/dist/src/mcp/tools/app-tools.d.ts +2 -0
- package/dist/src/mcp/tools/app-tools.js +220 -0
- package/dist/src/mcp/tools/element-tools.d.ts +23 -0
- package/dist/src/mcp/tools/element-tools.js +59 -0
- package/dist/src/mcp/tools/helpers.d.ts +82 -0
- package/dist/src/mcp/tools/helpers.js +243 -0
- package/dist/src/mcp/tools/index.d.ts +19 -0
- package/dist/src/mcp/tools/index.js +54 -0
- package/dist/src/mcp/tools/input-tools.d.ts +2 -0
- package/dist/src/mcp/tools/input-tools.js +66 -0
- package/dist/src/mcp/tools/keyboard-tools.d.ts +2 -0
- package/dist/src/mcp/tools/keyboard-tools.js +35 -0
- package/dist/src/mcp/tools/screen-tools.d.ts +2 -0
- package/dist/src/mcp/tools/screen-tools.js +69 -0
- package/dist/src/mcp/tools.d.ts +9 -0
- package/dist/src/mcp/tools.js +96 -25
- package/dist/src/platform/base.d.ts +3 -0
- package/dist/src/platform/jxa-helpers.d.ts +11 -0
- package/dist/src/platform/jxa-helpers.js +206 -0
- package/dist/src/platform/macos/ax-tree.d.ts +4 -0
- package/dist/src/platform/macos/ax-tree.js +462 -0
- package/dist/src/platform/macos/base.d.ts +57 -0
- package/dist/src/platform/macos/base.js +92 -0
- package/dist/src/platform/macos/clipboard.d.ts +3 -0
- package/dist/src/platform/macos/clipboard.js +20 -0
- package/dist/src/platform/macos/element.d.ts +4 -0
- package/dist/src/platform/macos/element.js +212 -0
- package/dist/src/platform/macos/focus.d.ts +3 -0
- package/dist/src/platform/macos/focus.js +33 -0
- package/dist/src/platform/macos/helpers.d.ts +35 -0
- package/dist/src/platform/macos/helpers.js +54 -0
- package/dist/src/platform/macos/index.d.ts +2 -0
- package/dist/src/platform/macos/index.js +1 -0
- package/dist/src/platform/macos/input.d.ts +9 -0
- package/dist/src/platform/macos/input.js +62 -0
- package/dist/src/platform/macos/screen.d.ts +7 -0
- package/dist/src/platform/macos/screen.js +197 -0
- package/dist/src/platform/macos/window.d.ts +6 -0
- package/dist/src/platform/macos/window.js +251 -0
- package/dist/src/platform/macos.d.ts +1 -0
- package/dist/src/platform/macos.js +114 -583
- package/dist/src/safety/guard.js +1 -1
- package/dist/src/util/errors.d.ts +7 -2
- package/dist/src/util/errors.js +7 -3
- package/native/cgevent/cgevent-helper +0 -0
- package/native/ocr/ocr-helper +0 -0
- package/native/windowlist/windowlist-helper +0 -0
- package/package.json +1 -1
package/dist/src/mcp/tools.js
CHANGED
|
@@ -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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
477
|
-
ocr: { ok: ocr.path !== null, path: ocr.path, tried: ocr.tried
|
|
478
|
-
windowlist: { ok: windowlist.path !== null, path: windowlist.path, tried: windowlist.tried
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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>;
|