Saunato/dsh-mac-cua ↗★ 0

dsh-mac-cua

Computer Use for DeepSeek Harness: control macOS desktop apps through a persistent JavaScript REPL, backed by the Accessibility API. 适合需要让智能体直接控制和操作macOS系统及应用的用户。

Package
dsh-mac-cua
Compatibility
Unverified
Harness peer range
>=0.0.1-rc.1 <0.1.0 || >=0.1.0-rc.1 <0.2.0-0
Version
1.0.3
License
MIT
Last updated
Sep 16, 2026

Install

$npx -p @deepseek-ai/dsh dsh plugin --profile web add github:Saunato/dsh-mac-cua

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.