ucu-mcp 0.5.2 → 0.6.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 CHANGED
@@ -5,6 +5,36 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.6.0] - 2026-06-17
9
+
10
+ 对标 Codex 原生 computer use:输入注入走 per-process 路径,不移动全局光标、不抢前台。仅 Mac 端(与 Codex mac 版一致,Win 版只前台)。
11
+
12
+ ### Added — 核心后台能力
13
+
14
+ - **skylight-helper(native/skylight/main.swift)**:第 4 个 Swift native helper。dlopen SkyLight.framework,`SLEventPostToPid`(私有 SPI)按 PID 投递鼠标事件——绕开全局 HID 流,光标不动。完整 cua-driver 配方:NSEvent bridge 构建(绕开 Chromium renderer 过滤)+ field 40 pid latch + `CGEventSetWindowLocation` + focus-without-raise(`SLPSPostEventRecordTo` 248-byte event record,不调 `SLPSSetFrontProcessWithOptions`)+ (-1,-1) primer 欺骗 Chromium user-activation gate。键盘走公开 `CGEventPostToPid`。frontmost/canvas app 自动 fallback HID-tap。
15
+ - **pid/windowNumber 透传链**:`WindowInfo`/`AppTarget` 加 `windowNumber`(真实 CGWindowID,window.ts 原本丢弃了)。`macos/input.ts` 读 `activeTarget` 透传到 `utils/input.ts`。`utils/input.ts` 函数加 `target` 参数,pid>0 走 skylight-helper,否则 fallback cgevent-helper(HID-tap)。
16
+ - **AX 保活(方向4b 根因修复)**:skylight-helper 的 `keepAlive` 命令写 `AXManualAccessibility`/`AXEnhancedUserInterface` + 订阅 remote-aware AX observer(`_AXObserverAddNotificationAndCheckRemote`,符号缺失时 fallback 公开 API)。focusApp 成功后调用,保活目标 AX 树(被遮挡的 Electron/Tauri 不再冻结 → AXPress 不再静默失败)。
17
+ - **tray per-process**:`MenuBarExtraItem` 加 `pid` 字段,findMenuBarExtra 捕获 SystemUIServer pid。focusApp 建 tray target 时用真实 host pid(非 0),tray 坐标点击也走 per-process。
18
+ - **dispatch 信号透出**:所有输入工具返回 `result.dispatch`(`"per-pid" | "hid-tap"`)。hid-tap 时附 warning。doctor 检测 skylight-helper 可用性。
19
+ - **DispatchMethod 类型**(base.ts):Platform 接口 mouse/keyboard 方法返回 `Promise<DispatchMethod | void>`。
20
+
21
+ ### Changed
22
+
23
+ - **element.ts 坐标 fallback 迁移**:`JXA_COORDINATE_CLICK`(JXA HID-tap)重命名为 `JXA_BOUNDS_CENTER`(只算中心坐标)。clickElement/clickMenuBarExtra 的坐标点击退出 JXA,改在 TS 层调 `this.click()`(走 per-process)。
24
+ - SKILL.md:加 "Dispatch method (v0.6.0+)" 节 + Operating Rules 加 "focus_app before input"。
25
+
26
+ ### 真机验证(2026-06-17)
27
+
28
+ - ✅ skylight-helper ping 返回 `{"ok":true,"skylight":true}`(SPI 全加载)。
29
+ - ✅ 对后台 Finder pid click(100,100) 返回 `method:per-pid`,光标不跳到 (100,100)(HID-tap 会直接跳过去)——核心承诺验证。
30
+ - ✅ keepAlive 对 Finder pid 返回 `method:ax-keepalive`。
31
+ - ✅ 310 tests passed(4 native helpers 全编译)。
32
+
33
+ ### 已知限制
34
+
35
+ - Canvas/GPU app(Blender/Unity/游戏)的 event loop 只接受 HID-tap → 自动 fallback,返回 `dispatch:"hid-tap"` + warning。
36
+ - `_AXObserverAddNotificationAndCheckRemote` 符号在本机未通过 dlsym 解析(remote:false),但 AXManualAccessibility/AXEnhancedUserInterface 属性写入生效(覆盖非 Chromium 场景)。
37
+
8
38
  ## [0.5.2] - 2026-06-17
9
39
 
10
40
  Agent Skill 重写为 CLI agent 优先(Claude Code / Codex / OpenCode 都是 stdio CLI 环境)。
@@ -122,10 +122,12 @@ export function registerAppTools(registerTool) {
122
122
  const cgevent = resolveHelperPath(["native", "cgevent", "cgevent-helper"]);
123
123
  const ocr = resolveHelperPath(["native", "ocr", "ocr-helper"]);
124
124
  const windowlist = resolveHelperPath(["native", "windowlist", "windowlist-helper"]);
125
+ const skylight = resolveHelperPath(["native", "skylight", "skylight-helper"]);
125
126
  nativeHelpers = {
126
127
  cgevent: { ok: cgevent.path !== null, path: cgevent.path, tried: cgevent.tried },
127
128
  ocr: { ok: ocr.path !== null, path: ocr.path, tried: ocr.tried },
128
129
  windowlist: { ok: windowlist.path !== null, path: windowlist.path, tried: windowlist.tried },
130
+ skylight: { ok: skylight.path !== null, path: skylight.path, tried: skylight.tried },
129
131
  };
130
132
  }
131
133
  let readiness = "ready";
@@ -153,6 +155,11 @@ export function registerAppTools(registerTool) {
153
155
  readiness = readiness === "ready" ? "degraded" : readiness;
154
156
  issues.push("Native windowlist helper not found (window enumeration will fall back to slow JXA). Run `npm run build` to compile it.");
155
157
  }
158
+ if (!nativeHelpers.skylight.ok) {
159
+ // skylight missing → per-process posting unavailable; input falls back to
160
+ // HID-tap (moves cursor). Note as info, not degraded, since HID-tap still works.
161
+ issues.push("Native skylight helper not found (per-process event posting unavailable — clicks/typing will move the cursor via HID-tap). Run `npm run build` to compile it.");
162
+ }
156
163
  }
157
164
  const electronHint = "If the target app is Electron (e.g. CC Switch, VS Code, Discord), list_windows may return [] even with Accessibility granted to your terminal. Grant Accessibility to the Electron app itself in System Settings > Privacy & Security > Accessibility, and restart the app. Pixel-level workaround: use screenshot + ocr to locate UI elements by text, then click(x, y) at the detected bounding box coordinates. Alternatively, modify the app\'s config file or database directly.";
158
165
  const clients = {};
@@ -1,5 +1,11 @@
1
1
  import { z } from "zod";
2
2
  import { getPlatform, resolvePoint, getSafetyContext, withSafety, actionResponse, captureAfterFields, } from "./helpers.js";
3
+ /** Warning text emitted when an input action had to fall back to HID-tap (moves the cursor). */
4
+ const HID_TAP_WARNING = "Event dispatched via global HID tap (moved the cursor / may disturb foreground). This happens for frontmost or canvas/GPU apps where per-process posting is filtered.";
5
+ function dispatchWarnings(dispatch) {
6
+ // undefined means JXA fallback (HID-tap) or no target — warn in both cases.
7
+ return dispatch === "per-pid" ? [] : [HID_TAP_WARNING];
8
+ }
3
9
  export function registerInputTools(registerTool) {
4
10
  registerTool("click", "Click at screen coordinates", {
5
11
  x: z.number().describe("X coordinate"), y: z.number().describe("Y coordinate"),
@@ -9,8 +15,8 @@ export function registerInputTools(registerTool) {
9
15
  }, async (params) => {
10
16
  const pt = await resolvePoint(params.x, params.y, params.windowId);
11
17
  const safetyCtx = await getSafetyContext(params.windowId);
12
- await withSafety({ action: "click", params: { x: pt.x, y: pt.y, ...safetyCtx }, requiresAccessibility: true, execute: () => getPlatform().click(pt.x, pt.y, params.button) });
13
- 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);
18
+ const dispatch = await withSafety({ action: "click", params: { x: pt.x, y: pt.y, ...safetyCtx }, requiresAccessibility: true, execute: () => getPlatform().click(pt.x, pt.y, params.button) });
19
+ return actionResponse("click", { clicked: true, x: pt.x, y: pt.y, dispatch: dispatch ?? "hid-tap" }, { x: pt.x, y: pt.y, windowId: params.windowId }, params.captureAfter, params.captureFormat, params.captureMaxWidth, dispatchWarnings(dispatch));
14
20
  });
15
21
  registerTool("double_click", "Double-click at screen coordinates", {
16
22
  x: z.number().describe("X coordinate"), y: z.number().describe("Y coordinate"),
@@ -20,8 +26,8 @@ export function registerInputTools(registerTool) {
20
26
  }, async (params) => {
21
27
  const pt = await resolvePoint(params.x, params.y, params.windowId);
22
28
  const safetyCtx = await getSafetyContext(params.windowId);
23
- await withSafety({ action: "double_click", params: { x: pt.x, y: pt.y, doubleClick: true, ...safetyCtx }, requiresAccessibility: true, execute: () => getPlatform().click(pt.x, pt.y, params.button, true) });
24
- 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);
29
+ const dispatch = await withSafety({ action: "double_click", params: { x: pt.x, y: pt.y, doubleClick: true, ...safetyCtx }, requiresAccessibility: true, execute: () => getPlatform().click(pt.x, pt.y, params.button, true) });
30
+ return actionResponse("double_click", { doubleClicked: true, x: pt.x, y: pt.y, dispatch: dispatch ?? "hid-tap" }, { x: pt.x, y: pt.y, windowId: params.windowId }, params.captureAfter, params.captureFormat, params.captureMaxWidth, dispatchWarnings(dispatch));
25
31
  });
26
32
  registerTool("scroll", "Scroll at coordinates", {
27
33
  x: z.number().describe("X coordinate"), y: z.number().describe("Y coordinate"),
@@ -32,8 +38,8 @@ export function registerInputTools(registerTool) {
32
38
  const pt = await resolvePoint(params.x, params.y, params.windowId);
33
39
  const deltaX = params.deltaX ?? 0;
34
40
  const safetyCtx = await getSafetyContext(params.windowId);
35
- await withSafety({ action: "scroll", params: { x: pt.x, y: pt.y, ...safetyCtx }, requiresAccessibility: true, execute: () => getPlatform().scroll(pt.x, pt.y, deltaX, params.deltaY) });
36
- 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);
41
+ const dispatch = await withSafety({ action: "scroll", params: { x: pt.x, y: pt.y, ...safetyCtx }, requiresAccessibility: true, execute: () => getPlatform().scroll(pt.x, pt.y, deltaX, params.deltaY) });
42
+ return actionResponse("scroll", { scrolled: true, x: pt.x, y: pt.y, dispatch: dispatch ?? "hid-tap" }, { x: pt.x, y: pt.y, windowId: params.windowId }, params.captureAfter, params.captureFormat, params.captureMaxWidth, dispatchWarnings(dispatch));
37
43
  });
38
44
  registerTool("drag", "Drag from one point to another", {
39
45
  startX: z.number().describe("Start X"), startY: z.number().describe("Start Y"),
@@ -46,8 +52,8 @@ export function registerInputTools(registerTool) {
46
52
  const start = await resolvePoint(params.startX, params.startY, params.windowId);
47
53
  const end = await resolvePoint(params.endX, params.endY, params.windowId);
48
54
  const safetyCtx = await getSafetyContext(params.windowId);
49
- 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) });
50
- 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);
55
+ const dispatch = 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) });
56
+ return actionResponse("drag", { dragged: true, startX: start.x, startY: start.y, endX: end.x, endY: end.y, dispatch: dispatch ?? "hid-tap" }, { startX: start.x, startY: start.y, endX: end.x, endY: end.y, windowId: params.windowId }, params.captureAfter, params.captureFormat, params.captureMaxWidth, dispatchWarnings(dispatch));
51
57
  });
52
58
  registerTool("move", "Move cursor to coordinates", {
53
59
  x: z.number().describe("X coordinate"), y: z.number().describe("Y coordinate"),
@@ -56,8 +62,8 @@ export function registerInputTools(registerTool) {
56
62
  }, async (params) => {
57
63
  const pt = await resolvePoint(params.x, params.y, params.windowId);
58
64
  const safetyCtx = await getSafetyContext(params.windowId);
59
- await withSafety({ action: "move", params: { x: pt.x, y: pt.y, ...safetyCtx }, requiresAccessibility: true, execute: () => getPlatform().move(pt.x, pt.y) });
60
- 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);
65
+ const dispatch = await withSafety({ action: "move", params: { x: pt.x, y: pt.y, ...safetyCtx }, requiresAccessibility: true, execute: () => getPlatform().move(pt.x, pt.y) });
66
+ return actionResponse("move", { moved: true, x: pt.x, y: pt.y, dispatch: dispatch ?? "hid-tap" }, { x: pt.x, y: pt.y, windowId: params.windowId }, params.captureAfter, params.captureFormat, params.captureMaxWidth, dispatchWarnings(dispatch));
61
67
  });
62
68
  registerTool("get_cursor_position", "Get current cursor position", {}, async () => {
63
69
  const pos = await withSafety({ action: "get_cursor_position", params: {}, execute: () => Promise.resolve(getPlatform().getCursorPosition()) });
@@ -1,6 +1,10 @@
1
1
  import { z } from "zod";
2
2
  import { UnsupportedParameterError } from "../../util/errors.js";
3
3
  import { getPlatform, getSafetyContext, withSafety, actionResponse, captureAfterFields, } from "./helpers.js";
4
+ function keyDispatchWarnings(dispatch) {
5
+ // undefined means JXA fallback (HID-tap) or no target — warn in both cases.
6
+ return dispatch === "per-pid" ? [] : ["Key event dispatched via global HID tap (no target pid available; may affect foreground). Use focus_app first to enable per-process posting."];
7
+ }
4
8
  export function registerKeyboardTools(registerTool) {
5
9
  registerTool("type_text", "Type text at the current cursor position", {
6
10
  text: z.string().describe("Text to type"), delay: z.number().optional().describe("Delay between keystrokes in ms"),
@@ -10,8 +14,8 @@ export function registerKeyboardTools(registerTool) {
10
14
  if (params.windowId)
11
15
  throw new UnsupportedParameterError("windowId-targeted keyboard typing is not implemented");
12
16
  const safetyCtx = await getSafetyContext();
13
- await withSafety({ action: "type_text", params: { text: params.text, ...safetyCtx }, requiresAccessibility: true, execute: () => getPlatform().type(params.text, params.delay) });
14
- return actionResponse("type_text", { typed: true, charCount: params.text.length }, {}, params.captureAfter, params.captureFormat, params.captureMaxWidth);
17
+ const dispatch = await withSafety({ action: "type_text", params: { text: params.text, ...safetyCtx }, requiresAccessibility: true, execute: () => getPlatform().type(params.text, params.delay) });
18
+ return actionResponse("type_text", { typed: true, charCount: params.text.length, dispatch: dispatch ?? "hid-tap" }, {}, params.captureAfter, params.captureFormat, params.captureMaxWidth, keyDispatchWarnings(dispatch));
15
19
  });
16
20
  registerTool("press_key", "Press a keyboard shortcut", {
17
21
  keys: z.array(z.string()).optional().describe("Keys to press simultaneously — special keys (enter/escape/tab/f1-f12/arrows...), single letters a-z, or single digits 0-9"),
@@ -29,7 +33,7 @@ export function registerKeyboardTools(registerTool) {
29
33
  if (keys.length === 0)
30
34
  throw new UnsupportedParameterError("press_key requires at least one key");
31
35
  const safetyCtx = await getSafetyContext();
32
- await withSafety({ action: "press_key", params: { keys, ...safetyCtx }, requiresAccessibility: true, execute: () => getPlatform().key(keys) });
33
- return actionResponse("press_key", { pressed: true, keys }, {}, params.captureAfter, params.captureFormat, params.captureMaxWidth);
36
+ const dispatch = await withSafety({ action: "press_key", params: { keys, ...safetyCtx }, requiresAccessibility: true, execute: () => getPlatform().key(keys) });
37
+ return actionResponse("press_key", { pressed: true, keys, dispatch: dispatch ?? "hid-tap" }, {}, params.captureAfter, params.captureFormat, params.captureMaxWidth, keyDispatchWarnings(dispatch));
34
38
  });
35
39
  }
@@ -31,6 +31,8 @@ export interface WindowInfo {
31
31
  };
32
32
  isMinimized: boolean;
33
33
  isOnScreen: boolean;
34
+ /** Real CGWindowID (kCGWindowNumber) — needed for per-process event posting (focus-without-raise). Undefined for JXA-fallback enumeration. */
35
+ windowNumber?: number;
34
36
  }
35
37
  export interface AppInfo {
36
38
  name: string;
@@ -45,6 +47,8 @@ export interface AppTarget {
45
47
  windowId?: string;
46
48
  title?: string;
47
49
  capturedAt: string;
50
+ /** Real CGWindowID for per-process event posting (focus-without-raise). Absent for tray targets. */
51
+ windowNumber?: number;
48
52
  }
49
53
  export interface BrowserContext {
50
54
  appName: string;
@@ -162,6 +166,13 @@ export interface ScreenDescription {
162
166
  message: string;
163
167
  }>;
164
168
  }
169
+ /**
170
+ * How an input event was dispatched. "per-pid" = posted to the target process
171
+ * via SLEventPostToPid/CGEventPostToPid (no global cursor move, no foreground
172
+ * theft — Codex-style background operation). "hid-tap" = posted to the global
173
+ * HID event tap (moves the cursor; fallback when no pid or skylight unavailable).
174
+ */
175
+ export type DispatchMethod = "per-pid" | "hid-tap";
165
176
  export interface Platform {
166
177
  screenshot(display?: number, region?: ScreenRegion, options?: ScreenshotOptions): Promise<Buffer>;
167
178
  screenshotWindow?(windowId: string, options?: ScreenshotOptions): Promise<Buffer>;
@@ -171,14 +182,14 @@ export interface Platform {
171
182
  getActiveBrowserContext?(app?: string): Promise<BrowserContext | undefined>;
172
183
  listWindows(includeMinimized?: boolean): Promise<WindowInfo[]>;
173
184
  getWindowState(windowId?: string, depth?: number, includeBounds?: boolean): Promise<WindowState>;
174
- click(x: number, y: number, button?: "left" | "right" | "middle", doubleClick?: boolean): Promise<void>;
175
- move(x: number, y: number): Promise<void>;
176
- drag(startX: number, startY: number, endX: number, endY: number, button?: "left" | "right" | "middle", duration?: number): Promise<void>;
177
- scroll(x: number, y: number, deltaX: number, deltaY: number): Promise<void>;
185
+ click(x: number, y: number, button?: "left" | "right" | "middle", doubleClick?: boolean): Promise<DispatchMethod | void>;
186
+ move(x: number, y: number): Promise<DispatchMethod | void>;
187
+ drag(startX: number, startY: number, endX: number, endY: number, button?: "left" | "right" | "middle", duration?: number): Promise<DispatchMethod | void>;
188
+ scroll(x: number, y: number, deltaX: number, deltaY: number): Promise<DispatchMethod | void>;
178
189
  getCursorPosition(): CursorPosition;
179
190
  ocr(display?: number, region?: ScreenRegion): Promise<OcrResult>;
180
- type(text: string, delay?: number): Promise<void>;
181
- key(keys: string[]): Promise<void>;
191
+ type(text: string, delay?: number): Promise<DispatchMethod | void>;
192
+ key(keys: string[]): Promise<DispatchMethod | void>;
182
193
  findElement(options: FindElementOptions): Promise<FindElementResponse>;
183
194
  clickElement(elementId: string, app?: string): Promise<ClickResult>;
184
195
  typeInElement(elementId: string, text: string, app?: string, clearFirst?: boolean): Promise<void>;
@@ -2,7 +2,7 @@ import type { Platform, WindowInfo, AppTarget } from "../base.js";
2
2
  import type { CachedElementDescriptor, MacOSPlatformOptions } from "./helpers.js";
3
3
  import { saveFocus, restoreFocus } from "./focus.js";
4
4
  import { screenshot, screenshotWindow, getScreenSize, isScreenLocked, ocr } from "./screen.js";
5
- import { listApps, focusApp, getActiveBrowserContext, listWindows } from "./window.js";
5
+ import { listApps, focusApp, getActiveBrowserContext, listWindows, keepTargetAxAlive } from "./window.js";
6
6
  import { getWindowState, findElement } from "./ax-tree.js";
7
7
  import { click, move, drag, scroll, getCursorPosition, type as typeMethod, key } from "./input.js";
8
8
  import { clickElement, typeInElement, setElementValue, findMenuBarExtra, clickMenuBarExtra } from "./element.js";
@@ -39,6 +39,7 @@ export declare class MacOSPlatform implements Platform {
39
39
  listApps: typeof listApps;
40
40
  focusApp: typeof focusApp;
41
41
  getActiveBrowserContext: typeof getActiveBrowserContext;
42
+ keepTargetAxAlive: typeof keepTargetAxAlive;
42
43
  listWindows: typeof listWindows;
43
44
  getWindowState: typeof getWindowState;
44
45
  findElement: typeof findElement;
@@ -1,7 +1,7 @@
1
1
  import { TargetStaleError } from "../../util/errors.js";
2
2
  import { saveFocus, restoreFocus } from "./focus.js";
3
3
  import { screenshot, screenshotWindow, getScreenSize, isScreenLocked, ocr } from "./screen.js";
4
- import { listApps, focusApp, getActiveBrowserContext, listWindows } from "./window.js";
4
+ import { listApps, focusApp, getActiveBrowserContext, listWindows, keepTargetAxAlive } from "./window.js";
5
5
  import { getWindowState, findElement } from "./ax-tree.js";
6
6
  import { click, move, drag, scroll, getCursorPosition, type as typeMethod, key } from "./input.js";
7
7
  import { clickElement, typeInElement, setElementValue, findMenuBarExtra, clickMenuBarExtra } from "./element.js";
@@ -78,6 +78,7 @@ export class MacOSPlatform {
78
78
  listApps = listApps;
79
79
  focusApp = focusApp;
80
80
  getActiveBrowserContext = getActiveBrowserContext;
81
+ keepTargetAxAlive = keepTargetAxAlive;
81
82
  listWindows = listWindows;
82
83
  getWindowState = getWindowState;
83
84
  findElement = findElement;
@@ -14,6 +14,8 @@ export interface MenuBarExtraItem {
14
14
  height: number;
15
15
  /** Which process hosts this status item. "self" = app's own menu bar; "systemuiserver" = third-party tray hosted by SystemUIServer. */
16
16
  host: "self" | "systemuiserver";
17
+ /** The hosting process pid — app's own pid for host:"self", SystemUIServer's pid for host:"systemuiserver". Enables per-process event posting. */
18
+ pid: number;
17
19
  }
18
20
  export interface MenuBarExtraSelector {
19
21
  description?: string;
@@ -44,23 +44,16 @@ const JXA_STATE_SIGNATURE = `
44
44
  function spinMs(ms) { var t = Date.now(); while (Date.now() - t < ms) {} }
45
45
  `;
46
46
  /**
47
- * Shared JXA snippet: perform a coordinate click at the element's bounds center.
48
- * Returns the {x, y} used, or null if bounds could not be read.
47
+ * Shared JXA snippet: compute the element's bounds center (for coordinate fallback).
48
+ * Does NOT post any event the actual click is performed by the TS input layer
49
+ * (this.click) so it routes through per-process posting (skylight-helper) when a
50
+ * pid is available, instead of the JXA HID-tap that would move the global cursor.
49
51
  */
50
- const JXA_COORDINATE_CLICK = `
51
- function coordinateClick(elem) {
52
+ const JXA_BOUNDS_CENTER = `
53
+ function boundsCenter(elem) {
52
54
  var pos = elem.position();
53
55
  var sz = elem.size();
54
- var cx = pos[0] + sz[0] / 2;
55
- var cy = pos[1] + sz[1] / 2;
56
- ObjC.import('CoreGraphics');
57
- var src = $.CGEventSourceCreate($.kCGEventSourceStateHIDSystemState);
58
- var pt = $.CGPointMake(cx, cy);
59
- var down = $.CGEventCreateMouseEvent(src, $.kCGEventLeftMouseDown, pt, $.kCGMouseButtonLeft);
60
- $.CGEventPost($.kCGHIDEventTap, down);
61
- var up = $.CGEventCreateMouseEvent(src, $.kCGEventLeftMouseUp, pt, $.kCGMouseButtonLeft);
62
- $.CGEventPost($.kCGHIDEventTap, up);
63
- return {x: cx, y: cy};
56
+ return {x: pos[0] + sz[0] / 2, y: pos[1] + sz[1] / 2};
64
57
  }
65
58
  `;
66
59
  export async function clickElement(elementId, app) {
@@ -75,7 +68,7 @@ export async function clickElement(elementId, app) {
75
68
  var _result = null;
76
69
  ${jxaElementActionHelpers()}
77
70
  ${JXA_STATE_SIGNATURE}
78
- ${JXA_COORDINATE_CLICK}
71
+ ${JXA_BOUNDS_CENTER}
79
72
  var elemPath = ${elementIdLiteral};
80
73
  var appName = ${appLiteral};
81
74
  // 容忍大小写/空格/连字符/下划线变体(cc-switch vs CC Switch vs cc_switch)
@@ -95,15 +88,11 @@ export async function clickElement(elementId, app) {
95
88
  if (!elem) {
96
89
  _result = {success: false, error: "Element not found: " + elemPath};
97
90
  } else if (preferCoord) {
98
- // 启发式命中(Tauri 等已知静默吞 AXPress 的应用):直接坐标点击
99
- try {
100
- coordinateClick(elem);
101
- _result = {success: true, method: "coordinate", verified: false};
102
- } catch(e) {
103
- _result = {success: false, error: "Could not click element (coordinate): " + String(e.message || e)};
104
- }
91
+ // 启发式命中(Tauri 等已知静默吞 AXPress 的应用):请求坐标点击(TS 层执行 per-process)
92
+ var c = boundsCenter(elem);
93
+ _result = {success: true, needCoordinateClick: true, cx: c.x, cy: c.y};
105
94
  } else {
106
- // AXPress + verify:采样前后状态签名,无变化则降级坐标
95
+ // AXPress + verify:采样前后状态签名,无变化则请求坐标点击
107
96
  var sigBefore = stateSignature(elem);
108
97
  var axpressThrew = false;
109
98
  try {
@@ -112,57 +101,50 @@ export async function clickElement(elementId, app) {
112
101
  axpressThrew = true;
113
102
  }
114
103
  if (axpressThrew) {
115
- // AXPress 抛异常 → 坐标 fallback
116
- try {
117
- coordinateClick(elem);
118
- _result = {success: true, method: "coordinate", verified: false};
119
- } catch(e2) {
120
- _result = {success: false, error: "Could not click element: " + String(e2.message || e2)};
121
- }
104
+ var c1 = boundsCenter(elem);
105
+ _result = {success: true, needCoordinateClick: true, cx: c1.x, cy: c1.y};
122
106
  } else {
123
- // AXPress 未抛异常:spin 等待异步生效,再采样签名判断是否真的生效
124
107
  spinMs(80);
125
108
  var sigAfter = stateSignature(elem);
126
109
  if (sigBefore !== '' && sigAfter !== sigBefore) {
127
- // 状态变化 → AXPress 生效
128
110
  _result = {success: true, method: "axpress", verified: true};
129
111
  } else if (sigBefore === '' && sigAfter === '') {
130
- // 无可观测状态(纯按钮无 value/focused/selected):无法 verify,
131
- // 保守认为 AXPress 可能成功(不降级坐标,避免对正常按钮误伤)
132
112
  _result = {success: true, method: "axpress", verified: false};
133
113
  } else {
134
- // 有可观测状态但无变化 静默吞,降级坐标
135
- try {
136
- coordinateClick(elem);
137
- _result = {success: true, method: "coordinate", verified: false};
138
- } catch(e3) {
139
- _result = {success: false, error: "Could not click element (coordinate fallback): " + String(e3.message || e3)};
140
- }
114
+ var c2 = boundsCenter(elem);
115
+ _result = {success: true, needCoordinateClick: true, cx: c2.x, cy: c2.y};
141
116
  }
142
117
  }
143
118
  }
144
119
  JSON.stringify(_result);
145
120
  `;
121
+ let result;
146
122
  try {
147
123
  const out = execFileSync("osascript", [
148
124
  "-l", "JavaScript",
149
125
  "-e", jxaScript,
150
126
  ], { encoding: "utf-8", timeout: 15000 }).trim();
151
- const result = JSON.parse(out);
152
- if (!result.success) {
153
- throw result.error
154
- ? new Error(result.error)
155
- : new ElementNotFoundError(elementId);
156
- }
157
- return {
158
- method: result.method ?? "axpress",
159
- verified: Boolean(result.verified),
160
- };
127
+ result = JSON.parse(out);
161
128
  }
162
129
  catch (error) {
163
130
  rethrowElementActionError(error, "click_element", elementId);
164
131
  return { method: "axpress", verified: false }; // unreachable — rethrow throws
165
132
  }
133
+ if (!result.success) {
134
+ // Route through rethrowElementActionError so "element not found" / accessibility
135
+ // errors are converted to the proper UcuError subclasses (ElementNotFoundError etc.).
136
+ rethrowElementActionError(new Error(result.error || "click_element failed"), "click_element", elementId);
137
+ }
138
+ // JXA requested a coordinate click → perform it via the input layer (per-process
139
+ // when a pid is available, so the global cursor does not move).
140
+ if (result.needCoordinateClick && typeof result.cx === "number" && typeof result.cy === "number") {
141
+ await this.click(Math.round(result.cx), Math.round(result.cy), "left", false);
142
+ return { method: "coordinate", verified: false };
143
+ }
144
+ return {
145
+ method: result.method ?? "axpress",
146
+ verified: Boolean(result.verified),
147
+ };
166
148
  }
167
149
  export async function typeInElement(elementId, text, app, clearFirst) {
168
150
  const textLiteral = JSON.stringify(text);
@@ -324,7 +306,7 @@ export async function findMenuBarExtra(app) {
324
306
  return itemNorm === appNorm || itemNorm.indexOf(appNorm) !== -1 || appNorm.indexOf(itemNorm) !== -1;
325
307
  };
326
308
 
327
- var _readItem = function(item, mb, i, host) {
309
+ var _readItem = function(item, mb, i, host, hostPid) {
328
310
  var desc = '', nm = '';
329
311
  try { desc = item.description(); } catch(e) {}
330
312
  try { nm = item.name(); } catch(e) {}
@@ -332,7 +314,7 @@ export async function findMenuBarExtra(app) {
332
314
  try { pos = item.position(); } catch(e) {}
333
315
  try { sz = item.size(); } catch(e) {}
334
316
  if (sz[0] === 0 && sz[1] === 0) return null;
335
- return {menuBar: mb, index: i, name: nm, description: desc, x: pos[0], y: pos[1], width: sz[0], height: sz[1], host: host};
317
+ return {menuBar: mb, index: i, name: nm, description: desc, x: pos[0], y: pos[1], width: sz[0], height: sz[1], host: host, pid: hostPid || 0};
336
318
  };
337
319
 
338
320
  try {
@@ -346,7 +328,9 @@ export async function findMenuBarExtra(app) {
346
328
 
347
329
  var items = [];
348
330
 
349
- // 阶段 1:app 自身 menuBarItems(host:"self")
331
+ // 阶段 1:app 自身 menuBarItems(host:"self",用 app 自身 pid
332
+ var appPid = 0;
333
+ if (p) { try { appPid = p.unixId(); } catch(e) {} }
350
334
  if (p) {
351
335
  try {
352
336
  var menuBars = p.menuBars();
@@ -354,7 +338,7 @@ export async function findMenuBarExtra(app) {
354
338
  var mbItems;
355
339
  try { mbItems = menuBars[mb].menuBarItems(); } catch(e) { continue; }
356
340
  for (var i = 0; i < mbItems.length; i++) {
357
- var rec = _readItem(mbItems[i], mb, i, "self");
341
+ var rec = _readItem(mbItems[i], mb, i, "self", appPid);
358
342
  if (rec) items.push(rec);
359
343
  }
360
344
  }
@@ -372,6 +356,8 @@ export async function findMenuBarExtra(app) {
372
356
  if (!hasNonApple) {
373
357
  try {
374
358
  var suiProcs = se.processes.byName("SystemUIServer");
359
+ var suiPid = 0;
360
+ if (suiProcs) { try { suiPid = suiProcs.unixId(); } catch(e) {} }
375
361
  if (suiProcs) {
376
362
  var suiBars = suiProcs.menuBars();
377
363
  for (var smb = 0; smb < suiBars.length; smb++) {
@@ -384,7 +370,7 @@ export async function findMenuBarExtra(app) {
384
370
  try { sNm = sItem.name(); } catch(e) {}
385
371
  // 按 description/name 匹配目标 app(status item 的 description 通常是 app 名)
386
372
  if (_matchApp(_norm(sDesc)) || _matchApp(_norm(sNm))) {
387
- var sRec = _readItem(sItem, smb, si, "systemuiserver");
373
+ var sRec = _readItem(sItem, smb, si, "systemuiserver", suiPid);
388
374
  if (sRec) {
389
375
  // 保留匹配信号,供 click 二次定位
390
376
  sRec.name = sNm; sRec.description = sDesc;
@@ -499,7 +485,7 @@ export async function clickMenuBarExtra(app, selector = {}) {
499
485
  var tgtName = ${tgtNameLiteral};
500
486
  var tgtDesc = ${tgtDescLiteral};
501
487
  ${JXA_STATE_SIGNATURE}
502
- ${JXA_COORDINATE_CLICK}
488
+ ${JXA_BOUNDS_CENTER}
503
489
  // 容忍大小写/空格/连字符/下划线变体(cc-switch vs CC Switch vs cc_switch)
504
490
  var _norm = function(s) { return String(s||'').toLowerCase().split(' ').join('').split('-').join('').split('_').join(''); };
505
491
  var appNorm = _norm(appName);
@@ -521,9 +507,9 @@ export async function clickMenuBarExtra(app, selector = {}) {
521
507
  ${resolveItemBlock}
522
508
  if (!_result && item) {
523
509
  if (preferCoord) {
524
- // 启发式命中:直接坐标点击
525
- coordinateClick(item);
526
- _result = {success: true, method: "coordinate", verified: false};
510
+ // 启发式命中:请求坐标点击(TS 层执行 per-process)
511
+ var c0 = boundsCenter(item);
512
+ _result = {success: true, needCoordinateClick: true, cx: c0.x, cy: c0.y};
527
513
  } else {
528
514
  // AXPress + verify
529
515
  var sigBefore = stateSignature(item);
@@ -534,20 +520,18 @@ export async function clickMenuBarExtra(app, selector = {}) {
534
520
  axpressThrew = true;
535
521
  }
536
522
  if (axpressThrew) {
537
- coordinateClick(item);
538
- _result = {success: true, method: "coordinate", verified: false};
523
+ var c1 = boundsCenter(item);
524
+ _result = {success: true, needCoordinateClick: true, cx: c1.x, cy: c1.y};
539
525
  } else {
540
526
  spinMs(80);
541
527
  var sigAfter = stateSignature(item);
542
528
  if (sigBefore !== '' && sigAfter !== sigBefore) {
543
529
  _result = {success: true, method: "axpress", verified: true};
544
530
  } else if (sigBefore === '' && sigAfter === '') {
545
- // 托盘 status item 通常无可观测状态(无 value/selected);AXPress 开菜单的效果
546
- // 难以从 item 本身验证。保守认为成功(菜单是否打开由后续 find_element 判断)。
547
531
  _result = {success: true, method: "axpress", verified: false};
548
532
  } else {
549
- coordinateClick(item);
550
- _result = {success: true, method: "coordinate", verified: false};
533
+ var c2 = boundsCenter(item);
534
+ _result = {success: true, needCoordinateClick: true, cx: c2.x, cy: c2.y};
551
535
  }
552
536
  }
553
537
  }
@@ -569,6 +553,10 @@ export async function clickMenuBarExtra(app, selector = {}) {
569
553
  if (!result.success) {
570
554
  throw new Error(`click_menu_bar_extra failed in ${app}: ${result.error}`);
571
555
  }
556
+ if (result.needCoordinateClick && typeof result.cx === "number" && typeof result.cy === "number") {
557
+ await this.click(Math.round(result.cx), Math.round(result.cy), "left", false);
558
+ return { method: "coordinate", verified: false };
559
+ }
572
560
  return {
573
561
  method: result.method ?? "axpress",
574
562
  verified: Boolean(result.verified),
@@ -1,9 +1,10 @@
1
1
  import type { MacOSPlatform } from "./base.js";
2
2
  import type { CursorPosition } from "../base.js";
3
- export declare function click(this: MacOSPlatform, x: number, y: number, button?: "left" | "right" | "middle", doubleClick?: boolean): Promise<void>;
4
- export declare function move(this: MacOSPlatform, x: number, y: number): Promise<void>;
5
- export declare function drag(this: MacOSPlatform, startX: number, startY: number, endX: number, endY: number, button?: "left" | "right" | "middle", duration?: number): Promise<void>;
6
- export declare function scroll(this: MacOSPlatform, x: number, y: number, deltaX: number, deltaY: number): Promise<void>;
3
+ import type { DispatchMethod } from "../base.js";
4
+ export declare function click(this: MacOSPlatform, x: number, y: number, button?: "left" | "right" | "middle", doubleClick?: boolean): Promise<DispatchMethod | void>;
5
+ export declare function move(this: MacOSPlatform, x: number, y: number): Promise<DispatchMethod | void>;
6
+ export declare function drag(this: MacOSPlatform, startX: number, startY: number, endX: number, endY: number, button?: "left" | "right" | "middle", duration?: number): Promise<DispatchMethod | void>;
7
+ export declare function scroll(this: MacOSPlatform, x: number, y: number, deltaX: number, deltaY: number): Promise<DispatchMethod | void>;
7
8
  export declare function getCursorPosition(this: MacOSPlatform): CursorPosition;
8
- export declare function type(this: MacOSPlatform, text: string, delay?: number): Promise<void>;
9
- export declare function key(this: MacOSPlatform, keys: string[]): Promise<void>;
9
+ export declare function type(this: MacOSPlatform, text: string, delay?: number): Promise<DispatchMethod | void>;
10
+ export declare function key(this: MacOSPlatform, keys: string[]): Promise<DispatchMethod | void>;
@@ -2,13 +2,37 @@ import { execFileSync } from "node:child_process";
2
2
  import { click as inputClick, doubleClick as inputDoubleClick, move as inputMove, drag as inputDrag, scroll as inputScroll, typeText, pressShortcut } from "../../utils/input.js";
3
3
  import { PlatformError } from "../../util/errors.js";
4
4
  import { rethrowInputError, errorMessage } from "./helpers.js";
5
+ /**
6
+ * Process names that must NEVER receive per-process input injection (security).
7
+ * Per-process posting bypasses the frontmost/focus requirement, so without this
8
+ * denylist an attacker could deliver keystrokes/clicks directly into a password
9
+ * manager or auth dialog. These fall back to HID-tap (which at least requires
10
+ * the app to be frontmost / visible to the user).
11
+ */
12
+ const SENSITIVE_PROCESS_HINTS = [
13
+ "keychainaccess", "1password", "lastpass", "bitwarden", "securityagent",
14
+ "authd", "loginwindow", "applekeychain", "dashlane",
15
+ ];
16
+ /** Resolve the per-process event target from the active focus_app target (pid + windowNumber). */
17
+ function targetOf() {
18
+ const t = this.activeTarget;
19
+ if (!t || !t.pid || t.pid <= 0)
20
+ return undefined;
21
+ // Security: never route per-process events into sensitive apps (password managers / auth).
22
+ const name = (t.appName || "").toLowerCase();
23
+ if (SENSITIVE_PROCESS_HINTS.some((h) => name.includes(h))) {
24
+ return undefined; // forces HID-tap fallback (requires frontmost)
25
+ }
26
+ return { pid: t.pid, windowNumber: t.windowNumber };
27
+ }
5
28
  export async function click(x, y, button, doubleClick) {
6
29
  try {
30
+ const target = targetOf.call(this);
7
31
  if (doubleClick) {
8
- await inputDoubleClick(x, y, button);
32
+ return await inputDoubleClick(x, y, button, process.platform, target);
9
33
  }
10
34
  else {
11
- await inputClick(x, y, button);
35
+ return await inputClick(x, y, button, process.platform, target);
12
36
  }
13
37
  }
14
38
  catch (error) {
@@ -17,7 +41,7 @@ export async function click(x, y, button, doubleClick) {
17
41
  }
18
42
  export async function move(x, y) {
19
43
  try {
20
- await inputMove(x, y);
44
+ return await inputMove(x, y, process.platform, targetOf.call(this));
21
45
  }
22
46
  catch (error) {
23
47
  rethrowInputError(error, "move");
@@ -25,7 +49,7 @@ export async function move(x, y) {
25
49
  }
26
50
  export async function drag(startX, startY, endX, endY, button, duration) {
27
51
  try {
28
- await inputDrag(startX, startY, endX, endY, button, duration);
52
+ return await inputDrag(startX, startY, endX, endY, button, duration, process.platform, targetOf.call(this));
29
53
  }
30
54
  catch (error) {
31
55
  rethrowInputError(error, "drag");
@@ -33,7 +57,7 @@ export async function drag(startX, startY, endX, endY, button, duration) {
33
57
  }
34
58
  export async function scroll(x, y, deltaX, deltaY) {
35
59
  try {
36
- await inputScroll(x, y, deltaX, deltaY);
60
+ return await inputScroll(x, y, deltaX, deltaY, process.platform, targetOf.call(this));
37
61
  }
38
62
  catch (error) {
39
63
  rethrowInputError(error, "scroll");
@@ -55,8 +79,8 @@ export function getCursorPosition() {
55
79
  }
56
80
  }
57
81
  export async function type(text, delay) {
58
- await typeText(text, delay);
82
+ return await typeText(text, delay, process.platform, targetOf.call(this));
59
83
  }
60
84
  export async function key(keys) {
61
- await pressShortcut(keys);
85
+ return await pressShortcut(keys, process.platform, targetOf.call(this));
62
86
  }
@@ -2,5 +2,7 @@ import type { MacOSPlatform } from "./base.js";
2
2
  import type { AppInfo, AppTarget, WindowInfo, BrowserContext } from "../base.js";
3
3
  export declare function listApps(this: MacOSPlatform): Promise<AppInfo[]>;
4
4
  export declare function focusApp(this: MacOSPlatform, app: string): Promise<AppTarget>;
5
+ /** Call the skylight-helper keepAlive command for a pid (best-effort, non-throwing). */
6
+ export declare function keepTargetAxAlive(this: MacOSPlatform, pid: number): Promise<void>;
5
7
  export declare function getActiveBrowserContext(this: MacOSPlatform, app?: string): Promise<BrowserContext | undefined>;
6
8
  export declare function listWindows(this: MacOSPlatform, _includeMinimized?: boolean): Promise<WindowInfo[]>;