ucu-mcp 0.2.0 → 0.3.1
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 +31 -53
- package/README.md +90 -4
- package/dist/src/mcp/server.js +11 -6
- package/dist/src/mcp/tools.d.ts +6 -1
- package/dist/src/mcp/tools.js +219 -46
- package/dist/src/platform/base.d.ts +26 -1
- package/dist/src/platform/linux.d.ts +4 -2
- package/dist/src/platform/linux.js +51 -0
- package/dist/src/platform/macos.d.ts +6 -2
- package/dist/src/platform/macos.js +160 -16
- package/dist/src/platform/windows.d.ts +4 -2
- package/dist/src/platform/windows.js +33 -0
- package/dist/src/safety/guard.d.ts +8 -1
- package/dist/src/safety/guard.js +43 -4
- package/dist/src/util/errors.d.ts +26 -1
- package/dist/src/util/errors.js +43 -11
- package/dist/src/util/metrics.d.ts +37 -0
- package/dist/src/util/metrics.js +97 -0
- package/native/cgevent/cgevent-helper +0 -0
- package/native/ocr/ocr-helper +0 -0
- package/package.json +2 -2
|
@@ -3,7 +3,7 @@ import { randomUUID } from "node:crypto";
|
|
|
3
3
|
import { promisify } from "node:util";
|
|
4
4
|
import { captureFullScreen, captureRegion } from "../utils/screenshot.js";
|
|
5
5
|
import { click as inputClick, doubleClick as inputDoubleClick, move as inputMove, drag as inputDrag, scroll as inputScroll, typeText, pressShortcut } from "../utils/input.js";
|
|
6
|
-
import { CaptureError, ElementNotFoundError, InputSynthesisError, PermissionError, PlatformError, UcuError, WindowNotFoundError } from "../util/errors.js";
|
|
6
|
+
import { CaptureError, ElementNotFoundError, InputSynthesisError, PermissionError, PlatformError, TargetStaleError, UcuError, WindowNotFoundError } from "../util/errors.js";
|
|
7
7
|
const execFileAsync = promisify(execFile);
|
|
8
8
|
function errorMessage(error) {
|
|
9
9
|
return error instanceof Error ? error.message : String(error);
|
|
@@ -40,6 +40,24 @@ function rethrowInputError(error, operation) {
|
|
|
40
40
|
throw error;
|
|
41
41
|
throw new InputSynthesisError(`${operation} failed: ${errorMessage(error)}`);
|
|
42
42
|
}
|
|
43
|
+
function normalizeAppName(name) {
|
|
44
|
+
return name.trim().toLowerCase();
|
|
45
|
+
}
|
|
46
|
+
function appNameMatches(processName, requestedApp) {
|
|
47
|
+
const process = normalizeAppName(processName);
|
|
48
|
+
const requested = normalizeAppName(requestedApp);
|
|
49
|
+
if (!process || !requested)
|
|
50
|
+
return false;
|
|
51
|
+
return process === requested ||
|
|
52
|
+
process.startsWith(`${requested} `) ||
|
|
53
|
+
process.startsWith(`${requested}-`) ||
|
|
54
|
+
process.includes(` ${requested} `);
|
|
55
|
+
}
|
|
56
|
+
function selectWindowForApp(windows, requestedApp) {
|
|
57
|
+
const requested = normalizeAppName(requestedApp);
|
|
58
|
+
return windows.find((window) => normalizeAppName(window.processName) === requested) ??
|
|
59
|
+
windows.find((window) => appNameMatches(window.processName, requestedApp));
|
|
60
|
+
}
|
|
43
61
|
export class MacOSPlatform {
|
|
44
62
|
elementCache = new Map();
|
|
45
63
|
elementCacheTtlMs = 30_000;
|
|
@@ -81,6 +99,18 @@ export class MacOSPlatform {
|
|
|
81
99
|
isCacheEntryExpired(descriptor) {
|
|
82
100
|
return Date.now() - descriptor.cachedAt > this.elementCacheTtlMs;
|
|
83
101
|
}
|
|
102
|
+
// ── Target Validation ────────────────────────────────────────────────────
|
|
103
|
+
/** Validate that the active target window still exists. */
|
|
104
|
+
async validateActiveTarget() {
|
|
105
|
+
if (!this.activeTarget?.windowId)
|
|
106
|
+
return;
|
|
107
|
+
this.windowCache = undefined; // Bypass cache — stale detection must use fresh data
|
|
108
|
+
const windows = await this.listWindows(true);
|
|
109
|
+
const stillExists = windows.some(w => w.id === this.activeTarget.windowId);
|
|
110
|
+
if (!stillExists) {
|
|
111
|
+
throw new TargetStaleError(this.activeTarget.windowId);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
84
114
|
// ── Focus Management ────────────────────────────────────────────────────
|
|
85
115
|
/** Save the current frontmost app/window so we can restore after an action. */
|
|
86
116
|
async saveFocus() {
|
|
@@ -199,7 +229,6 @@ export class MacOSPlatform {
|
|
|
199
229
|
return JSON.parse(out);
|
|
200
230
|
}
|
|
201
231
|
async focusApp(app) {
|
|
202
|
-
const appLower = app.toLowerCase();
|
|
203
232
|
const escapedApp = app.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
204
233
|
this.windowCache = undefined;
|
|
205
234
|
try {
|
|
@@ -213,7 +242,7 @@ export class MacOSPlatform {
|
|
|
213
242
|
const deadline = Date.now() + 3000;
|
|
214
243
|
do {
|
|
215
244
|
const windows = await this.listWindows(true);
|
|
216
|
-
target = windows
|
|
245
|
+
target = selectWindowForApp(windows, app);
|
|
217
246
|
if (target)
|
|
218
247
|
break;
|
|
219
248
|
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
@@ -222,10 +251,12 @@ export class MacOSPlatform {
|
|
|
222
251
|
throw new WindowNotFoundError(app);
|
|
223
252
|
}
|
|
224
253
|
this.activeTarget = {
|
|
254
|
+
targetId: randomUUID(),
|
|
225
255
|
appName: target.processName,
|
|
226
256
|
pid: target.pid,
|
|
227
257
|
windowId: target.id,
|
|
228
258
|
title: target.title,
|
|
259
|
+
capturedAt: new Date().toISOString(),
|
|
229
260
|
};
|
|
230
261
|
return this.activeTarget;
|
|
231
262
|
}
|
|
@@ -347,6 +378,9 @@ export class MacOSPlatform {
|
|
|
347
378
|
}
|
|
348
379
|
}
|
|
349
380
|
async getWindowState(windowId, depth, includeBounds = true) {
|
|
381
|
+
if (!windowId || windowId === this.activeTarget?.windowId) {
|
|
382
|
+
await this.validateActiveTarget();
|
|
383
|
+
}
|
|
350
384
|
const resolvedWindowId = windowId || this.activeTarget?.windowId;
|
|
351
385
|
if (!resolvedWindowId) {
|
|
352
386
|
throw new WindowNotFoundError("active target");
|
|
@@ -769,13 +803,24 @@ export class MacOSPlatform {
|
|
|
769
803
|
// ── Accessibility (AX) Element Actions ───────────────────────────────────
|
|
770
804
|
async findElement(options) {
|
|
771
805
|
this.evictExpiredCacheEntries();
|
|
772
|
-
const { text, role, app, depth, includeBounds = true } = options;
|
|
806
|
+
const { text, role, app, depth, includeBounds = true, textMode = "contains", visibleOnly = false, value } = options;
|
|
773
807
|
const effectiveApp = app || this.activeTarget?.appName;
|
|
774
808
|
const maxDepth = Math.min(depth || 5, 10);
|
|
775
809
|
const maxResults = Math.min(Math.max(options.maxResults ?? 50, 1), 200);
|
|
776
810
|
const escapedApp = (effectiveApp || "").replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/`/g, '\\`').replace(/\$/g, '\\$');
|
|
777
811
|
const escapedText = text ? text.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/`/g, '\\`').replace(/\$/g, '\\$') : "";
|
|
778
812
|
const escapedRole = role ? role.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/`/g, '\\`').replace(/\$/g, '\\$') : "";
|
|
813
|
+
const escapedValue = value ? value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/`/g, '\\`').replace(/\$/g, '\\$') : "";
|
|
814
|
+
// Pre-compile regex on TS side to validate syntax before passing to JXA
|
|
815
|
+
if (text && textMode === "regex") {
|
|
816
|
+
try {
|
|
817
|
+
new RegExp(text);
|
|
818
|
+
}
|
|
819
|
+
catch {
|
|
820
|
+
throw new PlatformError(`Invalid regex pattern: ${text}`);
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
const startTime = Date.now();
|
|
779
824
|
const jxaScript = `
|
|
780
825
|
var se = Application('System Events');
|
|
781
826
|
function childElements(elem) {
|
|
@@ -784,14 +829,72 @@ export class MacOSPlatform {
|
|
|
784
829
|
}
|
|
785
830
|
}
|
|
786
831
|
var results = [];
|
|
832
|
+
var scannedCount = 0;
|
|
833
|
+
var matchedCount = 0;
|
|
787
834
|
var resultCount = [0];
|
|
788
835
|
var maxResults = ${maxResults};
|
|
789
836
|
var includeBounds = ${includeBounds ? "true" : "false"};
|
|
837
|
+
var visibleOnly = ${visibleOnly ? "true" : "false"};
|
|
838
|
+
var textMode = "${textMode}";
|
|
790
839
|
|
|
791
840
|
var textFilter = ${text ? `"${escapedText}"` : "null"};
|
|
792
841
|
var roleFilter = ${role ? `"${escapedRole}"` : "null"};
|
|
842
|
+
var valueFilter = ${value ? `"${escapedValue}"` : "null"};
|
|
843
|
+
|
|
844
|
+
function isVisible(elem) {
|
|
845
|
+
try {
|
|
846
|
+
var pos = elem.position();
|
|
847
|
+
var sz = elem.size();
|
|
848
|
+
if (!pos || !sz) return false;
|
|
849
|
+
return sz[0] > 0 && sz[1] > 0 && pos[0] > -10000 && pos[1] > -10000;
|
|
850
|
+
} catch(e) {
|
|
851
|
+
return false;
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
function textMatches(elemName, elemValue, elemDesc) {
|
|
856
|
+
if (textFilter === null) return true;
|
|
857
|
+
var sources = [elemName, elemValue, elemDesc];
|
|
858
|
+
if (textMode === "exact") {
|
|
859
|
+
var t = textFilter.toLowerCase();
|
|
860
|
+
for (var i = 0; i < sources.length; i++) {
|
|
861
|
+
if (sources[i].toLowerCase() === t) return true;
|
|
862
|
+
}
|
|
863
|
+
return false;
|
|
864
|
+
} else if (textMode === "regex") {
|
|
865
|
+
try {
|
|
866
|
+
var re = new RegExp(textFilter, "i");
|
|
867
|
+
for (var i = 0; i < sources.length; i++) {
|
|
868
|
+
if (re.test(sources[i])) return true;
|
|
869
|
+
}
|
|
870
|
+
} catch(e) {}
|
|
871
|
+
return false;
|
|
872
|
+
} else {
|
|
873
|
+
// contains (default)
|
|
874
|
+
var t = textFilter.toLowerCase();
|
|
875
|
+
for (var i = 0; i < sources.length; i++) {
|
|
876
|
+
if (sources[i].toLowerCase().indexOf(t) !== -1) return true;
|
|
877
|
+
}
|
|
878
|
+
return false;
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
function valueMatches(elemValue) {
|
|
883
|
+
if (valueFilter === null) return true;
|
|
884
|
+
if (textMode === "exact") {
|
|
885
|
+
return elemValue.toLowerCase() === valueFilter.toLowerCase();
|
|
886
|
+
} else if (textMode === "regex") {
|
|
887
|
+
try {
|
|
888
|
+
return new RegExp(valueFilter, "i").test(elemValue);
|
|
889
|
+
} catch(e) { return false; }
|
|
890
|
+
} else {
|
|
891
|
+
// contains (default)
|
|
892
|
+
return elemValue.toLowerCase().indexOf(valueFilter.toLowerCase()) !== -1;
|
|
893
|
+
}
|
|
894
|
+
}
|
|
793
895
|
|
|
794
896
|
function matches(elem) {
|
|
897
|
+
scannedCount++;
|
|
795
898
|
var elemName = '';
|
|
796
899
|
var elemRole = '';
|
|
797
900
|
var elemDesc = '';
|
|
@@ -801,17 +904,14 @@ export class MacOSPlatform {
|
|
|
801
904
|
try { elemDesc = elem.description() || ''; } catch(e) {}
|
|
802
905
|
try { var v = elem.value(); elemValue = (v !== undefined && v !== null) ? String(v) : ''; } catch(e) {}
|
|
803
906
|
|
|
804
|
-
if (
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
elemValue.toLowerCase().indexOf(t) === -1 &&
|
|
808
|
-
elemDesc.toLowerCase().indexOf(t) === -1) {
|
|
809
|
-
return false;
|
|
810
|
-
}
|
|
811
|
-
}
|
|
907
|
+
if (visibleOnly && !isVisible(elem)) return false;
|
|
908
|
+
|
|
909
|
+
if (!textMatches(elemName, elemValue, elemDesc)) return false;
|
|
812
910
|
if (roleFilter !== null) {
|
|
813
911
|
if (elemRole !== roleFilter) return false;
|
|
814
912
|
}
|
|
913
|
+
if (!valueMatches(elemValue)) return false;
|
|
914
|
+
matchedCount++;
|
|
815
915
|
return true;
|
|
816
916
|
}
|
|
817
917
|
|
|
@@ -894,15 +994,16 @@ export class MacOSPlatform {
|
|
|
894
994
|
}
|
|
895
995
|
} catch(e) {}
|
|
896
996
|
|
|
897
|
-
JSON.stringify(results);
|
|
997
|
+
JSON.stringify({results: results, scannedCount: scannedCount, matchedCount: matchedCount});
|
|
898
998
|
`;
|
|
899
999
|
try {
|
|
900
1000
|
const out = execFileSync("osascript", [
|
|
901
1001
|
"-l", "JavaScript",
|
|
902
1002
|
"-e", jxaScript,
|
|
903
1003
|
], { encoding: "utf-8", timeout: 30000 }).trim();
|
|
904
|
-
const
|
|
905
|
-
|
|
1004
|
+
const parsed = JSON.parse(out);
|
|
1005
|
+
const durationMs = Date.now() - startTime;
|
|
1006
|
+
for (const result of parsed.results) {
|
|
906
1007
|
const appName = effectiveApp || result.id.split("/")[0] || "";
|
|
907
1008
|
this.elementCache.set(result.id, {
|
|
908
1009
|
elementId: result.id,
|
|
@@ -918,7 +1019,32 @@ export class MacOSPlatform {
|
|
|
918
1019
|
});
|
|
919
1020
|
}
|
|
920
1021
|
this.evictOverflowCacheEntries();
|
|
921
|
-
|
|
1022
|
+
let finalResults = parsed.results;
|
|
1023
|
+
if (options.near) {
|
|
1024
|
+
const nx = options.near.x;
|
|
1025
|
+
const ny = options.near.y;
|
|
1026
|
+
finalResults = [...finalResults].sort((a, b) => {
|
|
1027
|
+
const acx = (a.bounds?.x ?? 0) + (a.bounds?.width ?? 0) / 2;
|
|
1028
|
+
const acy = (a.bounds?.y ?? 0) + (a.bounds?.height ?? 0) / 2;
|
|
1029
|
+
const bcx = (b.bounds?.x ?? 0) + (b.bounds?.width ?? 0) / 2;
|
|
1030
|
+
const bcy = (b.bounds?.y ?? 0) + (b.bounds?.height ?? 0) / 2;
|
|
1031
|
+
return Math.hypot(acx - nx, acy - ny) - Math.hypot(bcx - nx, bcy - ny);
|
|
1032
|
+
});
|
|
1033
|
+
}
|
|
1034
|
+
if (typeof options.index === "number") {
|
|
1035
|
+
finalResults = options.index >= 0 && options.index < finalResults.length
|
|
1036
|
+
? [finalResults[options.index]]
|
|
1037
|
+
: [];
|
|
1038
|
+
}
|
|
1039
|
+
return {
|
|
1040
|
+
results: finalResults,
|
|
1041
|
+
metrics: {
|
|
1042
|
+
scannedCount: parsed.scannedCount,
|
|
1043
|
+
matchedCount: parsed.matchedCount,
|
|
1044
|
+
durationMs,
|
|
1045
|
+
truncated: parsed.results.length >= maxResults,
|
|
1046
|
+
},
|
|
1047
|
+
};
|
|
922
1048
|
}
|
|
923
1049
|
catch (error) {
|
|
924
1050
|
rethrowAccessibilityError(error, "find_element");
|
|
@@ -1397,6 +1523,24 @@ export class MacOSPlatform {
|
|
|
1397
1523
|
rethrowElementActionError(error, "type_in_element", elementId);
|
|
1398
1524
|
}
|
|
1399
1525
|
}
|
|
1526
|
+
// ── Clipboard ───────────────────────────────────────────────────────────
|
|
1527
|
+
async readClipboard() {
|
|
1528
|
+
try {
|
|
1529
|
+
const out = execFileSync("pbpaste", [], { encoding: "utf-8", timeout: 5000 });
|
|
1530
|
+
return out;
|
|
1531
|
+
}
|
|
1532
|
+
catch (error) {
|
|
1533
|
+
throw new PlatformError(`read_clipboard failed: ${errorMessage(error)}`);
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
async writeClipboard(text) {
|
|
1537
|
+
try {
|
|
1538
|
+
execFileSync("pbcopy", [], { input: text, encoding: "utf-8", timeout: 5000 });
|
|
1539
|
+
}
|
|
1540
|
+
catch (error) {
|
|
1541
|
+
throw new PlatformError(`write_clipboard failed: ${errorMessage(error)}`);
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1400
1544
|
async setElementValue(elementId, value, app) {
|
|
1401
1545
|
this.evictExpiredCacheEntries();
|
|
1402
1546
|
const effectiveApp = app || this.activeTarget?.appName;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Platform, ScreenRegion, ScreenSize, CursorPosition, WindowInfo, WindowState, OcrResult, FindElementOptions,
|
|
1
|
+
import type { Platform, ScreenRegion, ScreenSize, CursorPosition, WindowInfo, WindowState, OcrResult, FindElementOptions, FindElementResponse } from "./base.js";
|
|
2
2
|
export declare class WindowsPlatform implements Platform {
|
|
3
3
|
screenshot(_display?: number, _region?: ScreenRegion): Promise<Buffer>;
|
|
4
4
|
getScreenSize(_display?: number): ScreenSize;
|
|
@@ -12,7 +12,9 @@ export declare class WindowsPlatform implements Platform {
|
|
|
12
12
|
type(_text: string, _delay?: number): Promise<void>;
|
|
13
13
|
key(_keys: string[]): Promise<void>;
|
|
14
14
|
ocr(_display?: number, _region?: ScreenRegion): Promise<OcrResult>;
|
|
15
|
-
findElement(_options: FindElementOptions): Promise<
|
|
15
|
+
findElement(_options: FindElementOptions): Promise<FindElementResponse>;
|
|
16
16
|
clickElement(_elementId: string, _app?: string): Promise<void>;
|
|
17
17
|
typeInElement(_elementId: string, _text: string, _app?: string, _clearFirst?: boolean): Promise<void>;
|
|
18
|
+
readClipboard(): Promise<string>;
|
|
19
|
+
writeClipboard(text: string): Promise<void>;
|
|
18
20
|
}
|
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { PlatformError } from "../util/errors.js";
|
|
3
|
+
function runPowerShell(script, input) {
|
|
4
|
+
try {
|
|
5
|
+
return execFileSync("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script], {
|
|
6
|
+
encoding: "utf-8",
|
|
7
|
+
timeout: 10000,
|
|
8
|
+
...(input !== undefined ? { input } : {}),
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
catch (error) {
|
|
12
|
+
throw new PlatformError(`PowerShell failed: ${error.message}`);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
1
15
|
export class WindowsPlatform {
|
|
2
16
|
async screenshot(_display, _region) {
|
|
3
17
|
throw new Error("Not implemented: Windows screenshot");
|
|
@@ -45,4 +59,23 @@ export class WindowsPlatform {
|
|
|
45
59
|
async typeInElement(_elementId, _text, _app, _clearFirst) {
|
|
46
60
|
throw new Error("Not implemented: Windows typeInElement");
|
|
47
61
|
}
|
|
62
|
+
async readClipboard() {
|
|
63
|
+
try {
|
|
64
|
+
// Get-Clipboard returns the clipboard text; trim trailing newline PowerShell adds
|
|
65
|
+
const out = runPowerShell("Get-Clipboard -Raw");
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
throw new PlatformError(`read_clipboard failed: ${error.message}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
async writeClipboard(text) {
|
|
73
|
+
try {
|
|
74
|
+
// Pipe the text to Set-Clipboard via stdin to avoid shell quoting issues
|
|
75
|
+
runPowerShell("$stdin = [Console]::In.ReadToEnd(); Set-Clipboard -Value $stdin", text);
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
throw new PlatformError(`write_clipboard failed: ${error.message}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
48
81
|
}
|
|
@@ -22,6 +22,11 @@ export interface SafetyGuardConfig {
|
|
|
22
22
|
/** Minimum milliseconds between consecutive actions (default 100). */
|
|
23
23
|
rateLimitMs?: number;
|
|
24
24
|
}
|
|
25
|
+
/** Actions that observe UI/system state without altering it. */
|
|
26
|
+
export declare const OBSERVE_ACTIONS: ReadonlySet<string>;
|
|
27
|
+
/** Actions that synthesize user input — need full user-activity protection. */
|
|
28
|
+
export declare const INPUT_ACTIONS: ReadonlySet<string>;
|
|
29
|
+
export declare function classifyAction(action: string): "observe" | "input" | "other";
|
|
25
30
|
export declare class SafetyGuard {
|
|
26
31
|
private readonly blockedKeys;
|
|
27
32
|
private readonly skippedWindows;
|
|
@@ -40,7 +45,9 @@ export declare class SafetyGuard {
|
|
|
40
45
|
* - "key": { keys: string[] }
|
|
41
46
|
* - any action: { windowTitle?: string }
|
|
42
47
|
*/
|
|
43
|
-
checkAction(action: string, params?: Record<string, unknown
|
|
48
|
+
checkAction(action: string, params?: Record<string, unknown>, options?: {
|
|
49
|
+
skipUserActivityPause?: boolean;
|
|
50
|
+
}): SafetyCheckResult;
|
|
44
51
|
/** Record that the user performed an activity (mouse/keyboard). */
|
|
45
52
|
recordUserActivity(): void;
|
|
46
53
|
/** Set the pause duration after user activity (default 2000ms). */
|
package/dist/src/safety/guard.js
CHANGED
|
@@ -84,6 +84,45 @@ function normalizeShortcut(raw) {
|
|
|
84
84
|
.join("+");
|
|
85
85
|
}
|
|
86
86
|
// ---------------------------------------------------------------------------
|
|
87
|
+
// Action classification (observe vs input)
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
/** Actions that observe UI/system state without altering it. */
|
|
90
|
+
export const OBSERVE_ACTIONS = new Set([
|
|
91
|
+
"screenshot",
|
|
92
|
+
"list_windows",
|
|
93
|
+
"list_apps",
|
|
94
|
+
"get_window_state",
|
|
95
|
+
"get_screen_size",
|
|
96
|
+
"get_cursor_position",
|
|
97
|
+
"ocr",
|
|
98
|
+
"find_element",
|
|
99
|
+
"wait",
|
|
100
|
+
"wait_for_element",
|
|
101
|
+
"doctor",
|
|
102
|
+
"clipboard_read",
|
|
103
|
+
]);
|
|
104
|
+
/** Actions that synthesize user input — need full user-activity protection. */
|
|
105
|
+
export const INPUT_ACTIONS = new Set([
|
|
106
|
+
"click",
|
|
107
|
+
"double_click",
|
|
108
|
+
"scroll",
|
|
109
|
+
"drag",
|
|
110
|
+
"move",
|
|
111
|
+
"type_text",
|
|
112
|
+
"press_key",
|
|
113
|
+
"click_element",
|
|
114
|
+
"type_in_element",
|
|
115
|
+
"set_value",
|
|
116
|
+
"clipboard_write",
|
|
117
|
+
]);
|
|
118
|
+
export function classifyAction(action) {
|
|
119
|
+
if (OBSERVE_ACTIONS.has(action))
|
|
120
|
+
return "observe";
|
|
121
|
+
if (INPUT_ACTIONS.has(action))
|
|
122
|
+
return "input";
|
|
123
|
+
return "other";
|
|
124
|
+
}
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
87
126
|
// SafetyGuard
|
|
88
127
|
// ---------------------------------------------------------------------------
|
|
89
128
|
export class SafetyGuard {
|
|
@@ -123,7 +162,7 @@ export class SafetyGuard {
|
|
|
123
162
|
* - "key": { keys: string[] }
|
|
124
163
|
* - any action: { windowTitle?: string }
|
|
125
164
|
*/
|
|
126
|
-
checkAction(action, params = {}) {
|
|
165
|
+
checkAction(action, params = {}, options = {}) {
|
|
127
166
|
// 1. Key blocklist -------------------------------------------------------
|
|
128
167
|
if (action === "key" || action === "press_key") {
|
|
129
168
|
const keys = params.keys;
|
|
@@ -164,7 +203,7 @@ export class SafetyGuard {
|
|
|
164
203
|
}
|
|
165
204
|
}
|
|
166
205
|
// 4. Text injection scan --------------------------------------------------
|
|
167
|
-
if (!this.allowUnsafeText && (action === "type" || action === "type_text" || action === "type_in_element" || action === "set_value")) {
|
|
206
|
+
if (!this.allowUnsafeText && (action === "type" || action === "type_text" || action === "type_in_element" || action === "set_value" || action === "clipboard_write")) {
|
|
168
207
|
const text = typeof params.text === "string"
|
|
169
208
|
? params.text
|
|
170
209
|
: typeof params.value === "string"
|
|
@@ -191,8 +230,8 @@ export class SafetyGuard {
|
|
|
191
230
|
};
|
|
192
231
|
}
|
|
193
232
|
this.lastActionTime = now;
|
|
194
|
-
// 6. User activity pause
|
|
195
|
-
if (this.isUserActivityPauseActive()) {
|
|
233
|
+
// 6. User activity pause (skipped for observe-class actions) -----------------
|
|
234
|
+
if (!options.skipUserActivityPause && this.isUserActivityPauseActive()) {
|
|
196
235
|
return {
|
|
197
236
|
allowed: false,
|
|
198
237
|
reason: `User activity detected — pausing automation for ${this.userActivityPauseMs}ms`,
|
|
@@ -2,48 +2,70 @@
|
|
|
2
2
|
* Error taxonomy for UCU-MCP.
|
|
3
3
|
*
|
|
4
4
|
* All errors inherit from UcuError and are categorized by:
|
|
5
|
-
* - code: machine-readable error code
|
|
5
|
+
* - code: machine-readable error code (also exposed via toJSON)
|
|
6
6
|
* - retryable: whether the operation can be retried
|
|
7
7
|
*/
|
|
8
8
|
export declare class UcuError extends Error {
|
|
9
|
+
/** Default error code for this class. Subclasses override. */
|
|
10
|
+
static readonly defaultCode: string;
|
|
9
11
|
readonly code: string;
|
|
10
12
|
readonly retryable: boolean;
|
|
11
13
|
constructor(message: string, code?: string, retryable?: boolean);
|
|
14
|
+
/** Serialize for MCP response / JSON.stringify. */
|
|
15
|
+
toJSON(): {
|
|
16
|
+
name: string;
|
|
17
|
+
code: string;
|
|
18
|
+
retryable: boolean;
|
|
19
|
+
message: string;
|
|
20
|
+
};
|
|
12
21
|
}
|
|
13
22
|
/**
|
|
14
23
|
* Native API call failed (permissions, OS error, timeout).
|
|
15
24
|
*/
|
|
16
25
|
export declare class PlatformError extends UcuError {
|
|
26
|
+
static readonly defaultCode = "PLATFORM_ERROR";
|
|
17
27
|
constructor(message: string, retryable?: boolean);
|
|
18
28
|
}
|
|
19
29
|
/**
|
|
20
30
|
* Action blocked by safety guard.
|
|
21
31
|
*/
|
|
22
32
|
export declare class SafetyError extends UcuError {
|
|
33
|
+
static readonly defaultCode = "SAFETY_BLOCKED";
|
|
23
34
|
constructor(message: string);
|
|
24
35
|
}
|
|
25
36
|
/**
|
|
26
37
|
* Missing OS accessibility/screen-recording permissions.
|
|
27
38
|
*/
|
|
28
39
|
export declare class PermissionError extends UcuError {
|
|
40
|
+
static readonly defaultCode = "PERMISSION_DENIED";
|
|
29
41
|
constructor(permission: string, platform: string);
|
|
30
42
|
}
|
|
31
43
|
/**
|
|
32
44
|
* Requested window ID no longer exists.
|
|
33
45
|
*/
|
|
34
46
|
export declare class WindowNotFoundError extends UcuError {
|
|
47
|
+
static readonly defaultCode = "WINDOW_NOT_FOUND";
|
|
48
|
+
constructor(windowId: string);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Active target window is no longer available.
|
|
52
|
+
*/
|
|
53
|
+
export declare class TargetStaleError extends UcuError {
|
|
54
|
+
static readonly defaultCode = "TARGET_STALE";
|
|
35
55
|
constructor(windowId: string);
|
|
36
56
|
}
|
|
37
57
|
/**
|
|
38
58
|
* Requested accessibility element ID no longer resolves.
|
|
39
59
|
*/
|
|
40
60
|
export declare class ElementNotFoundError extends UcuError {
|
|
61
|
+
static readonly defaultCode = "ELEMENT_NOT_FOUND";
|
|
41
62
|
constructor(elementId: string);
|
|
42
63
|
}
|
|
43
64
|
/**
|
|
44
65
|
* Click/scroll target is outside screen bounds.
|
|
45
66
|
*/
|
|
46
67
|
export declare class CoordinateError extends UcuError {
|
|
68
|
+
static readonly defaultCode = "COORDINATE_OUT_OF_BOUNDS";
|
|
47
69
|
constructor(x: number, y: number, bounds: {
|
|
48
70
|
width: number;
|
|
49
71
|
height: number;
|
|
@@ -53,6 +75,7 @@ export declare class CoordinateError extends UcuError {
|
|
|
53
75
|
* Keystroke or mouse event injection failed.
|
|
54
76
|
*/
|
|
55
77
|
export declare class InputSynthesisError extends UcuError {
|
|
78
|
+
static readonly defaultCode = "INPUT_FAILED";
|
|
56
79
|
constructor(message: string);
|
|
57
80
|
}
|
|
58
81
|
/**
|
|
@@ -60,11 +83,13 @@ export declare class InputSynthesisError extends UcuError {
|
|
|
60
83
|
* implementation does not support.
|
|
61
84
|
*/
|
|
62
85
|
export declare class UnsupportedParameterError extends UcuError {
|
|
86
|
+
static readonly defaultCode = "UNSUPPORTED_PARAMETER";
|
|
63
87
|
constructor(message: string);
|
|
64
88
|
}
|
|
65
89
|
/**
|
|
66
90
|
* Screenshot or window-state capture failed.
|
|
67
91
|
*/
|
|
68
92
|
export declare class CaptureError extends UcuError {
|
|
93
|
+
static readonly defaultCode = "CAPTURE_FAILED";
|
|
69
94
|
constructor(message: string);
|
|
70
95
|
}
|
package/dist/src/util/errors.js
CHANGED
|
@@ -2,21 +2,35 @@
|
|
|
2
2
|
* Error taxonomy for UCU-MCP.
|
|
3
3
|
*
|
|
4
4
|
* All errors inherit from UcuError and are categorized by:
|
|
5
|
-
* - code: machine-readable error code
|
|
5
|
+
* - code: machine-readable error code (also exposed via toJSON)
|
|
6
6
|
* - retryable: whether the operation can be retried
|
|
7
7
|
*/
|
|
8
8
|
// ---------------------------------------------------------------------------
|
|
9
9
|
// Base Error Class
|
|
10
10
|
// ---------------------------------------------------------------------------
|
|
11
11
|
export class UcuError extends Error {
|
|
12
|
+
/** Default error code for this class. Subclasses override. */
|
|
13
|
+
static defaultCode = "UCU_ERROR";
|
|
12
14
|
code;
|
|
13
15
|
retryable;
|
|
14
|
-
constructor(message, code
|
|
16
|
+
constructor(message, code, retryable = false) {
|
|
15
17
|
super(message);
|
|
18
|
+
if (code === undefined) {
|
|
19
|
+
code = this.constructor.defaultCode;
|
|
20
|
+
}
|
|
16
21
|
this.name = this.constructor.name;
|
|
17
22
|
this.code = code;
|
|
18
23
|
this.retryable = retryable;
|
|
19
24
|
}
|
|
25
|
+
/** Serialize for MCP response / JSON.stringify. */
|
|
26
|
+
toJSON() {
|
|
27
|
+
return {
|
|
28
|
+
name: this.name,
|
|
29
|
+
code: this.code,
|
|
30
|
+
retryable: this.retryable,
|
|
31
|
+
message: this.message,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
20
34
|
}
|
|
21
35
|
// ---------------------------------------------------------------------------
|
|
22
36
|
// Platform Errors
|
|
@@ -25,8 +39,9 @@ export class UcuError extends Error {
|
|
|
25
39
|
* Native API call failed (permissions, OS error, timeout).
|
|
26
40
|
*/
|
|
27
41
|
export class PlatformError extends UcuError {
|
|
42
|
+
static defaultCode = "PLATFORM_ERROR";
|
|
28
43
|
constructor(message, retryable = true) {
|
|
29
|
-
super(message,
|
|
44
|
+
super(message, PlatformError.defaultCode, retryable);
|
|
30
45
|
}
|
|
31
46
|
}
|
|
32
47
|
// ---------------------------------------------------------------------------
|
|
@@ -36,8 +51,9 @@ export class PlatformError extends UcuError {
|
|
|
36
51
|
* Action blocked by safety guard.
|
|
37
52
|
*/
|
|
38
53
|
export class SafetyError extends UcuError {
|
|
54
|
+
static defaultCode = "SAFETY_BLOCKED";
|
|
39
55
|
constructor(message) {
|
|
40
|
-
super(message,
|
|
56
|
+
super(message, SafetyError.defaultCode, false);
|
|
41
57
|
}
|
|
42
58
|
}
|
|
43
59
|
// ---------------------------------------------------------------------------
|
|
@@ -47,8 +63,9 @@ export class SafetyError extends UcuError {
|
|
|
47
63
|
* Missing OS accessibility/screen-recording permissions.
|
|
48
64
|
*/
|
|
49
65
|
export class PermissionError extends UcuError {
|
|
66
|
+
static defaultCode = "PERMISSION_DENIED";
|
|
50
67
|
constructor(permission, platform) {
|
|
51
|
-
super(getPermissionMessage(permission, platform),
|
|
68
|
+
super(getPermissionMessage(permission, platform), PermissionError.defaultCode, false);
|
|
52
69
|
}
|
|
53
70
|
}
|
|
54
71
|
function getPermissionMessage(permission, platform) {
|
|
@@ -64,16 +81,27 @@ function getPermissionMessage(permission, platform) {
|
|
|
64
81
|
* Requested window ID no longer exists.
|
|
65
82
|
*/
|
|
66
83
|
export class WindowNotFoundError extends UcuError {
|
|
84
|
+
static defaultCode = "WINDOW_NOT_FOUND";
|
|
85
|
+
constructor(windowId) {
|
|
86
|
+
super(`Window ${windowId} not found. It may have been closed. Run list_windows to get fresh IDs.`, WindowNotFoundError.defaultCode, false);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Active target window is no longer available.
|
|
91
|
+
*/
|
|
92
|
+
export class TargetStaleError extends UcuError {
|
|
93
|
+
static defaultCode = "TARGET_STALE";
|
|
67
94
|
constructor(windowId) {
|
|
68
|
-
super(`
|
|
95
|
+
super(`Active target window ${windowId} is no longer available. Run focus_app or list_windows to refresh.`, TargetStaleError.defaultCode, false);
|
|
69
96
|
}
|
|
70
97
|
}
|
|
71
98
|
/**
|
|
72
99
|
* Requested accessibility element ID no longer resolves.
|
|
73
100
|
*/
|
|
74
101
|
export class ElementNotFoundError extends UcuError {
|
|
102
|
+
static defaultCode = "ELEMENT_NOT_FOUND";
|
|
75
103
|
constructor(elementId) {
|
|
76
|
-
super(`Element ${elementId} not found. It may have been removed or invalidated. Run find_element to get a fresh ID.`,
|
|
104
|
+
super(`Element ${elementId} not found. It may have been removed or invalidated. Run find_element to get a fresh ID.`, ElementNotFoundError.defaultCode, false);
|
|
77
105
|
}
|
|
78
106
|
}
|
|
79
107
|
// ---------------------------------------------------------------------------
|
|
@@ -83,16 +111,18 @@ export class ElementNotFoundError extends UcuError {
|
|
|
83
111
|
* Click/scroll target is outside screen bounds.
|
|
84
112
|
*/
|
|
85
113
|
export class CoordinateError extends UcuError {
|
|
114
|
+
static defaultCode = "COORDINATE_OUT_OF_BOUNDS";
|
|
86
115
|
constructor(x, y, bounds) {
|
|
87
|
-
super(`Coordinate (${x}, ${y}) is outside screen bounds (0-${bounds.width}, 0-${bounds.height}).`,
|
|
116
|
+
super(`Coordinate (${x}, ${y}) is outside screen bounds (0-${bounds.width}, 0-${bounds.height}).`, CoordinateError.defaultCode, false);
|
|
88
117
|
}
|
|
89
118
|
}
|
|
90
119
|
/**
|
|
91
120
|
* Keystroke or mouse event injection failed.
|
|
92
121
|
*/
|
|
93
122
|
export class InputSynthesisError extends UcuError {
|
|
123
|
+
static defaultCode = "INPUT_FAILED";
|
|
94
124
|
constructor(message) {
|
|
95
|
-
super(message,
|
|
125
|
+
super(message, InputSynthesisError.defaultCode, true);
|
|
96
126
|
}
|
|
97
127
|
}
|
|
98
128
|
/**
|
|
@@ -100,8 +130,9 @@ export class InputSynthesisError extends UcuError {
|
|
|
100
130
|
* implementation does not support.
|
|
101
131
|
*/
|
|
102
132
|
export class UnsupportedParameterError extends UcuError {
|
|
133
|
+
static defaultCode = "UNSUPPORTED_PARAMETER";
|
|
103
134
|
constructor(message) {
|
|
104
|
-
super(message,
|
|
135
|
+
super(message, UnsupportedParameterError.defaultCode, false);
|
|
105
136
|
}
|
|
106
137
|
}
|
|
107
138
|
// ---------------------------------------------------------------------------
|
|
@@ -111,7 +142,8 @@ export class UnsupportedParameterError extends UcuError {
|
|
|
111
142
|
* Screenshot or window-state capture failed.
|
|
112
143
|
*/
|
|
113
144
|
export class CaptureError extends UcuError {
|
|
145
|
+
static defaultCode = "CAPTURE_FAILED";
|
|
114
146
|
constructor(message) {
|
|
115
|
-
super(message,
|
|
147
|
+
super(message, CaptureError.defaultCode, true);
|
|
116
148
|
}
|
|
117
149
|
}
|