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
@@ -0,0 +1,462 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { WindowNotFoundError, PlatformError } from "../../util/errors.js";
3
+ import { rethrowAccessibilityError } from "./helpers.js";
4
+ import { jxaChildElements, jxaGetBounds, jxaIsVisible } from "../jxa-helpers.js";
5
+ export async function getWindowState(windowId, depth, includeBounds = true) {
6
+ if (!windowId || windowId === this.activeTarget?.windowId) {
7
+ await this.validateActiveTarget();
8
+ }
9
+ const resolvedWindowId = windowId || this.activeTarget?.windowId;
10
+ if (!resolvedWindowId) {
11
+ throw new WindowNotFoundError("active target");
12
+ }
13
+ const maxDepth = Math.min(depth || 3, 10);
14
+ const maxElements = 50;
15
+ const windowIdLiteral = JSON.stringify(resolvedWindowId);
16
+ const targetWindow = (await this.listWindows(true)).find((w) => w.id === resolvedWindowId);
17
+ const targetJson = JSON.stringify(targetWindow ?? null);
18
+ try {
19
+ const jxaScript = `
20
+ ObjC.import('AppKit');
21
+ var se = Application('System Events');
22
+ ${jxaChildElements()}
23
+ var result = {window: null, focusedElement: null, tree: null, error: null};
24
+ var target = ${targetJson};
25
+ var includeBounds = ${includeBounds ? "true" : "false"};
26
+
27
+ function closeEnough(a, b, tolerance) {
28
+ return Math.abs(Number(a || 0) - Number(b || 0)) <= tolerance;
29
+ }
30
+
31
+ function windowMatches(win, proc) {
32
+ if (!target) {
33
+ try { return String(win.id()) === String(${windowIdLiteral}); } catch(e) { return false; }
34
+ }
35
+ try {
36
+ if (target.pid && proc.unixId && proc.unixId() !== target.pid) return false;
37
+ } catch(e) {}
38
+
39
+ var name = "";
40
+ try { name = win.name() || ""; } catch(e) {}
41
+ if (target.title && name && name === target.title) return true;
42
+
43
+ try {
44
+ var pos = win.position();
45
+ var size = win.size();
46
+ var b = target.bounds || {};
47
+ return closeEnough(pos[0], b.x, 12) &&
48
+ closeEnough(pos[1], b.y, 12) &&
49
+ closeEnough(size[0], b.width, 24) &&
50
+ closeEnough(size[1], b.height, 24);
51
+ } catch(e) {}
52
+
53
+ try { return String(win.id()) === String(${windowIdLiteral}); } catch(e) {}
54
+ return false;
55
+ }
56
+
57
+ var foundWin = null;
58
+ var foundProc = null;
59
+
60
+ var idParts = ${windowIdLiteral}.split('/');
61
+ if (idParts.length >= 2 && idParts[0]) {
62
+ var procName = idParts[0];
63
+ var winIdx = 0;
64
+ var winMatch = idParts[1].match(/^win(\\d+)$/);
65
+ if (winMatch) winIdx = parseInt(winMatch[1]);
66
+ try {
67
+ var proc = se.processes[procName]();
68
+ var ws = proc.windows();
69
+ if (winIdx < ws.length) {
70
+ foundWin = ws[winIdx];
71
+ foundProc = proc;
72
+ }
73
+ } catch(e) {}
74
+ }
75
+
76
+ try {
77
+ if (!foundWin) {
78
+ var procs = se.processes();
79
+ for (var p = 0; p < procs.length; p++) {
80
+ var proc = procs[p];
81
+ try {
82
+ var wins = proc.windows();
83
+ for (var w = 0; w < wins.length; w++) {
84
+ if (windowMatches(wins[w], proc)) {
85
+ foundWin = wins[w];
86
+ foundProc = proc;
87
+ break;
88
+ }
89
+ }
90
+ } catch(e) {}
91
+ if (foundWin) break;
92
+ }
93
+ }
94
+ if (!foundWin) {
95
+ result.error = 'Window not found';
96
+ } else {
97
+
98
+ var winPos = foundWin.position();
99
+ var winSize = foundWin.size();
100
+ result.window = {
101
+ id: String(${windowIdLiteral}),
102
+ title: foundWin.name() || '',
103
+ processName: foundProc.name() || '',
104
+ pid: foundProc.unixId ? foundProc.unixId() : 0,
105
+ bounds: {x: winPos[0] || 0, y: winPos[1] || 0, width: winSize[0] || 0, height: winSize[1] || 0},
106
+ isMinimized: false,
107
+ isOnScreen: true
108
+ };
109
+
110
+ var elemCount = [0];
111
+ function summarizeFocusedElement(info) {
112
+ var summary = {
113
+ role: info.role || '',
114
+ name: info.name || '',
115
+ value: info.value || '',
116
+ states: info.states ? info.states.slice(0) : []
117
+ };
118
+ if (includeBounds && info.bounds) summary.bounds = info.bounds;
119
+ return summary;
120
+ }
121
+
122
+ function getElementBounds(axElem) {
123
+ try {
124
+ var pos = axElem.position();
125
+ var sz = axElem.size();
126
+ return {x: pos[0]||0, y: pos[1]||0, width: sz[0]||0, height: sz[1]||0};
127
+ } catch(e) {
128
+ return null;
129
+ }
130
+ }
131
+
132
+ function elementBelongsToWindow(axElem) {
133
+ var b = getElementBounds(axElem);
134
+ if (!b) return false;
135
+ var wx = winPos[0] || 0;
136
+ var wy = winPos[1] || 0;
137
+ var ww = winSize[0] || 0;
138
+ var wh = winSize[1] || 0;
139
+ var cx = b.x + b.width / 2;
140
+ var cy = b.y + b.height / 2;
141
+ return cx >= wx && cx <= wx + ww && cy >= wy && cy <= wy + wh;
142
+ }
143
+
144
+ function readElementInfo(axElem) {
145
+ var info = {role: '', name: '', value: '', states: [], children: []};
146
+ try { info.role = axElem.role() || ''; } catch(e) {}
147
+ try { info.name = axElem.description ? axElem.description() : (axElem.name ? axElem.name() : ''); } catch(e) {}
148
+ try {
149
+ var val = axElem.value();
150
+ info.value = (val !== undefined && val !== null) ? String(val) : '';
151
+ } catch(e) {}
152
+ if (includeBounds) {
153
+ info.bounds = getElementBounds(axElem) || {x: 0, y: 0, width: 0, height: 0};
154
+ }
155
+ return info;
156
+ }
157
+
158
+ try {
159
+ var processFocused = foundProc.focusedUIElement ? foundProc.focusedUIElement() : null;
160
+ if (processFocused && elementBelongsToWindow(processFocused)) {
161
+ var focusedInfo = readElementInfo(processFocused);
162
+ focusedInfo.states.push('focused');
163
+ result.focusedElement = summarizeFocusedElement(focusedInfo);
164
+ }
165
+ } catch(e) {}
166
+
167
+ function extractElement(axElem, currentDepth) {
168
+ if (elemCount[0] >= ${maxElements}) return null;
169
+ elemCount[0]++;
170
+ var info = readElementInfo(axElem);
171
+ try {
172
+ try {
173
+ if (axElem.focused && axElem.focused()) info.states.push('focused');
174
+ } catch(e0) {}
175
+ } catch(e) {}
176
+ if (!result.focusedElement && info.states.indexOf('focused') !== -1) {
177
+ result.focusedElement = summarizeFocusedElement(info);
178
+ }
179
+
180
+ if (currentDepth < ${maxDepth}) {
181
+ try {
182
+ var kids = childElements(axElem);
183
+ for (var k = 0; k < kids.length && elemCount[0] < ${maxElements}; k++) {
184
+ var child = extractElement(kids[k], currentDepth + 1);
185
+ if (child) info.children.push(child);
186
+ }
187
+ } catch(e) {}
188
+ }
189
+ return info;
190
+ }
191
+
192
+ result.tree = extractElement(foundWin, 0);
193
+ }
194
+ } catch(e) {
195
+ result.error = String(e.message || e);
196
+ }
197
+ JSON.stringify(result);
198
+ `;
199
+ const out = execFileSync("osascript", [
200
+ "-l", "JavaScript",
201
+ "-e", jxaScript,
202
+ ], { encoding: "utf-8", timeout: 15000 }).trim();
203
+ const parsed = JSON.parse(out);
204
+ if (parsed.error && !parsed.window) {
205
+ throw new WindowNotFoundError(resolvedWindowId);
206
+ }
207
+ const windowInfo = parsed.window || {
208
+ id: resolvedWindowId,
209
+ title: "",
210
+ processName: "",
211
+ pid: 0,
212
+ bounds: { x: 0, y: 0, width: 0, height: 0 },
213
+ isMinimized: false,
214
+ isOnScreen: true,
215
+ };
216
+ return {
217
+ window: windowInfo,
218
+ focusedElement: parsed.focusedElement || undefined,
219
+ tree: parsed.tree || undefined,
220
+ };
221
+ }
222
+ catch (error) {
223
+ if (error instanceof WindowNotFoundError)
224
+ throw error;
225
+ rethrowAccessibilityError(error, "get_window_state");
226
+ }
227
+ }
228
+ export async function findElement(options) {
229
+ this.evictExpiredCacheEntries();
230
+ const { text, role, app, depth, includeBounds = true, textMode = "contains", visibleOnly = false, value } = options;
231
+ const effectiveApp = app || this.activeTarget?.appName;
232
+ const maxDepth = Math.min(depth || 5, 10);
233
+ const maxResults = Math.min(Math.max(options.maxResults ?? 50, 1), 200);
234
+ const appLiteral = JSON.stringify(effectiveApp || "");
235
+ const textLiteral = text ? JSON.stringify(text) : "null";
236
+ const roleLiteral = role ? JSON.stringify(role) : "null";
237
+ const valueLiteral = value ? JSON.stringify(value) : "null";
238
+ if (text && textMode === "regex") {
239
+ try {
240
+ new RegExp(text);
241
+ }
242
+ catch {
243
+ throw new PlatformError(`Invalid regex pattern: ${text}`);
244
+ }
245
+ }
246
+ if (value && textMode === "regex") {
247
+ try {
248
+ new RegExp(value);
249
+ }
250
+ catch {
251
+ throw new PlatformError(`Invalid regex pattern: ${value}`);
252
+ }
253
+ }
254
+ const startTime = Date.now();
255
+ const jxaScript = `
256
+ var se = Application('System Events');
257
+ ${jxaChildElements()}
258
+ var results = [];
259
+ var scannedCount = 0;
260
+ var matchedCount = 0;
261
+ var resultCount = [0];
262
+ var maxResults = ${maxResults};
263
+ var includeBounds = ${includeBounds ? "true" : "false"};
264
+ var visibleOnly = ${visibleOnly ? "true" : "false"};
265
+ var textMode = ${JSON.stringify(textMode)};
266
+
267
+ var textFilter = ${textLiteral};
268
+ var roleFilter = ${roleLiteral};
269
+ var valueFilter = ${valueLiteral};
270
+
271
+ ${jxaIsVisible()}
272
+
273
+ function matchesValue(filter, value, mode) {
274
+ if (filter === null) return true;
275
+ if (mode === "exact") {
276
+ return value.toLowerCase() === filter.toLowerCase();
277
+ } else if (mode === "regex") {
278
+ try {
279
+ return new RegExp(filter, "i").test(value);
280
+ } catch(e) { return false; }
281
+ } else {
282
+ return value.toLowerCase().indexOf(filter.toLowerCase()) !== -1;
283
+ }
284
+ }
285
+
286
+ function textMatches(elemName, elemValue, elemDesc) {
287
+ if (textFilter === null) return true;
288
+ var sources = [elemName, elemValue, elemDesc];
289
+ if (textMode === "regex") {
290
+ var reText = new RegExp(textFilter, "i");
291
+ for (var i = 0; i < sources.length; i++) {
292
+ if (reText.test(sources[i])) return true;
293
+ }
294
+ return false;
295
+ }
296
+ for (var j = 0; j < sources.length; j++) {
297
+ if (matchesValue(textFilter, sources[j], textMode)) return true;
298
+ }
299
+ return false;
300
+ }
301
+
302
+ function valueMatches(elemValue) {
303
+ return matchesValue(valueFilter, elemValue, textMode);
304
+ }
305
+
306
+ function matches(elem) {
307
+ scannedCount++;
308
+ var elemName = '';
309
+ var elemRole = '';
310
+ var elemDesc = '';
311
+ var elemValue = '';
312
+ try { elemName = elem.name() || ''; } catch(e) {}
313
+ try { elemRole = elem.role() || ''; } catch(e) {}
314
+ try { elemDesc = elem.description() || ''; } catch(e) {}
315
+ try { var v = elem.value(); elemValue = (v !== undefined && v !== null) ? String(v) : ''; } catch(e) {}
316
+
317
+ if (visibleOnly && !isVisible(elem)) return false;
318
+
319
+ if (!textMatches(elemName, elemValue, elemDesc)) return false;
320
+ if (roleFilter !== null) {
321
+ if (elemRole !== roleFilter) return false;
322
+ }
323
+ if (!valueMatches(elemValue)) return false;
324
+ matchedCount++;
325
+ return true;
326
+ }
327
+
328
+ ${jxaGetBounds()}
329
+
330
+ function traverse(elem, path, currentDepth) {
331
+ if (resultCount[0] >= maxResults) return;
332
+ if (currentDepth > ${maxDepth}) return;
333
+
334
+ if (matches(elem)) {
335
+ var item = {
336
+ id: path,
337
+ role: '',
338
+ name: '',
339
+ value: undefined,
340
+ description: undefined,
341
+ subrole: undefined,
342
+ identifier: undefined
343
+ };
344
+ var elemName = '';
345
+ var elemRole = '';
346
+ var elemDesc = '';
347
+ var elemValue = '';
348
+ var elemSubrole = '';
349
+ var elemIdentifier = '';
350
+ try { elemName = elem.name() || ''; } catch(e) {}
351
+ try { elemRole = elem.role() || ''; } catch(e) {}
352
+ try { elemDesc = elem.description() || ''; } catch(e) {}
353
+ try { var v = elem.value(); elemValue = (v !== undefined && v !== null) ? String(v) : ''; } catch(e) {}
354
+ try { elemSubrole = elem.subrole() || ''; } catch(e) {}
355
+ try { elemIdentifier = elem.identifier() || ''; } catch(e) {}
356
+
357
+ item.role = elemRole;
358
+ item.name = elemName;
359
+ if (elemValue) item.value = elemValue;
360
+ if (elemDesc) item.description = elemDesc;
361
+ if (elemSubrole) item.subrole = elemSubrole;
362
+ if (elemIdentifier) item.identifier = elemIdentifier;
363
+ if (includeBounds) item.bounds = getBounds(elem);
364
+ results.push(item);
365
+ resultCount[0]++;
366
+ }
367
+
368
+ if (currentDepth < ${maxDepth}) {
369
+ try {
370
+ var kids = childElements(elem);
371
+ for (var k = 0; k < kids.length && resultCount[0] < maxResults; k++) {
372
+ traverse(kids[k], path + '/' + k, currentDepth + 1);
373
+ }
374
+ } catch(e) {}
375
+ }
376
+ }
377
+
378
+ try {
379
+ if (${appLiteral}) {
380
+ var proc = se.processes[${appLiteral}]();
381
+ var wins = proc.windows();
382
+ for (var w = 0; w < wins.length && resultCount[0] < maxResults; w++) {
383
+ traverse(wins[w], ${appLiteral} + "/win" + w, 0);
384
+ }
385
+ } else {
386
+ var procs = se.processes();
387
+ for (var p = 0; p < procs.length && resultCount[0] < maxResults; p++) {
388
+ try {
389
+ var procName = procs[p].name();
390
+ var wins = procs[p].windows();
391
+ for (var w = 0; w < wins.length && resultCount[0] < maxResults; w++) {
392
+ traverse(wins[w], procName + "/win" + w, 0);
393
+ }
394
+ } catch(e) {}
395
+ }
396
+ }
397
+ } catch(e) {}
398
+
399
+ JSON.stringify({results: results, scannedCount: scannedCount, matchedCount: matchedCount});
400
+ `;
401
+ try {
402
+ const out = execFileSync("osascript", [
403
+ "-l", "JavaScript",
404
+ "-e", jxaScript,
405
+ ], { encoding: "utf-8", timeout: 30000 }).trim();
406
+ const parsed = JSON.parse(out);
407
+ const durationMs = Date.now() - startTime;
408
+ for (const result of parsed.results) {
409
+ const appName = effectiveApp || result.id.split("/")[0] || "";
410
+ this.elementCache.set(result.id, {
411
+ elementId: result.id,
412
+ appName,
413
+ role: result.role,
414
+ name: result.name,
415
+ value: result.value,
416
+ description: result.description,
417
+ subrole: result.subrole,
418
+ identifier: result.identifier,
419
+ bounds: result.bounds,
420
+ cachedAt: Date.now(),
421
+ });
422
+ }
423
+ this.evictOverflowCacheEntries();
424
+ let finalResults = parsed.results;
425
+ if (options.near) {
426
+ const nx = options.near.x;
427
+ const ny = options.near.y;
428
+ finalResults = [...finalResults].sort((a, b) => {
429
+ const aHasBounds = !!a.bounds;
430
+ const bHasBounds = !!b.bounds;
431
+ if (!aHasBounds && !bHasBounds)
432
+ return 0;
433
+ if (!aHasBounds)
434
+ return 1;
435
+ if (!bHasBounds)
436
+ return -1;
437
+ const acx = (a.bounds?.x ?? 0) + (a.bounds?.width ?? 0) / 2;
438
+ const acy = (a.bounds?.y ?? 0) + (a.bounds?.height ?? 0) / 2;
439
+ const bcx = (b.bounds?.x ?? 0) + (b.bounds?.width ?? 0) / 2;
440
+ const bcy = (b.bounds?.y ?? 0) + (b.bounds?.height ?? 0) / 2;
441
+ return Math.hypot(acx - nx, acy - ny) - Math.hypot(bcx - nx, bcy - ny);
442
+ });
443
+ }
444
+ if (typeof options.index === "number") {
445
+ finalResults = options.index >= 0 && options.index < finalResults.length
446
+ ? [finalResults[options.index]]
447
+ : [];
448
+ }
449
+ return {
450
+ results: finalResults,
451
+ metrics: {
452
+ scannedCount: parsed.scannedCount,
453
+ matchedCount: parsed.matchedCount,
454
+ durationMs,
455
+ truncated: parsed.results.length >= maxResults,
456
+ },
457
+ };
458
+ }
459
+ catch (error) {
460
+ rethrowAccessibilityError(error, "find_element");
461
+ }
462
+ }
@@ -0,0 +1,57 @@
1
+ import type { Platform, WindowInfo, AppTarget } from "../base.js";
2
+ import type { CachedElementDescriptor, MacOSPlatformOptions } from "./helpers.js";
3
+ import { saveFocus, restoreFocus } from "./focus.js";
4
+ import { screenshot, screenshotWindow, getScreenSize, isScreenLocked, ocr } from "./screen.js";
5
+ import { listApps, focusApp, getActiveBrowserContext, listWindows } from "./window.js";
6
+ import { getWindowState, findElement } from "./ax-tree.js";
7
+ import { click, move, drag, scroll, getCursorPosition, type as typeMethod, key } from "./input.js";
8
+ import { clickElement, typeInElement, setElementValue } from "./element.js";
9
+ import { readClipboard, writeClipboard } from "./clipboard.js";
10
+ export type { MacOSPlatformOptions } from "./helpers.js";
11
+ export declare class MacOSPlatform implements Platform {
12
+ readonly _nativeHelperPaths: Record<string, string | null> | undefined;
13
+ readonly elementCache: Map<string, CachedElementDescriptor>;
14
+ readonly elementCacheTtlMs = 30000;
15
+ readonly elementCacheMaxSize = 100;
16
+ readonly windowCacheTtlMs = 300;
17
+ windowCache: {
18
+ cachedAt: number;
19
+ windows: WindowInfo[];
20
+ } | undefined;
21
+ windowCacheInFlight: boolean;
22
+ activeTarget: AppTarget | undefined;
23
+ savedFocus: {
24
+ appName: string;
25
+ windowTitle: string;
26
+ } | undefined;
27
+ constructor(options?: MacOSPlatformOptions);
28
+ evictExpiredCacheEntries(): void;
29
+ evictOverflowCacheEntries(): void;
30
+ isCacheEntryExpired(descriptor: CachedElementDescriptor): boolean;
31
+ validateActiveTarget(): Promise<void>;
32
+ saveFocus: typeof saveFocus;
33
+ restoreFocus: typeof restoreFocus;
34
+ screenshot: typeof screenshot;
35
+ screenshotWindow: typeof screenshotWindow;
36
+ getScreenSize: typeof getScreenSize;
37
+ isScreenLocked: typeof isScreenLocked;
38
+ ocr: typeof ocr;
39
+ listApps: typeof listApps;
40
+ focusApp: typeof focusApp;
41
+ getActiveBrowserContext: typeof getActiveBrowserContext;
42
+ listWindows: typeof listWindows;
43
+ getWindowState: typeof getWindowState;
44
+ findElement: typeof findElement;
45
+ click: typeof click;
46
+ move: typeof move;
47
+ drag: typeof drag;
48
+ scroll: typeof scroll;
49
+ getCursorPosition: typeof getCursorPosition;
50
+ type: typeof typeMethod;
51
+ key: typeof key;
52
+ clickElement: typeof clickElement;
53
+ typeInElement: typeof typeInElement;
54
+ setElementValue: typeof setElementValue;
55
+ readClipboard: typeof readClipboard;
56
+ writeClipboard: typeof writeClipboard;
57
+ }
@@ -0,0 +1,92 @@
1
+ import { TargetStaleError } from "../../util/errors.js";
2
+ import { saveFocus, restoreFocus } from "./focus.js";
3
+ import { screenshot, screenshotWindow, getScreenSize, isScreenLocked, ocr } from "./screen.js";
4
+ import { listApps, focusApp, getActiveBrowserContext, listWindows } from "./window.js";
5
+ import { getWindowState, findElement } from "./ax-tree.js";
6
+ import { click, move, drag, scroll, getCursorPosition, type as typeMethod, key } from "./input.js";
7
+ import { clickElement, typeInElement, setElementValue } from "./element.js";
8
+ import { readClipboard, writeClipboard } from "./clipboard.js";
9
+ export class MacOSPlatform {
10
+ _nativeHelperPaths;
11
+ elementCache = new Map();
12
+ elementCacheTtlMs = 30_000;
13
+ elementCacheMaxSize = 100;
14
+ windowCacheTtlMs = 300;
15
+ windowCache;
16
+ windowCacheInFlight = false;
17
+ activeTarget;
18
+ savedFocus;
19
+ constructor(options) {
20
+ this._nativeHelperPaths = options?.nativeHelperPaths;
21
+ }
22
+ // ── Element Cache Management ────────────────────────────────────────────
23
+ evictExpiredCacheEntries() {
24
+ const now = Date.now();
25
+ for (const [key, descriptor] of this.elementCache) {
26
+ if (now - descriptor.cachedAt > this.elementCacheTtlMs) {
27
+ this.elementCache.delete(key);
28
+ }
29
+ }
30
+ }
31
+ evictOverflowCacheEntries() {
32
+ while (this.elementCache.size > this.elementCacheMaxSize) {
33
+ let oldestKey = null;
34
+ let oldestTime = Infinity;
35
+ for (const [key, descriptor] of this.elementCache) {
36
+ if (descriptor.cachedAt < oldestTime) {
37
+ oldestTime = descriptor.cachedAt;
38
+ oldestKey = key;
39
+ }
40
+ }
41
+ if (oldestKey !== null) {
42
+ this.elementCache.delete(oldestKey);
43
+ }
44
+ else {
45
+ break;
46
+ }
47
+ }
48
+ }
49
+ isCacheEntryExpired(descriptor) {
50
+ return Date.now() - descriptor.cachedAt > this.elementCacheTtlMs;
51
+ }
52
+ // ── Target Validation ────────────────────────────────────────────────────
53
+ async validateActiveTarget() {
54
+ if (!this.activeTarget?.windowId)
55
+ return;
56
+ this.windowCache = undefined;
57
+ const windows = await this.listWindows(true);
58
+ const match = windows.find(w => w.id === this.activeTarget.windowId);
59
+ if (!match) {
60
+ throw new TargetStaleError(this.activeTarget.windowId);
61
+ }
62
+ if (match.pid !== this.activeTarget.pid) {
63
+ throw new TargetStaleError(this.activeTarget.windowId);
64
+ }
65
+ }
66
+ // ── Bound methods from domain modules ────────────────────────────────────
67
+ saveFocus = saveFocus;
68
+ restoreFocus = restoreFocus;
69
+ screenshot = screenshot;
70
+ screenshotWindow = screenshotWindow;
71
+ getScreenSize = getScreenSize;
72
+ isScreenLocked = isScreenLocked;
73
+ ocr = ocr;
74
+ listApps = listApps;
75
+ focusApp = focusApp;
76
+ getActiveBrowserContext = getActiveBrowserContext;
77
+ listWindows = listWindows;
78
+ getWindowState = getWindowState;
79
+ findElement = findElement;
80
+ click = click;
81
+ move = move;
82
+ drag = drag;
83
+ scroll = scroll;
84
+ getCursorPosition = getCursorPosition;
85
+ type = typeMethod;
86
+ key = key;
87
+ clickElement = clickElement;
88
+ typeInElement = typeInElement;
89
+ setElementValue = setElementValue;
90
+ readClipboard = readClipboard;
91
+ writeClipboard = writeClipboard;
92
+ }
@@ -0,0 +1,3 @@
1
+ import type { MacOSPlatform } from "./base.js";
2
+ export declare function readClipboard(this: MacOSPlatform): Promise<string>;
3
+ export declare function writeClipboard(this: MacOSPlatform, text: string): Promise<void>;
@@ -0,0 +1,20 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { PlatformError } from "../../util/errors.js";
3
+ import { errorMessage } from "./helpers.js";
4
+ export async function readClipboard() {
5
+ try {
6
+ const out = execFileSync("pbpaste", [], { encoding: "utf-8", timeout: 5000 });
7
+ return out;
8
+ }
9
+ catch (error) {
10
+ throw new PlatformError(`read_clipboard failed: ${errorMessage(error)}`);
11
+ }
12
+ }
13
+ export async function writeClipboard(text) {
14
+ try {
15
+ execFileSync("pbcopy", [], { input: text, encoding: "utf-8", timeout: 5000 });
16
+ }
17
+ catch (error) {
18
+ throw new PlatformError(`write_clipboard failed: ${errorMessage(error)}`);
19
+ }
20
+ }
@@ -0,0 +1,4 @@
1
+ import type { MacOSPlatform } from "./base.js";
2
+ export declare function clickElement(this: MacOSPlatform, elementId: string, app?: string): Promise<void>;
3
+ export declare function typeInElement(this: MacOSPlatform, elementId: string, text: string, app?: string, clearFirst?: boolean): Promise<void>;
4
+ export declare function setElementValue(this: MacOSPlatform, elementId: string, value: string, app?: string): Promise<void>;