ucu-mcp 0.1.0
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 +55 -0
- package/README.md +393 -0
- package/dist/bin/ucu-mcp.d.ts +2 -0
- package/dist/bin/ucu-mcp.js +47 -0
- package/dist/src/index.d.ts +7 -0
- package/dist/src/index.js +6 -0
- package/dist/src/mcp/server.d.ts +1 -0
- package/dist/src/mcp/server.js +26 -0
- package/dist/src/mcp/tools.d.ts +17 -0
- package/dist/src/mcp/tools.js +340 -0
- package/dist/src/mcp/transport.d.ts +2 -0
- package/dist/src/mcp/transport.js +4 -0
- package/dist/src/platform/base.d.ts +127 -0
- package/dist/src/platform/base.js +1 -0
- package/dist/src/platform/linux.d.ts +22 -0
- package/dist/src/platform/linux.js +62 -0
- package/dist/src/platform/macos.d.ts +39 -0
- package/dist/src/platform/macos.js +1478 -0
- package/dist/src/platform/windows.d.ts +18 -0
- package/dist/src/platform/windows.js +48 -0
- package/dist/src/safety/guard.d.ts +50 -0
- package/dist/src/safety/guard.js +220 -0
- package/dist/src/safety/permissions.d.ts +17 -0
- package/dist/src/safety/permissions.js +184 -0
- package/dist/src/util/errors.d.ts +64 -0
- package/dist/src/util/errors.js +109 -0
- package/dist/src/util/logger.d.ts +41 -0
- package/dist/src/util/logger.js +92 -0
- package/dist/src/util/retry.d.ts +30 -0
- package/dist/src/util/retry.js +53 -0
- package/dist/src/utils/input.d.ts +23 -0
- package/dist/src/utils/input.js +425 -0
- package/dist/src/utils/screenshot.d.ts +20 -0
- package/dist/src/utils/screenshot.js +157 -0
- package/package.json +50 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Platform, ScreenRegion, ScreenSize, CursorPosition, WindowInfo, WindowState, OcrResult, FindElementOptions, FindElementResult } from "./base.js";
|
|
2
|
+
export declare class WindowsPlatform implements Platform {
|
|
3
|
+
screenshot(_display?: number, _region?: ScreenRegion): Promise<Buffer>;
|
|
4
|
+
getScreenSize(_display?: number): ScreenSize;
|
|
5
|
+
listWindows(_includeMinimized?: boolean): Promise<WindowInfo[]>;
|
|
6
|
+
getWindowState(_windowId?: string, _depth?: number, _includeBounds?: boolean): Promise<WindowState>;
|
|
7
|
+
click(_x: number, _y: number, _button?: "left" | "right" | "middle", _doubleClick?: boolean): Promise<void>;
|
|
8
|
+
move(_x: number, _y: number): Promise<void>;
|
|
9
|
+
drag(_startX: number, _startY: number, _endX: number, _endY: number, _button?: "left" | "right" | "middle", _duration?: number): Promise<void>;
|
|
10
|
+
scroll(_x: number, _y: number, _deltaX: number, _deltaY: number): Promise<void>;
|
|
11
|
+
getCursorPosition(): CursorPosition;
|
|
12
|
+
type(_text: string, _delay?: number): Promise<void>;
|
|
13
|
+
key(_keys: string[]): Promise<void>;
|
|
14
|
+
ocr(_display?: number, _region?: ScreenRegion): Promise<OcrResult>;
|
|
15
|
+
findElement(_options: FindElementOptions): Promise<FindElementResult[]>;
|
|
16
|
+
clickElement(_elementId: string, _app?: string): Promise<void>;
|
|
17
|
+
typeInElement(_elementId: string, _text: string, _app?: string, _clearFirst?: boolean): Promise<void>;
|
|
18
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export class WindowsPlatform {
|
|
2
|
+
async screenshot(_display, _region) {
|
|
3
|
+
throw new Error("Not implemented: Windows screenshot");
|
|
4
|
+
}
|
|
5
|
+
getScreenSize(_display) {
|
|
6
|
+
throw new Error("Not implemented: Windows getScreenSize");
|
|
7
|
+
}
|
|
8
|
+
async listWindows(_includeMinimized) {
|
|
9
|
+
throw new Error("Not implemented: Windows listWindows");
|
|
10
|
+
}
|
|
11
|
+
async getWindowState(_windowId, _depth, _includeBounds) {
|
|
12
|
+
// TODO: Implement using UI Automation API
|
|
13
|
+
throw new Error("Not implemented: Windows getWindowState");
|
|
14
|
+
}
|
|
15
|
+
async click(_x, _y, _button, _doubleClick) {
|
|
16
|
+
throw new Error("Not implemented: Windows click");
|
|
17
|
+
}
|
|
18
|
+
async move(_x, _y) {
|
|
19
|
+
throw new Error("Not implemented: Windows move");
|
|
20
|
+
}
|
|
21
|
+
async drag(_startX, _startY, _endX, _endY, _button, _duration) {
|
|
22
|
+
throw new Error("Not implemented: Windows drag");
|
|
23
|
+
}
|
|
24
|
+
async scroll(_x, _y, _deltaX, _deltaY) {
|
|
25
|
+
throw new Error("Not implemented: Windows scroll");
|
|
26
|
+
}
|
|
27
|
+
getCursorPosition() {
|
|
28
|
+
throw new Error("Not implemented: Windows getCursorPosition");
|
|
29
|
+
}
|
|
30
|
+
async type(_text, _delay) {
|
|
31
|
+
throw new Error("Not implemented: Windows type");
|
|
32
|
+
}
|
|
33
|
+
async key(_keys) {
|
|
34
|
+
throw new Error("Not implemented: Windows key");
|
|
35
|
+
}
|
|
36
|
+
async ocr(_display, _region) {
|
|
37
|
+
throw new Error("Not implemented: Windows OCR");
|
|
38
|
+
}
|
|
39
|
+
async findElement(_options) {
|
|
40
|
+
throw new Error("Not implemented: Windows findElement");
|
|
41
|
+
}
|
|
42
|
+
async clickElement(_elementId, _app) {
|
|
43
|
+
throw new Error("Not implemented: Windows clickElement");
|
|
44
|
+
}
|
|
45
|
+
async typeInElement(_elementId, _text, _app, _clearFirst) {
|
|
46
|
+
throw new Error("Not implemented: Windows typeInElement");
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SafetyGuard - Action safety checker for UCU automation.
|
|
3
|
+
*
|
|
4
|
+
* Evaluates proposed actions against a set of configurable rules:
|
|
5
|
+
* - key-blocklist: blocks dangerous keyboard shortcuts
|
|
6
|
+
* - window-skip: skips sensitive windows (banking, password managers)
|
|
7
|
+
* - rate-limit: throttles action frequency
|
|
8
|
+
*/
|
|
9
|
+
export interface SafetyCheckResult {
|
|
10
|
+
allowed: boolean;
|
|
11
|
+
reason?: string;
|
|
12
|
+
}
|
|
13
|
+
export interface SafetyGuardConfig {
|
|
14
|
+
/** Extra shortcut patterns to block (in addition to built-ins). */
|
|
15
|
+
blockedKeys?: string[];
|
|
16
|
+
/** Extra window title patterns to skip (in addition to built-ins). */
|
|
17
|
+
skippedWindows?: string[];
|
|
18
|
+
/** Extra URL patterns to block (in addition to built-ins). */
|
|
19
|
+
blockedUrls?: string[];
|
|
20
|
+
/** Disable text injection scanning for controlled test harnesses. */
|
|
21
|
+
allowUnsafeText?: boolean;
|
|
22
|
+
/** Minimum milliseconds between consecutive actions (default 100). */
|
|
23
|
+
rateLimitMs?: number;
|
|
24
|
+
}
|
|
25
|
+
export declare class SafetyGuard {
|
|
26
|
+
private readonly blockedKeys;
|
|
27
|
+
private readonly skippedWindows;
|
|
28
|
+
private readonly blockedUrls;
|
|
29
|
+
private readonly allowUnsafeText;
|
|
30
|
+
private readonly rateLimitMs;
|
|
31
|
+
private lastActionTime;
|
|
32
|
+
private lastUserActivityTime;
|
|
33
|
+
private userActivityPauseMs;
|
|
34
|
+
constructor(config?: SafetyGuardConfig);
|
|
35
|
+
/**
|
|
36
|
+
* Evaluate whether the proposed action should be allowed.
|
|
37
|
+
*
|
|
38
|
+
* @param action The action type (e.g. "key", "click", "type", "screenshot").
|
|
39
|
+
* @param params Arbitrary action parameters; expected keys depend on action.
|
|
40
|
+
* - "key": { keys: string[] }
|
|
41
|
+
* - any action: { windowTitle?: string }
|
|
42
|
+
*/
|
|
43
|
+
checkAction(action: string, params?: Record<string, unknown>): SafetyCheckResult;
|
|
44
|
+
/** Record that the user performed an activity (mouse/keyboard). */
|
|
45
|
+
recordUserActivity(): void;
|
|
46
|
+
/** Set the pause duration after user activity (default 2000ms). */
|
|
47
|
+
setUserActivityPauseMs(ms: number): void;
|
|
48
|
+
/** Check if user activity pause is still active. */
|
|
49
|
+
isUserActivityPauseActive(): boolean;
|
|
50
|
+
}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SafetyGuard - Action safety checker for UCU automation.
|
|
3
|
+
*
|
|
4
|
+
* Evaluates proposed actions against a set of configurable rules:
|
|
5
|
+
* - key-blocklist: blocks dangerous keyboard shortcuts
|
|
6
|
+
* - window-skip: skips sensitive windows (banking, password managers)
|
|
7
|
+
* - rate-limit: throttles action frequency
|
|
8
|
+
*/
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// Built-in blocked shortcuts
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
const DEFAULT_BLOCKED_KEYS = [
|
|
13
|
+
// macOS – app-level
|
|
14
|
+
"cmd+q",
|
|
15
|
+
"cmd+w",
|
|
16
|
+
"cmd+l", // lock screen
|
|
17
|
+
// macOS – system-level
|
|
18
|
+
"cmd+option+esc", // Force-quit dialog
|
|
19
|
+
"cmd+ctrl+power", // force restart
|
|
20
|
+
"cmd+option+power", // sleep
|
|
21
|
+
// Windows / Linux
|
|
22
|
+
"alt+f4",
|
|
23
|
+
"alt+f2", // Linux run dialog
|
|
24
|
+
"ctrl+alt+del",
|
|
25
|
+
"ctrl+alt+backspace", // Linux kill X
|
|
26
|
+
"ctrl+alt+t", // Linux terminal
|
|
27
|
+
];
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// Built-in sensitive window patterns (case-insensitive substring match)
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
const DEFAULT_SKIPPED_WINDOWS = [
|
|
32
|
+
"1password",
|
|
33
|
+
"bitwarden",
|
|
34
|
+
"lastpass",
|
|
35
|
+
"keepass",
|
|
36
|
+
"dashlane",
|
|
37
|
+
"keychain access",
|
|
38
|
+
"钥匙串访问",
|
|
39
|
+
"bank",
|
|
40
|
+
"银行",
|
|
41
|
+
"paypal",
|
|
42
|
+
"stripe",
|
|
43
|
+
"robinhood",
|
|
44
|
+
"coinbase",
|
|
45
|
+
];
|
|
46
|
+
const DEFAULT_BLOCKED_URL_PATTERNS = [
|
|
47
|
+
"1password.com",
|
|
48
|
+
"bitwarden.com",
|
|
49
|
+
"lastpass.com",
|
|
50
|
+
"dashlane.com",
|
|
51
|
+
"keepersecurity.com",
|
|
52
|
+
"icloud.com/keychain",
|
|
53
|
+
"paypal.com",
|
|
54
|
+
"stripe.com",
|
|
55
|
+
"bank",
|
|
56
|
+
"bankofamerica.com",
|
|
57
|
+
"chase.com",
|
|
58
|
+
"wellsfargo.com",
|
|
59
|
+
"capitalone.com",
|
|
60
|
+
"americanexpress.com",
|
|
61
|
+
"coinbase.com",
|
|
62
|
+
"robinhood.com",
|
|
63
|
+
"binance.com",
|
|
64
|
+
"kraken.com",
|
|
65
|
+
];
|
|
66
|
+
const DEFAULT_TEXT_INJECTION_PATTERNS = [
|
|
67
|
+
{ pattern: /\$\s*\(/, reason: "shell command substitution" },
|
|
68
|
+
{ pattern: /`[^`]+`/, reason: "shell backtick substitution" },
|
|
69
|
+
{ pattern: /&&|\|\|/, reason: "shell command chaining" },
|
|
70
|
+
{ pattern: /\|\s*(sh|bash|zsh|python|ruby|perl|node)\b/i, reason: "piping into an interpreter" },
|
|
71
|
+
{ pattern: /\b(sudo\s+rm|rm\s+-rf|mkfs|diskutil\s+erase|dd\s+if=|chmod\s+-R\s+777)\b/i, reason: "dangerous shell command" },
|
|
72
|
+
{ pattern: /\b(ObjC\.import|Application\s*\(|do\s+shell\s+script)\b/, reason: "AppleScript/JXA injection primitive" },
|
|
73
|
+
];
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
// Helpers
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
/** Normalize a shortcut string to lowercase, trimmed, sorted modifiers. */
|
|
78
|
+
function normalizeShortcut(raw) {
|
|
79
|
+
return raw
|
|
80
|
+
.toLowerCase()
|
|
81
|
+
.split("+")
|
|
82
|
+
.map((s) => s.trim())
|
|
83
|
+
.sort()
|
|
84
|
+
.join("+");
|
|
85
|
+
}
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
// SafetyGuard
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
export class SafetyGuard {
|
|
90
|
+
blockedKeys;
|
|
91
|
+
skippedWindows;
|
|
92
|
+
blockedUrls;
|
|
93
|
+
allowUnsafeText;
|
|
94
|
+
rateLimitMs;
|
|
95
|
+
lastActionTime = 0;
|
|
96
|
+
lastUserActivityTime = 0;
|
|
97
|
+
userActivityPauseMs = 2000;
|
|
98
|
+
constructor(config) {
|
|
99
|
+
const extra = (config?.blockedKeys ?? []).map(normalizeShortcut);
|
|
100
|
+
this.blockedKeys = new Set([
|
|
101
|
+
...DEFAULT_BLOCKED_KEYS.map(normalizeShortcut),
|
|
102
|
+
...extra,
|
|
103
|
+
]);
|
|
104
|
+
this.skippedWindows = [
|
|
105
|
+
...DEFAULT_SKIPPED_WINDOWS,
|
|
106
|
+
...(config?.skippedWindows ?? []),
|
|
107
|
+
].map((p) => p.toLowerCase());
|
|
108
|
+
this.blockedUrls = [
|
|
109
|
+
...DEFAULT_BLOCKED_URL_PATTERNS,
|
|
110
|
+
...(config?.blockedUrls ?? []),
|
|
111
|
+
].map((p) => p.toLowerCase());
|
|
112
|
+
this.allowUnsafeText = config?.allowUnsafeText ?? false;
|
|
113
|
+
this.rateLimitMs = config?.rateLimitMs ?? 100;
|
|
114
|
+
}
|
|
115
|
+
// -----------------------------------------------------------------------
|
|
116
|
+
// Public API
|
|
117
|
+
// -----------------------------------------------------------------------
|
|
118
|
+
/**
|
|
119
|
+
* Evaluate whether the proposed action should be allowed.
|
|
120
|
+
*
|
|
121
|
+
* @param action The action type (e.g. "key", "click", "type", "screenshot").
|
|
122
|
+
* @param params Arbitrary action parameters; expected keys depend on action.
|
|
123
|
+
* - "key": { keys: string[] }
|
|
124
|
+
* - any action: { windowTitle?: string }
|
|
125
|
+
*/
|
|
126
|
+
checkAction(action, params = {}) {
|
|
127
|
+
// 1. Key blocklist -------------------------------------------------------
|
|
128
|
+
if (action === "key" || action === "press_key") {
|
|
129
|
+
const keys = params.keys;
|
|
130
|
+
if (keys && keys.length > 0) {
|
|
131
|
+
const normalized = normalizeShortcut(keys.join("+"));
|
|
132
|
+
if (this.blockedKeys.has(normalized)) {
|
|
133
|
+
return {
|
|
134
|
+
allowed: false,
|
|
135
|
+
reason: `Blocked shortcut: ${keys.join("+")}`,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// 2. Window skip ----------------------------------------------------------
|
|
141
|
+
const windowTitle = typeof params.windowTitle === "string" ? params.windowTitle : undefined;
|
|
142
|
+
if (windowTitle) {
|
|
143
|
+
const lower = windowTitle.toLowerCase();
|
|
144
|
+
for (const pattern of this.skippedWindows) {
|
|
145
|
+
if (lower.includes(pattern)) {
|
|
146
|
+
return {
|
|
147
|
+
allowed: false,
|
|
148
|
+
reason: `Skipped sensitive window: "${windowTitle}"`,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
// 3. URL blocklist --------------------------------------------------------
|
|
154
|
+
const url = typeof params.url === "string" ? params.url : undefined;
|
|
155
|
+
if (url) {
|
|
156
|
+
const lower = url.toLowerCase();
|
|
157
|
+
for (const pattern of this.blockedUrls) {
|
|
158
|
+
if (lower.includes(pattern)) {
|
|
159
|
+
return {
|
|
160
|
+
allowed: false,
|
|
161
|
+
reason: `Blocked sensitive URL: ${url}`,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
// 4. Text injection scan --------------------------------------------------
|
|
167
|
+
if (!this.allowUnsafeText && (action === "type" || action === "type_text" || action === "type_in_element" || action === "set_value")) {
|
|
168
|
+
const text = typeof params.text === "string"
|
|
169
|
+
? params.text
|
|
170
|
+
: typeof params.value === "string"
|
|
171
|
+
? params.value
|
|
172
|
+
: undefined;
|
|
173
|
+
if (text) {
|
|
174
|
+
for (const { pattern, reason } of DEFAULT_TEXT_INJECTION_PATTERNS) {
|
|
175
|
+
if (pattern.test(text)) {
|
|
176
|
+
return {
|
|
177
|
+
allowed: false,
|
|
178
|
+
reason: `Blocked suspicious typed text (${reason})`,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
// 5. Rate limit -----------------------------------------------------------
|
|
185
|
+
const now = Date.now();
|
|
186
|
+
const elapsed = now - this.lastActionTime;
|
|
187
|
+
if (elapsed < this.rateLimitMs) {
|
|
188
|
+
return {
|
|
189
|
+
allowed: false,
|
|
190
|
+
reason: `Rate-limited: ${elapsed}ms since last action (min ${this.rateLimitMs}ms)`,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
this.lastActionTime = now;
|
|
194
|
+
// 6. User activity pause --------------------------------------------------
|
|
195
|
+
if (this.isUserActivityPauseActive()) {
|
|
196
|
+
return {
|
|
197
|
+
allowed: false,
|
|
198
|
+
reason: `User activity detected — pausing automation for ${this.userActivityPauseMs}ms`,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
return { allowed: true };
|
|
202
|
+
}
|
|
203
|
+
// -----------------------------------------------------------------------
|
|
204
|
+
// User Activity Monitoring
|
|
205
|
+
// -----------------------------------------------------------------------
|
|
206
|
+
/** Record that the user performed an activity (mouse/keyboard). */
|
|
207
|
+
recordUserActivity() {
|
|
208
|
+
this.lastUserActivityTime = Date.now();
|
|
209
|
+
}
|
|
210
|
+
/** Set the pause duration after user activity (default 2000ms). */
|
|
211
|
+
setUserActivityPauseMs(ms) {
|
|
212
|
+
this.userActivityPauseMs = ms;
|
|
213
|
+
}
|
|
214
|
+
/** Check if user activity pause is still active. */
|
|
215
|
+
isUserActivityPauseActive() {
|
|
216
|
+
if (this.userActivityPauseMs <= 0)
|
|
217
|
+
return false;
|
|
218
|
+
return Date.now() - this.lastUserActivityTime < this.userActivityPauseMs;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export type PermissionType = "accessibility" | "screenRecording";
|
|
2
|
+
export interface PermissionCheckResult {
|
|
3
|
+
granted: boolean;
|
|
4
|
+
missing: PermissionType[];
|
|
5
|
+
}
|
|
6
|
+
export interface PermissionDetail {
|
|
7
|
+
type: PermissionType;
|
|
8
|
+
granted: boolean;
|
|
9
|
+
instructions: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function checkPermissions(): Promise<PermissionCheckResult>;
|
|
12
|
+
export declare function checkPermission(type: "accessibility" | "screenRecording"): Promise<{
|
|
13
|
+
granted: boolean;
|
|
14
|
+
message?: string;
|
|
15
|
+
}>;
|
|
16
|
+
export declare function getPermissionInstructions(type: PermissionType): string;
|
|
17
|
+
export declare function runPermissionDoctor(): Promise<PermissionDetail[]>;
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
const execFileAsync = promisify(execFile);
|
|
4
|
+
/**
|
|
5
|
+
* Get the name of the terminal app that the user needs to authorize.
|
|
6
|
+
*/
|
|
7
|
+
function getTerminalAppName() {
|
|
8
|
+
// Walk up the process tree to find the terminal emulator
|
|
9
|
+
const ppid = process.ppid;
|
|
10
|
+
// Common terminal app names
|
|
11
|
+
const env = process.env.TERM_PROGRAM || "";
|
|
12
|
+
const nameMap = {
|
|
13
|
+
"Apple_Terminal": "Terminal.app",
|
|
14
|
+
"iTerm.app": "iTerm.app",
|
|
15
|
+
"vscode": "Visual Studio Code",
|
|
16
|
+
"alacritty": "Alacritty",
|
|
17
|
+
"kitty": "kitty",
|
|
18
|
+
"wezterm": "WezTerm",
|
|
19
|
+
"ghostty": "Ghostty",
|
|
20
|
+
"warp": "Warp",
|
|
21
|
+
"hyper": "Hyper",
|
|
22
|
+
};
|
|
23
|
+
return nameMap[env] || env || "your terminal app";
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Open the macOS System Settings page for a permission.
|
|
27
|
+
*/
|
|
28
|
+
async function openPermissionSettings(type) {
|
|
29
|
+
const url = type === "accessibility"
|
|
30
|
+
? "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility"
|
|
31
|
+
: "x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture";
|
|
32
|
+
try {
|
|
33
|
+
await execFileAsync("/usr/bin/open", [url], { timeout: 3000 });
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
// Non-critical — best effort
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Request accessibility permission by triggering the macOS system dialog.
|
|
41
|
+
* Uses AXIsProcessTrustedWithOptions with kAXTrustedCheckOptionPrompt=true
|
|
42
|
+
* which shows the system authorization prompt.
|
|
43
|
+
*/
|
|
44
|
+
async function requestAccessibilityWithPrompt() {
|
|
45
|
+
try {
|
|
46
|
+
const script = `
|
|
47
|
+
ObjC.import('CoreServices');
|
|
48
|
+
var opts = $.NSDictionary.dictionaryWithObjectForObject(
|
|
49
|
+
$(true), $("kAXTrustedCheckOptionPrompt")
|
|
50
|
+
);
|
|
51
|
+
$.AXIsProcessTrustedWithOptions(opts);
|
|
52
|
+
`;
|
|
53
|
+
const { stdout } = await execFileAsync("/usr/bin/osascript", [
|
|
54
|
+
"-l", "JavaScript", "-e", script,
|
|
55
|
+
], { timeout: 10000 });
|
|
56
|
+
return stdout.trim() === "true";
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Check accessibility by trying to use System Events via osascript.
|
|
64
|
+
* macOS grants the PARENT process the TCC entry — we check via
|
|
65
|
+
* a simple AXAPI call that returns the current process's status.
|
|
66
|
+
*
|
|
67
|
+
* NOTE: When running via `node`, the TCC entry is for "Terminal.app"
|
|
68
|
+
* (or whichever terminal hosts node). The osascript subprocess
|
|
69
|
+
* inherits the parent's TCC for accessibility because it runs
|
|
70
|
+
* under the same application context via the shell.
|
|
71
|
+
*/
|
|
72
|
+
async function checkAccessibility() {
|
|
73
|
+
if (process.platform !== "darwin")
|
|
74
|
+
return true;
|
|
75
|
+
try {
|
|
76
|
+
const script = `
|
|
77
|
+
tell application "System Events"
|
|
78
|
+
return (count of processes) as text
|
|
79
|
+
end tell
|
|
80
|
+
`;
|
|
81
|
+
const { stdout } = await execFileAsync("/usr/bin/osascript", ["-e", script], {
|
|
82
|
+
timeout: 5000,
|
|
83
|
+
});
|
|
84
|
+
const count = parseInt(stdout.trim(), 10);
|
|
85
|
+
return !isNaN(count) && count > 0;
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Check screen recording by attempting a minimal screenshot.
|
|
93
|
+
* screencapture respects TCC — it fails with a specific error
|
|
94
|
+
* if the calling process doesn't have Screen Recording permission.
|
|
95
|
+
*/
|
|
96
|
+
async function checkScreenRecording() {
|
|
97
|
+
if (process.platform !== "darwin")
|
|
98
|
+
return true;
|
|
99
|
+
try {
|
|
100
|
+
await execFileAsync("/usr/sbin/screencapture", [
|
|
101
|
+
"-x", "-R", "0,0,1,1", "/dev/null",
|
|
102
|
+
], { timeout: 5000 });
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
catch (e) {
|
|
106
|
+
const msg = (e.stderr || e.message || "").toLowerCase();
|
|
107
|
+
// screencapture returns error when no permission
|
|
108
|
+
if (msg.includes("not authorized") || msg.includes("denied") || msg.includes("permission")) {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
// Other errors (file system) — permission is likely granted
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
export async function checkPermissions() {
|
|
116
|
+
if (process.platform !== "darwin") {
|
|
117
|
+
return { granted: true, missing: [] };
|
|
118
|
+
}
|
|
119
|
+
const [hasAccessibility, hasScreenRecording] = await Promise.all([
|
|
120
|
+
checkAccessibility(),
|
|
121
|
+
checkScreenRecording(),
|
|
122
|
+
]);
|
|
123
|
+
const missing = [];
|
|
124
|
+
if (!hasAccessibility)
|
|
125
|
+
missing.push("accessibility");
|
|
126
|
+
if (!hasScreenRecording)
|
|
127
|
+
missing.push("screenRecording");
|
|
128
|
+
return { granted: missing.length === 0, missing };
|
|
129
|
+
}
|
|
130
|
+
export async function checkPermission(type) {
|
|
131
|
+
if (process.platform !== "darwin") {
|
|
132
|
+
return { granted: true };
|
|
133
|
+
}
|
|
134
|
+
const appName = getTerminalAppName();
|
|
135
|
+
if (type === "accessibility") {
|
|
136
|
+
const granted = await checkAccessibility();
|
|
137
|
+
if (!granted) {
|
|
138
|
+
// Trigger the macOS system prompt for Accessibility
|
|
139
|
+
await requestAccessibilityWithPrompt();
|
|
140
|
+
// Also open System Settings as a fallback
|
|
141
|
+
await openPermissionSettings("accessibility");
|
|
142
|
+
return {
|
|
143
|
+
granted: false,
|
|
144
|
+
message: `macOS Accessibility permission required for ${appName}. A system dialog should have appeared requesting authorization — please approve it. If no dialog appeared, open System Settings > Privacy & Security > Accessibility and manually enable ${appName}. Then restart ucu-mcp.`,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
return { granted: true };
|
|
148
|
+
}
|
|
149
|
+
const granted = await checkScreenRecording();
|
|
150
|
+
if (!granted) {
|
|
151
|
+
// Open System Settings for Screen Recording
|
|
152
|
+
await openPermissionSettings("screenRecording");
|
|
153
|
+
return {
|
|
154
|
+
granted: false,
|
|
155
|
+
message: `macOS Screen Recording permission required for ${appName}. Opening System Settings now — please navigate to Privacy & Security > Screen Recording and enable ${appName}. Then restart ucu-mcp.`,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
return { granted: true };
|
|
159
|
+
}
|
|
160
|
+
export function getPermissionInstructions(type) {
|
|
161
|
+
const instructions = {
|
|
162
|
+
accessibility: "Open System Settings > Privacy & Security > Accessibility. Add and enable your terminal application (e.g. Terminal.app, iTerm2, Alacritty). Restart ucu-mcp after granting.",
|
|
163
|
+
screenRecording: "Open System Settings > Privacy & Security > Screen Recording. Add and enable your terminal application. Restart ucu-mcp after granting.",
|
|
164
|
+
};
|
|
165
|
+
return instructions[type];
|
|
166
|
+
}
|
|
167
|
+
export async function runPermissionDoctor() {
|
|
168
|
+
const details = [];
|
|
169
|
+
const [accessibility, screenRecording] = await Promise.all([
|
|
170
|
+
checkAccessibility(),
|
|
171
|
+
checkScreenRecording(),
|
|
172
|
+
]);
|
|
173
|
+
details.push({
|
|
174
|
+
type: "accessibility",
|
|
175
|
+
granted: accessibility,
|
|
176
|
+
instructions: getPermissionInstructions("accessibility"),
|
|
177
|
+
});
|
|
178
|
+
details.push({
|
|
179
|
+
type: "screenRecording",
|
|
180
|
+
granted: screenRecording,
|
|
181
|
+
instructions: getPermissionInstructions("screenRecording"),
|
|
182
|
+
});
|
|
183
|
+
return details;
|
|
184
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error taxonomy for UCU-MCP.
|
|
3
|
+
*
|
|
4
|
+
* All errors inherit from UcuError and are categorized by:
|
|
5
|
+
* - code: machine-readable error code
|
|
6
|
+
* - retryable: whether the operation can be retried
|
|
7
|
+
*/
|
|
8
|
+
export declare class UcuError extends Error {
|
|
9
|
+
readonly code: string;
|
|
10
|
+
readonly retryable: boolean;
|
|
11
|
+
constructor(message: string, code?: string, retryable?: boolean);
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Native API call failed (permissions, OS error, timeout).
|
|
15
|
+
*/
|
|
16
|
+
export declare class PlatformError extends UcuError {
|
|
17
|
+
constructor(message: string, retryable?: boolean);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Action blocked by safety guard.
|
|
21
|
+
*/
|
|
22
|
+
export declare class SafetyError extends UcuError {
|
|
23
|
+
constructor(message: string);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Missing OS accessibility/screen-recording permissions.
|
|
27
|
+
*/
|
|
28
|
+
export declare class PermissionError extends UcuError {
|
|
29
|
+
constructor(permission: string, platform: string);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Requested window ID no longer exists.
|
|
33
|
+
*/
|
|
34
|
+
export declare class WindowNotFoundError extends UcuError {
|
|
35
|
+
constructor(windowId: string);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Click/scroll target is outside screen bounds.
|
|
39
|
+
*/
|
|
40
|
+
export declare class CoordinateError extends UcuError {
|
|
41
|
+
constructor(x: number, y: number, bounds: {
|
|
42
|
+
width: number;
|
|
43
|
+
height: number;
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Keystroke or mouse event injection failed.
|
|
48
|
+
*/
|
|
49
|
+
export declare class InputSynthesisError extends UcuError {
|
|
50
|
+
constructor(message: string);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* The request is well-formed JSON, but asks for a parameter combination this
|
|
54
|
+
* implementation does not support.
|
|
55
|
+
*/
|
|
56
|
+
export declare class UnsupportedParameterError extends UcuError {
|
|
57
|
+
constructor(message: string);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Screenshot or window-state capture failed.
|
|
61
|
+
*/
|
|
62
|
+
export declare class CaptureError extends UcuError {
|
|
63
|
+
constructor(message: string);
|
|
64
|
+
}
|