dsh-mac-cua
通过JS REPL和无障碍API控制macOS桌面 适合需要让智能体直接控制和操作macOS系统及应用的用户。
安裝
npx -p @deepseek-ai/dsh dsh plugin --profile web add github:Saunato/dsh-mac-cua說明文件
閱讀完整 README ↗Configuration
The bundle ships this row, which mounts the MCP server. dsh-cua-setup --print
shows the same row with absolute paths for a manual install:
- insert:
- id: mcp-cua
name: '@deepseek-ai/dsh-mcp-client'
config:
serverName: cua_repl
transport: stdio
command: node
args:
- '/path/to/dsh-cua/cua-repl/server.js'
cwd: '/path/to/dsh-cua'
env:
DSH_CUA_NATIVE: '/path/to/dsh-cua/native/dsh_cua.node'
toolCallTimeoutMs: 180000
failOnStartupError: true
serverName determines the tool prefix, so the tools appear as
mcp__cua_repl__js and mcp__cua_repl__js_reset.
The shipped row uses paths relative to the profile directory, so it works for
every install location. To change a setting, add an override row to your profile
patch — - id: mcp-cua plus only the fields you want to differ.
Usage
The agent writes JavaScript. State persists between calls.
// Read state. `sky` is injected as a global — do not import it.
var state = await sky.get_app_state({ app: "Finder" });
nodeRepl.write(state.text);
// Act on an element by its index, then read state again.
await sky.click({ app: "Finder", element_index: 42 });
nodeRepl.write((await sky.get_app_state({ app: "Finder" })).text);
// Read a screenshot back into the conversation.
const fs = await import("node:fs/promises");
const { fileURLToPath } = await import("node:url");
var state = await sky.get_app_state({ app: "Finder" });
if (state.screenshot) {
await nodeRepl.emitImage({
bytes: await fs.readFile(fileURLToPath(state.screenshot.url)),
mimeType: "image/png",
});
}
API
sky.get_app_state({ app, disableDiff?, screenshot? }) // -> AppState
sky.list_apps() // -> App[]
sky.get_screenshot() // -> { url, note }
sky.click({ app, element_index?, x?, y?, mouse_button?, click_count? })
sky.set_value({ app, element_index, value })
sky.select_text({ app, element_index, text, prefix?, suffix?, selection_type? })
sky.perform_secondary_action({ app, element_index, action })
sky.type_text({ app, text })
sky.press_key({ app, key })
sky.paste({ app, text, format }) // "text" | "md" | "html"
sky.scroll({ app, element_index?, x?, y?, direction, pages? })
sky.drag({ app, from_x, from_y, to_x, to_y })
app accepts a display name, bundle identifier, or full path:
"Finder", "com.apple.finder", "/Applications/Safari.app".
Behaviour worth knowing
Element indices only mean something for the snapshot that produced them.
Every get_app_state publishes a fresh map and invalidates the previous one. A
stale index is refused with a message telling the model to re-read — it is never
applied to whatever now occupies that position.
paste restores your clipboard. It writes to the pasteboard, presses Cmd+V,
then puts your previous clipboard contents back.
\n in type_text presses Return, which submits forms and sends messages in
many apps. Use paste for multi-line text.
Typing waits for keyboard focus and reads the field back, so a mismatch is reported instead of silently lost.