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.
- package/CHANGELOG.md +50 -4
- 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 +87 -23
- 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.js +71 -563
- 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;
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
483
|
-
ocr: { ok: ocr.path !== null, path: ocr.path, tried: ocr.tried
|
|
484
|
-
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 },
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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>;
|