dsh-baton-mcp 0.3.1 → 0.5.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/README.md +48 -48
- package/README.zh-CN.md +48 -48
- package/WINDOWS.md +6 -18
- package/host/README.md +13 -5
- package/host/lib/client.js +130 -5
- package/host/lib/index.js +57 -1
- package/host/package.json +11 -4
- package/package.json +6 -3
- package/server.json +21 -0
- package/src/core.mjs +2 -0
- package/src/register.mjs +236 -0
- package/src/setup.mjs +42 -106
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
5
|
<p align="center">
|
|
6
|
-
English | <a href="README.zh-CN.md">中文</a>
|
|
6
|
+
English | <a href="README.zh-CN.md">中文</a>
|
|
7
7
|
</p>
|
|
8
8
|
|
|
9
9
|
<p align="center">
|
|
@@ -37,59 +37,18 @@ Codex and Claude take turns in the same Harness session, and every message is la
|
|
|
37
37
|
## Quick start
|
|
38
38
|
|
|
39
39
|
<p align="center">
|
|
40
|
-
<img src="./assets/readme/install.svg" width="100%" alt="Step 1:
|
|
40
|
+
<img src="./assets/readme/install.svg" width="100%" alt="Step 1: in Harness open Plugins, Add plugin, enter dsh-baton-host, Install, Enable now; it registers dsh-baton for Claude Code and Codex. Step 2: in a new chat, ask for dsh_list_workspaces">
|
|
41
41
|
</p>
|
|
42
42
|
|
|
43
|
-
Requires DeepSeek Harness ≥ 0.2.1-rc.2 (signed in), Node.js ≥ 22.15.
|
|
43
|
+
Requires DeepSeek Harness ≥ 0.2.1-rc.2 (signed in), Node.js ≥ 22.15, and Claude Code or Codex. The steps are the same on macOS and Windows.
|
|
44
44
|
|
|
45
|
-
**1.
|
|
45
|
+
**1. Add the plugin in Harness:** **Plugins** (插件) → **Add plugin** (添加插件) → type `dsh-baton-host` → **Install** (安装) → **Enable now** (立即启用).
|
|
46
46
|
|
|
47
|
-
|
|
48
|
-
npx -y dsh-baton-mcp setup
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
dsh-baton has no dependencies. `setup` copies itself to `~/.dsh-baton/app`, a folder that stays put (npx's cache does not), and registers the MCP server once, user-wide, for Claude Code and Codex: every new conversation, in any folder, has the `dsh_*` tools. It writes `~/.claude.json` and `~/.codex/config.toml` directly (a backup of each is kept as `*.bak-dsh-baton`), so it also works when only the desktop apps are installed and `claude` / `codex` are not on your `PATH`. Running it again is safe.
|
|
52
|
-
|
|
53
|
-
**2. Paste in Harness.** `setup` copies the plugin folder path to your clipboard:
|
|
54
|
-
|
|
55
|
-
**Plugins** (插件) → **Add plugin** (添加插件) → paste → **Install** (安装) → **Enable now** (立即启用)
|
|
47
|
+
Switched on, the plugin registers dsh-baton for Claude Code and Codex, for every folder, and Harness says so.
|
|
56
48
|
|
|
57
|
-
**
|
|
49
|
+
**2. Check:** in a **new** Claude Code or Codex conversation, ask it to call `dsh_list_workspaces`. It lists your Harness workspaces.
|
|
58
50
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
**How each client waits** (nothing to configure):
|
|
62
|
-
- **Claude Code** waits for a run in the background (the `wait_command` that `dsh_send` returns) and is woken once, when the run ends.
|
|
63
|
-
- **Codex** waits inside `dsh_wait`, up to 270 s per call, just under its 300 s tool timeout. An unfinished wait returns only the state, so Codex calls again without summarising. Codex gets no `wait_command`: its terminal cannot wake it, so it would poll.
|
|
64
|
-
- Codex asks you to approve MCP tool calls: approve the `dsh-baton` tools. For unattended `codex exec` runs, add `--approve-for-me`.
|
|
65
|
-
|
|
66
|
-
**Updating:** run `npx -y dsh-baton-mcp@latest setup` again, then restart Harness to load plugin changes; new conversations pick up MCP server changes.
|
|
67
|
-
|
|
68
|
-
**Removing:** `npx -y dsh-baton-mcp setup --remove`, then Harness → Plugins → dsh-baton-host → Uninstall, then `setup --remove` once more to delete `~/.dsh-baton/app`.
|
|
69
|
-
|
|
70
|
-
**Upgrading from dsh-bridge**: run `setup` as above. It replaces the old `dsh-bridge` MCP entries; in Harness, uninstall `dsh-bridge-host`, then add the new path. `DSH_BRIDGE_*` variables and an existing `~/.dsh-bridge` folder keep working for now. Tools get a new prefix (`mcp__dsh-baton__…`), so update any permission allowlists.
|
|
71
|
-
|
|
72
|
-
**From source** (to follow `main` or to change the code): `git clone https://github.com/JialaoLiu/dsh-baton.git` and `node dsh-baton/src/cli.mjs setup`. setup then registers the clone itself; update with `git pull`.
|
|
73
|
-
|
|
74
|
-
<details>
|
|
75
|
-
<summary>Manual registration and the Claude desktop app</summary>
|
|
76
|
-
|
|
77
|
-
`setup` writes the same entries these commands would (with the absolute path of `node`, since desktop apps start MCP servers without your shell's `PATH`):
|
|
78
|
-
|
|
79
|
-
```bash
|
|
80
|
-
claude mcp add dsh-baton -s user -- "$(command -v node)" "$PWD/dsh-baton/src/server.mjs"
|
|
81
|
-
codex mcp add dsh-baton -- "$(command -v node)" "$PWD/dsh-baton/src/server.mjs"
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
`-s user` matters: without it, Claude Code registers the server for the current project folder only.
|
|
85
|
-
|
|
86
|
-
**Claude desktop app (optional)**: for the chat app's connectors (separate from Claude Code), add this to `~/Library/Application Support/Claude/claude_desktop_config.json` and restart the app. It shows as a "Local dev" connector.
|
|
87
|
-
|
|
88
|
-
```json
|
|
89
|
-
{ "mcpServers": { "dsh-baton": { "command": "/absolute/path/to/node", "args": ["/absolute/path/to/dsh-baton/src/server.mjs"] } } }
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
</details>
|
|
51
|
+
That is all. If Harness says it could not register (no Node.js found, say), run `npx -y dsh-baton-mcp setup` in a terminal; it does the same registration. Updating, removing and Windows details are [further down](#install-details).
|
|
93
52
|
|
|
94
53
|
## How it works
|
|
95
54
|
|
|
@@ -101,6 +60,11 @@ codex mcp add dsh-baton -- "$(command -v node)" "$PWD/dsh-baton/src/server.mjs"
|
|
|
101
60
|
<img src="./assets/readme/comparison.svg" width="100%" alt="Plugin vs headless: only the plugin shows runs live in the Harness UI, continues desktop sessions and uses desktop agent presets; both reuse the desktop sign-in">
|
|
102
61
|
</p>
|
|
103
62
|
|
|
63
|
+
**How each client waits** (nothing to configure):
|
|
64
|
+
- **Claude Code** waits for a run in the background (the `wait_command` that `dsh_send` returns) and is woken once, when the run ends.
|
|
65
|
+
- **Codex** waits inside `dsh_wait`, up to 270 s per call, just under its 300 s tool timeout. An unfinished wait returns only the state, so Codex calls again without summarising. Codex gets no `wait_command`: its terminal cannot wake it, so it would poll.
|
|
66
|
+
- Codex asks you to approve MCP tool calls: approve the `dsh-baton` tools. For unattended `codex exec` runs, add `--approve-for-me`.
|
|
67
|
+
|
|
104
68
|
## Tools
|
|
105
69
|
|
|
106
70
|
<p align="center">
|
|
@@ -168,6 +132,42 @@ When the plugin is unreachable, the MCP server falls back to Harness's `dsh head
|
|
|
168
132
|
|
|
169
133
|
</details>
|
|
170
134
|
|
|
135
|
+
## Install details
|
|
136
|
+
|
|
137
|
+
**What the plugin does when switched on.** It copies the MCP server to `~/.dsh-baton/app`, a folder that stays put, and writes the MCP entry into `~/.claude.json` and `~/.codex/config.toml` directly, keeping a `*.bak-dsh-baton` backup of each. It registers the `node` it finds on `PATH` (never the one inside Harness), so it works with only the desktop apps installed, when `claude` / `codex` are not on your `PATH`. It runs again on each Harness start: an entry that is already right, or a working one that starts another copy of dsh-baton, is left alone. In Harness, the source switches to the npm registry ("Official npm registry", 「npm 官方源」) by itself once you type the package name, and no restart is needed. To turn the registration off, set `DSH_BATON_AUTO_REGISTER=0` for Harness.
|
|
138
|
+
|
|
139
|
+
**`npx -y dsh-baton-mcp setup`** does the same registration from a terminal: for a plugin that could not register, or when you come from an MCP directory. With the plugin already registered it changes nothing, then points you to the plugin if it is missing.
|
|
140
|
+
|
|
141
|
+
**Updating.** Harness has an update button only for its official plugins, so: Plugins → dsh-baton-host → **Uninstall**, then **Add plugin** → `dsh-baton-host`. Switched on again, the plugin brings the MCP server to the same version; new conversations use it. If the two are ever out of step, the first tool call that notices it says so.
|
|
142
|
+
|
|
143
|
+
**Removing.** Harness → Plugins → dsh-baton-host → **Uninstall**, then `npx -y dsh-baton-mcp setup --remove`. The plugin cannot tell an uninstall from Harness quitting, so it leaves the MCP entries to that command, which also deletes `~/.dsh-baton/app`; run records stay in `~/.dsh-baton` until you delete it.
|
|
144
|
+
|
|
145
|
+
**Upgrading from dsh-bridge.** In Harness, uninstall `dsh-bridge-host`, then add `dsh-baton-host`; switched on, it replaces the old `dsh-bridge` MCP entries. `DSH_BRIDGE_*` variables and an existing `~/.dsh-bridge` folder keep working for now. Tools get a new prefix (`mcp__dsh-baton__…`), so update any permission allowlists. A plugin added before 0.4 by pasting `~/.dsh-baton/app/host` still works but does not update with npm: uninstall it and add `dsh-baton-host` by name.
|
|
146
|
+
|
|
147
|
+
**From source** (to follow `main` or change the code): `git clone https://github.com/JialaoLiu/dsh-baton.git`, then in Harness add the plugin from the clone's `host` folder (Add plugin → paste the path). Switched on, it registers the clone itself, so changes run before they are published. Update with `git pull`. `node dsh-baton/src/cli.mjs setup` registers the clone from a terminal.
|
|
148
|
+
|
|
149
|
+
**Windows.** Same steps; [WINDOWS.md](WINDOWS.md) covers what differs (a named pipe instead of a socket file) and fixes for a Plugins page that cannot install or uninstall.
|
|
150
|
+
|
|
151
|
+
<details>
|
|
152
|
+
<summary>Manual registration and the Claude desktop app</summary>
|
|
153
|
+
|
|
154
|
+
The plugin and `setup` write the same entries these commands would (with the absolute path of `node`, since desktop apps start MCP servers without your shell's `PATH`):
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
claude mcp add dsh-baton -s user -- "$(command -v node)" "$PWD/dsh-baton/src/server.mjs"
|
|
158
|
+
codex mcp add dsh-baton -- "$(command -v node)" "$PWD/dsh-baton/src/server.mjs"
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
`-s user` matters: without it, Claude Code registers the server for the current project folder only.
|
|
162
|
+
|
|
163
|
+
**Claude desktop app (optional)**: for the chat app's connectors (separate from Claude Code), add this to `~/Library/Application Support/Claude/claude_desktop_config.json` and restart the app. It shows as a "Local dev" connector.
|
|
164
|
+
|
|
165
|
+
```json
|
|
166
|
+
{ "mcpServers": { "dsh-baton": { "command": "/absolute/path/to/node", "args": ["/absolute/path/to/dsh-baton/src/server.mjs"] } } }
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
</details>
|
|
170
|
+
|
|
171
171
|
## Security and limits
|
|
172
172
|
|
|
173
173
|
<p align="center">
|
package/README.zh-CN.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
5
|
<p align="center">
|
|
6
|
-
<a href="README.md">English</a> | 中文
|
|
6
|
+
<a href="README.md">English</a> | 中文
|
|
7
7
|
</p>
|
|
8
8
|
|
|
9
9
|
<p align="center">
|
|
@@ -37,59 +37,18 @@ Codex 和 Claude 轮流往同一个 Harness 对话发消息,每条都标明是
|
|
|
37
37
|
## 快速上手
|
|
38
38
|
|
|
39
39
|
<p align="center">
|
|
40
|
-
<img src="./assets/readme/zh/install.svg" width="100%" alt="
|
|
40
|
+
<img src="./assets/readme/zh/install.svg" width="100%" alt="第一步:Harness → 插件 → 添加插件 → 输入 dsh-baton-host → 安装 → 立即启用,它会为 Claude Code 和 Codex 注册 dsh-baton。第二步:新开对话,让它调用 dsh_list_workspaces">
|
|
41
41
|
</p>
|
|
42
42
|
|
|
43
|
-
需要:DeepSeek Harness ≥ 0.2.1-rc.2 并已登录;Node.js ≥ 22.15。Windows
|
|
43
|
+
需要:DeepSeek Harness ≥ 0.2.1-rc.2 并已登录;Node.js ≥ 22.15;装了 Claude Code 或 Codex。macOS 和 Windows 步骤完全相同。
|
|
44
44
|
|
|
45
|
-
**1.
|
|
45
|
+
**1. 在 Harness 里添加插件:** 插件 → 添加插件 → 输入 `dsh-baton-host` → 安装 → 立即启用。
|
|
46
46
|
|
|
47
|
-
|
|
48
|
-
npx -y dsh-baton-mcp setup
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
dsh-baton 没有任何依赖。`setup` 会把自己复制到 `~/.dsh-baton/app` 这个固定的文件夹(npx 的缓存目录会变),然后在用户级别给 Claude Code 和 Codex 注册一次 MCP 服务器:之后在任何文件夹新开的对话都有 `dsh_*` 工具。它直接写 `~/.claude.json` 和 `~/.codex/config.toml`(各留一份 `*.bak-dsh-baton` 备份),所以只装了桌面应用、`claude` / `codex` 不在 `PATH` 里也能用。重复运行也没问题。
|
|
52
|
-
|
|
53
|
-
**2. 在 Harness 里粘贴。**`setup` 已经把插件文件夹路径复制到剪贴板:
|
|
54
|
-
|
|
55
|
-
**插件** → **添加插件** → 粘贴 → **安装** → **立即启用**
|
|
47
|
+
插件启用后会为 Claude Code 和 Codex 注册 dsh-baton(对所有文件夹生效),Harness 里会弹出提示。
|
|
56
48
|
|
|
57
|
-
**
|
|
49
|
+
**2. 检查:**在 Claude Code 或 Codex 里**新开**一个对话,让它调用 `dsh_list_workspaces`,会列出你的 Harness 工作区。
|
|
58
50
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
**各客户端怎么等待**(不需要配置):
|
|
62
|
-
- **Claude Code** 在后台等待任务(`dsh_send` 返回的 `wait_command`),任务结束时只被唤醒一次。
|
|
63
|
-
- **Codex** 在 `dsh_wait` 里等待,每次最多 270 秒,刚好低于它 300 秒的工具超时。没做完时只返回状态,Codex 会直接再调用,不会去总结进度。Codex 不会收到 `wait_command`:它的终端无法在命令结束时唤醒它,只能轮询。
|
|
64
|
-
- Codex 调用 MCP 工具时会请你批准,批准 `dsh-baton` 的工具即可;无人值守的 `codex exec` 加上 `--approve-for-me`。
|
|
65
|
-
|
|
66
|
-
**更新:** 再运行一次 `npx -y dsh-baton-mcp@latest setup`,然后重启 Harness 以加载插件改动;新开的对话会用上 MCP 服务器的改动。
|
|
67
|
-
|
|
68
|
-
**卸载:** `npx -y dsh-baton-mcp setup --remove`,再到 Harness → 插件 → dsh-baton-host → 卸载,最后再运行一次 `setup --remove` 删除 `~/.dsh-baton/app`。
|
|
69
|
-
|
|
70
|
-
**从 dsh-bridge 升级:**照上面运行 `setup`。它会替换掉旧的 `dsh-bridge` MCP 注册;在 Harness 里先卸载 `dsh-bridge-host`,再添加新路径。`DSH_BRIDGE_*` 环境变量和已有的 `~/.dsh-bridge` 文件夹暂时继续可用。工具前缀变成 `mcp__dsh-baton__…`,权限白名单里写了旧前缀的要改一下。
|
|
71
|
-
|
|
72
|
-
**从源码安装**(想跟进 `main` 或改代码):`git clone https://github.com/JialaoLiu/dsh-baton.git`,再运行 `node dsh-baton/src/cli.mjs setup`。这时 setup 注册的就是克隆目录本身,更新用 `git pull`。
|
|
73
|
-
|
|
74
|
-
<details>
|
|
75
|
-
<summary>手动注册与 Claude 桌面应用</summary>
|
|
76
|
-
|
|
77
|
-
`setup` 写入的内容和下面两条命令等价(用 `node` 的绝对路径,因为桌面应用启动 MCP 服务器时没有你 shell 的 `PATH`):
|
|
78
|
-
|
|
79
|
-
```bash
|
|
80
|
-
claude mcp add dsh-baton -s user -- "$(command -v node)" "$PWD/dsh-baton/src/server.mjs"
|
|
81
|
-
codex mcp add dsh-baton -- "$(command -v node)" "$PWD/dsh-baton/src/server.mjs"
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
`-s user` 很关键:不加的话,Claude Code 只给当前项目文件夹注册。
|
|
85
|
-
|
|
86
|
-
**Claude 桌面应用(可选)**:聊天应用的连接器(和 Claude Code 是两回事),把下面这段加到 `~/Library/Application Support/Claude/claude_desktop_config.json`,然后重启应用,会显示为 "Local dev" 连接器。
|
|
87
|
-
|
|
88
|
-
```json
|
|
89
|
-
{ "mcpServers": { "dsh-baton": { "command": "/node 的绝对路径", "args": ["/dsh-baton 的绝对路径/src/server.mjs"] } } }
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
</details>
|
|
51
|
+
就这些。如果 Harness 提示没能注册(比如找不到 Node.js),在终端运行 `npx -y dsh-baton-mcp setup`,它做的是同样的注册。更新、卸载和 Windows 说明在[后面](#安装细节)。
|
|
93
52
|
|
|
94
53
|
## 工作原理
|
|
95
54
|
|
|
@@ -101,6 +60,11 @@ codex mcp add dsh-baton -- "$(command -v node)" "$PWD/dsh-baton/src/server.mjs"
|
|
|
101
60
|
<img src="./assets/readme/zh/comparison.svg" width="100%" alt="插件和 headless 对比:只有插件能在 Harness 界面实时可见、续接桌面版的对话、使用桌面版智能体预设;两者都沿用桌面版登录">
|
|
102
61
|
</p>
|
|
103
62
|
|
|
63
|
+
**各客户端怎么等待**(不需要配置):
|
|
64
|
+
- **Claude Code** 在后台等待任务(`dsh_send` 返回的 `wait_command`),任务结束时只被唤醒一次。
|
|
65
|
+
- **Codex** 在 `dsh_wait` 里等待,每次最多 270 秒,刚好低于它 300 秒的工具超时。没做完时只返回状态,Codex 会直接再调用,不会去总结进度。Codex 不会收到 `wait_command`:它的终端无法在命令结束时唤醒它,只能轮询。
|
|
66
|
+
- Codex 调用 MCP 工具时会请你批准,批准 `dsh-baton` 的工具即可;无人值守的 `codex exec` 加上 `--approve-for-me`。
|
|
67
|
+
|
|
104
68
|
## 工具
|
|
105
69
|
|
|
106
70
|
<p align="center">
|
|
@@ -170,6 +134,42 @@ dsh-baton rename-workspace ~/code/new-app 新名字
|
|
|
170
134
|
|
|
171
135
|
</details>
|
|
172
136
|
|
|
137
|
+
## 安装细节
|
|
138
|
+
|
|
139
|
+
**插件启用时做了什么。**它把 MCP 服务器复制到 `~/.dsh-baton/app` 这个固定的文件夹,再直接把 MCP 注册写进 `~/.claude.json` 和 `~/.codex/config.toml`,各留一份 `*.bak-dsh-baton` 备份。注册的是 `PATH` 里找到的 `node`(不会用 Harness 自带的那个),所以只装了桌面应用、`claude` / `codex` 不在 `PATH` 里也能用。每次 Harness 启动都会再检查一遍:已经正确的注册、或者指向另一份可用 dsh-baton 的注册,都不会改动。在 Harness 里输入包名后,安装源会自动切到「npm 官方源」(英文界面是 "Official npm registry"),不用另外选择,也不用重启。不想让插件自动注册,给 Harness 设置 `DSH_BATON_AUTO_REGISTER=0`。
|
|
140
|
+
|
|
141
|
+
**`npx -y dsh-baton-mcp setup`** 在终端里做同样的注册:用于插件没能注册的情况,或者从 MCP 目录过来的用户。插件已经注册过时它什么都不改;插件没装的话,它会提示去添加。
|
|
142
|
+
|
|
143
|
+
**更新。**Harness 只给官方插件提供"更新"按钮,所以要:插件 → dsh-baton-host → **卸载**,再**添加插件** → `dsh-baton-host`。重新启用后,插件会把 MCP 服务器也更新到同一版本,新开的对话就会用上。万一两边版本对不上,第一次发现不一致的工具调用会提示。
|
|
144
|
+
|
|
145
|
+
**卸载。**Harness → 插件 → dsh-baton-host → **卸载**,再运行 `npx -y dsh-baton-mcp setup --remove`。插件分不清"被卸载"和"Harness 退出",所以 MCP 注册交给这条命令删,它也会删掉 `~/.dsh-baton/app`;运行记录留在 `~/.dsh-baton`,不需要的话自己删掉。
|
|
146
|
+
|
|
147
|
+
**从 dsh-bridge 升级。**在 Harness 里先卸载 `dsh-bridge-host`,再添加 `dsh-baton-host`;启用后它会替换掉旧的 `dsh-bridge` MCP 注册。`DSH_BRIDGE_*` 环境变量和已有的 `~/.dsh-bridge` 文件夹暂时继续可用。工具前缀变成 `mcp__dsh-baton__…`,权限白名单里写了旧前缀的要改一下。0.4 之前通过粘贴 `~/.dsh-baton/app/host` 装的插件仍然能用,但不会跟着 npm 更新:卸载它,再按包名添加 `dsh-baton-host`。
|
|
148
|
+
|
|
149
|
+
**从源码安装**(想跟进 `main` 或改代码):`git clone https://github.com/JialaoLiu/dsh-baton.git`,然后在 Harness 里从克隆目录的 `host` 文件夹添加插件(添加插件 → 粘贴路径)。启用后注册的就是克隆目录本身,改动在发布前就能跑起来;更新用 `git pull`。在终端里运行 `node dsh-baton/src/cli.mjs setup` 也能注册克隆目录。
|
|
150
|
+
|
|
151
|
+
**Windows。**步骤相同;[WINDOWS.md](WINDOWS.md) 说明了不同之处(用命名管道代替套接字文件),以及插件页装不上或卸不掉时的修法。
|
|
152
|
+
|
|
153
|
+
<details>
|
|
154
|
+
<summary>手动注册与 Claude 桌面应用</summary>
|
|
155
|
+
|
|
156
|
+
插件和 `setup` 写入的内容和下面两条命令等价(用 `node` 的绝对路径,因为桌面应用启动 MCP 服务器时没有你 shell 的 `PATH`):
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
claude mcp add dsh-baton -s user -- "$(command -v node)" "$PWD/dsh-baton/src/server.mjs"
|
|
160
|
+
codex mcp add dsh-baton -- "$(command -v node)" "$PWD/dsh-baton/src/server.mjs"
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
`-s user` 很关键:不加的话,Claude Code 只给当前项目文件夹注册。
|
|
164
|
+
|
|
165
|
+
**Claude 桌面应用(可选)**:聊天应用的连接器(和 Claude Code 是两回事),把下面这段加到 `~/Library/Application Support/Claude/claude_desktop_config.json`,然后重启应用,会显示为 "Local dev" 连接器。
|
|
166
|
+
|
|
167
|
+
```json
|
|
168
|
+
{ "mcpServers": { "dsh-baton": { "command": "/node 的绝对路径", "args": ["/dsh-baton 的绝对路径/src/server.mjs"] } } }
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
</details>
|
|
172
|
+
|
|
173
173
|
## 安全与限制
|
|
174
174
|
|
|
175
175
|
<p align="center">
|
package/WINDOWS.md
CHANGED
|
@@ -20,23 +20,11 @@ Verified on Windows 11 with DeepSeek Harness 0.2.1-rc.2 (desktop app), Node.js 2
|
|
|
20
20
|
|
|
21
21
|
## Install
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
Exactly as in the [README](README.md#quick-start): in Harness add the plugin `dsh-baton-host` and switch it on. Updating and removing are the same too ([Install details](README.md#install-details)). On Windows:
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
`setup` copies itself to `%USERPROFILE%\.dsh-baton\app` (npx's cache is not a stable place) and registers the MCP server once, user-wide, for Claude Code (`%USERPROFILE%\.claude.json`) and Codex (`%USERPROFILE%\.codex\config.toml`), with the absolute path of `node`. It edits the files directly, so it works with the desktop apps alone: their `claude.exe` / `codex.exe` are usually not on `PATH`. It also copies the plugin path to the clipboard.
|
|
30
|
-
|
|
31
|
-
Then, in Harness: **Plugins / 插件** → **Add plugin / 添加插件** → paste the path it copied (`%USERPROFILE%\.dsh-baton\app\host`) → **Install / 安装** → **Enable now / 立即启用**. The pipe appears right away (see [Verify](#verify)); if it does not, restart Harness.
|
|
32
|
-
|
|
33
|
-
Nothing else to configure: Claude Code waits for a run in the background (Git Bash runs the `wait_command` that `dsh_send` returns), and Codex waits up to 270 s per `dsh_wait` call, under its 300 s tool timeout. Approve the `dsh-baton` tools when Codex asks; for unattended `codex exec`, add `--approve-for-me`.
|
|
34
|
-
|
|
35
|
-
Remove: `npx -y dsh-baton-mcp setup --remove`, then Harness → Plugins → dsh-baton-host → Uninstall, then `setup --remove` once more to delete the app folder.
|
|
36
|
-
|
|
37
|
-
Upgrading from dsh-bridge: `setup` replaces the old MCP entries and reuses an existing `%USERPROFILE%\.dsh-bridge` (the app then goes to `.dsh-bridge\app`). Uninstall `dsh-bridge-host` in Harness, then add the path `setup` copied. With the hoisted profile layout, uninstalling may leave a `node_modules\dsh-bridge-host` link behind in `%USERPROFILE%\.dsh\profiles\desktop`; `setup --remove` deletes such a leftover link (old or new name) once the profile no longer lists the plugin.
|
|
38
|
-
|
|
39
|
-
From source instead: `git clone https://github.com/JialaoLiu/dsh-baton.git C:\dev\dsh-baton` and `node C:\dev\dsh-baton\src\cli.mjs setup`; the plugin path is then `C:\dev\dsh-baton\host`.
|
|
25
|
+
- Switched on, the plugin copies the MCP server to `%USERPROFILE%\.dsh-baton\app` and writes `%USERPROFILE%\.claude.json` and `%USERPROFILE%\.codex\config.toml` with the absolute path of `node.exe` (from `PATH`, `%ProgramFiles%\nodejs` or nvm-windows' `NVM_SYMLINK`; never Harness's own runtime), so the desktop apps work even though their `claude.exe` / `codex.exe` are usually not on `PATH`. If it reports that it could not register, run `npx -y dsh-baton-mcp setup` in PowerShell.
|
|
26
|
+
- Claude Code runs the background `wait_command` in Git Bash.
|
|
27
|
+
- With the hoisted profile layout, uninstalling a plugin can leave a `node_modules\dsh-baton-host` (or old `dsh-bridge-host`) link in `%USERPROFILE%\.dsh\profiles\desktop`; `setup --remove` deletes it once the profile no longer lists the plugin.
|
|
40
28
|
|
|
41
29
|
### If the Plugins page cannot uninstall or install it
|
|
42
30
|
|
|
@@ -107,7 +95,7 @@ The suite runs on Windows with named pipes. Two POSIX-only checks are skipped: f
|
|
|
107
95
|
|
|
108
96
|
- **套接字改为命名管道。** Windows 上 Node 不能监听 Unix 套接字文件,所以同一路径被映射成按用户区分的命名管道 `\\.\pipe\dsh-baton-<哈希>`。插件和 MCP 两边算法一致,`DSH_BATON_SOCKET` 仍然可用,Harness 退出后管道自动消失。
|
|
109
97
|
- **headless 用 Harness 自带的 CLI。** Harness ≥ 0.2.1 自带 `resources\runtime\cli\bin\dsh.cmd`;更早的版本只有打包在 `app.asar` 里的 CLI,由 Harness 自己的 exe 以 Node 模式运行。会按顺序自动查找:`DSH_BIN`、`PATH` 里的 `dsh`、默认安装位置,以及注册表里的自定义安装目录。
|
|
110
|
-
-
|
|
111
|
-
-
|
|
98
|
+
- **安装和 macOS 完全相同。** 在 Harness 插件页添加 `dsh-baton-host` 并启用,详见 [README](README.zh-CN.md#快速上手)。
|
|
99
|
+
- **插件启用时自动注册 MCP。** 它把 MCP 服务器复制到 `%USERPROFILE%\.dsh-baton\app`,再直接写 Claude Code 和 Codex 的用户级配置(用 `PATH`、`%ProgramFiles%\nodejs` 或 nvm-windows 里找到的 `node.exe` 绝对路径,不用 Harness 自带的),桌面版的 `claude` / `codex` 不在 `PATH` 里也没关系。之后新开对话就有 `dsh_*` 工具;Codex 每次最多等 270 秒,不用再配超时。没能注册时,在 PowerShell 里运行 `npx -y dsh-baton-mcp setup`。
|
|
112
100
|
- **插件页卸不掉或装不上时**(`ERR_PNPM_UNEXPECTED_STORE`,或提示"该插件已安装"):是旧版 Harness 装 profile 时用了另一个 pnpm store。一次性修好:从菜单退出 Harness,把 `%USERPROFILE%\.dsh\profiles\desktop\node_modules` 改名备份,用 Harness 自带的 pnpm 运行 `install --frozen-lockfile`(不加 `--store-dir`;`$app` 改成你的安装目录,比如 `E:\Software\DeepSeek Harness`),再启动 Harness,插件页的卸载和添加就正常了;如果插件被关掉了,记得重新打开。
|
|
113
101
|
- **headless 需要独立凭据。** 要么登录账户,要么设置 `DEEPSEEK_API_KEY`。桌面版里保存的 Key,headless CLI 读不到。
|
package/host/README.md
CHANGED
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
# dsh-baton-host
|
|
2
2
|
|
|
3
|
-
DeepSeek Harness plugin for [dsh-baton](
|
|
3
|
+
DeepSeek Harness plugin for [dsh-baton](https://github.com/JialaoLiu/dsh-baton#readme). It runs inside the Harness host (desktop app or `dsh web`) and serves a local, owner-only Unix socket (`~/.dsh-baton/host.sock`; a per-user named pipe on Windows, see [WINDOWS.md](https://github.com/JialaoLiu/dsh-baton/blob/main/WINDOWS.md)). Through it, an MCP client can list and create workspaces, create sessions, continue a specific session, and wait for its turn to finish. All of this goes through Harness's own session controller, so the messages show up live in the Harness UI.
|
|
4
4
|
|
|
5
|
-
DeepSeek Harness 插件,配合 [dsh-baton](
|
|
5
|
+
DeepSeek Harness 插件,配合 [dsh-baton](https://github.com/JialaoLiu/dsh-baton/blob/main/README.zh-CN.md) 使用。它运行在 Harness 内部,只开一个当前用户才能访问的本地套接字,让 MCP 客户端能新建工作区和对话、续接指定对话、等待任务完成。所有操作都走 Harness 官方会话服务,界面实时可见。
|
|
6
6
|
|
|
7
7
|
## Install / 安装
|
|
8
8
|
|
|
9
|
-
Harness → Plugins / 插件 → Add plugin / 添加插件 → `dsh-baton-host` → Install / 安装 → Enable now /
|
|
9
|
+
Harness → Plugins / 插件 → Add plugin / 添加插件 → `dsh-baton-host` → Install / 安装 → Enable now / 立即启用
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
That is the whole install: switched on, the plugin registers the dsh-baton MCP server (the `dsh-baton-mcp` package, installed with it) for Claude Code and Codex, user-wide, and Harness says so. New conversations then have the `dsh_*` tools. If it could not register, run `npx -y dsh-baton-mcp setup`. Details: [dsh-baton](https://github.com/JialaoLiu/dsh-baton#install-details).
|
|
12
|
+
|
|
13
|
+
装好就完成了:插件启用时会为 Claude Code 和 Codex 注册 dsh-baton MCP 服务器(`dsh-baton-mcp` 包,随插件一起安装),Harness 里会提示。新开的对话就有 `dsh_*` 工具。没能注册的话,运行 `npx -y dsh-baton-mcp setup`。详见 [dsh-baton](https://github.com/JialaoLiu/dsh-baton/blob/main/README.zh-CN.md#安装细节)。
|
|
14
|
+
|
|
15
|
+
Update / 更新: uninstall, then add it again; the MCP server is updated with it / 先卸载再重新添加,MCP 服务器会一起更新.
|
|
16
|
+
|
|
17
|
+
Remove / 卸载: uninstall, then `npx -y dsh-baton-mcp setup --remove` / 卸载插件后运行 `npx -y dsh-baton-mcp setup --remove`.
|
|
12
18
|
|
|
13
19
|
## Socket protocol / 套接字协议
|
|
14
20
|
|
|
@@ -30,7 +36,9 @@ Newline-delimited JSON: request `{"id", "method", "params"}`, reply `{"id", "res
|
|
|
30
36
|
| `rename_workspace` | `workspace_id` or `path`, `title` |
|
|
31
37
|
| `list_runs` | `limit?` |
|
|
32
38
|
|
|
33
|
-
`DSH_BATON_SOCKET` / `DSH_BATON_HOME` in the Harness process environment move the socket.
|
|
39
|
+
`DSH_BATON_SOCKET` / `DSH_BATON_HOME` in the Harness process environment move the socket. `DSH_BATON_AUTO_REGISTER=0` turns off the MCP registration.
|
|
40
|
+
|
|
41
|
+
The Plugins page reads the registration outcome from `GET /dsh-baton/status` on the Harness web server.
|
|
34
42
|
|
|
35
43
|
## License
|
|
36
44
|
|
package/host/lib/client.js
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
// dsh-baton-host, browser half
|
|
2
|
-
//
|
|
1
|
+
// dsh-baton-host, browser half. Two jobs:
|
|
2
|
+
// - On the Plugins page: right after the plugin is switched on, and on its detail page,
|
|
3
|
+
// say whether the host half registered the MCP server for Claude Code and Codex
|
|
4
|
+
// (it serves the outcome at /dsh-baton/status).
|
|
5
|
+
// - In the chat: label messages sent through dsh-baton with their sender ("Sent by
|
|
6
|
+
// Claude from another chat"). The host half puts the sender
|
|
3
7
|
// into the prompt's request id ("dsh-baton.<sender>.<uuid>"); the chat records it as
|
|
4
8
|
// the user message's source.rpcId, so the label survives reloads with no extra state.
|
|
5
9
|
//
|
|
@@ -14,7 +18,10 @@ window.__ModuleLoader__.load({
|
|
|
14
18
|
id: 'dsh-baton-host',
|
|
15
19
|
factory: (require) => {
|
|
16
20
|
const module = { exports: {} }
|
|
17
|
-
const { createElement: h } = require('react')
|
|
21
|
+
const { createElement: h, useState, useEffect } = require('react')
|
|
22
|
+
// Harness's dialog and button; without them (an older or newer Harness), plain elements.
|
|
23
|
+
let ui = null
|
|
24
|
+
try { ui = require('@deepseek-ai/dsh-client-ui-primitives') } catch { /* fall back */ }
|
|
18
25
|
|
|
19
26
|
const SLOT = 'conversation.chat.node'
|
|
20
27
|
const KEYS = ['user', 'steering']
|
|
@@ -25,8 +32,124 @@ window.__ModuleLoader__.load({
|
|
|
25
32
|
// Codex shows as ChatGPT, the way the ChatGPT app labels its own cross-chat messages.
|
|
26
33
|
const NAMES = { claude: 'Claude', codex: 'ChatGPT' }
|
|
27
34
|
|
|
28
|
-
const
|
|
29
|
-
const
|
|
35
|
+
const PACKAGE = 'dsh-baton-host'
|
|
36
|
+
const SETUP = 'npx -y dsh-baton-mcp setup'
|
|
37
|
+
|
|
38
|
+
const zh = {
|
|
39
|
+
sentFrom: '由 {name} 从另一个聊天发送',
|
|
40
|
+
readyTitle: 'dsh-baton 已就绪',
|
|
41
|
+
failTitle: 'dsh-baton 还差一步',
|
|
42
|
+
pending: '正在为 Claude Code 和 Codex 注册 dsh-baton…',
|
|
43
|
+
ready: '已为 {clients} 注册 dsh-baton。在 Claude Code 或 Codex 里新开一个对话,就能用 dsh_* 工具。',
|
|
44
|
+
partial: '{clients} 没能注册:',
|
|
45
|
+
noNode: '没找到 Node.js 22.15 或更新版本。装好 Node.js 后,',
|
|
46
|
+
noClients: '没找到 Claude Code 或 Codex。装好之后,',
|
|
47
|
+
failed: '没能自动注册 MCP。',
|
|
48
|
+
runSetup: '请在终端运行 {cmd}。',
|
|
49
|
+
unknown: '如果新对话里没有 dsh_* 工具,请在终端运行 {cmd}。',
|
|
50
|
+
off: '已关闭自动注册(DSH_BATON_AUTO_REGISTER=0)。',
|
|
51
|
+
ok: '知道了',
|
|
52
|
+
status: 'MCP 注册',
|
|
53
|
+
registered: '已注册',
|
|
54
|
+
notInstalled: '未安装',
|
|
55
|
+
notRegistered: '未注册',
|
|
56
|
+
and: '和',
|
|
57
|
+
}
|
|
58
|
+
const en = {
|
|
59
|
+
sentFrom: 'Sent by {name} from another chat',
|
|
60
|
+
readyTitle: 'dsh-baton is ready',
|
|
61
|
+
failTitle: 'dsh-baton needs one more step',
|
|
62
|
+
pending: 'Registering dsh-baton for Claude Code and Codex…',
|
|
63
|
+
ready: 'dsh-baton is registered for {clients}. Start a new conversation in Claude Code or Codex to use the dsh_* tools.',
|
|
64
|
+
partial: 'Could not register for {clients}: ',
|
|
65
|
+
noNode: 'Node.js 22.15 or later was not found. Once it is installed, ',
|
|
66
|
+
noClients: 'Neither Claude Code nor Codex was found. Once one is installed, ',
|
|
67
|
+
failed: 'The MCP server could not be registered automatically. ',
|
|
68
|
+
runSetup: 'run {cmd} in a terminal.',
|
|
69
|
+
unknown: 'If new conversations have no dsh_* tools, run {cmd} in a terminal.',
|
|
70
|
+
off: 'Automatic registration is off (DSH_BATON_AUTO_REGISTER=0).',
|
|
71
|
+
ok: 'OK',
|
|
72
|
+
status: 'MCP registration',
|
|
73
|
+
registered: 'registered',
|
|
74
|
+
notInstalled: 'not installed',
|
|
75
|
+
notRegistered: 'not registered',
|
|
76
|
+
and: 'and',
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// --- Plugins page ------------------------------------------------------------
|
|
80
|
+
|
|
81
|
+
const statusUrl = () => new URL('dsh-baton/status', document.baseURI).pathname
|
|
82
|
+
const REGISTERED = ['registered', 'unchanged', 'kept']
|
|
83
|
+
|
|
84
|
+
// The host half registers right after it starts, so poll until it says it is done.
|
|
85
|
+
function useRegistration() {
|
|
86
|
+
const [reg, setReg] = useState(null)
|
|
87
|
+
useEffect(() => {
|
|
88
|
+
let stopped = false
|
|
89
|
+
let tries = 0
|
|
90
|
+
const poll = async () => {
|
|
91
|
+
let next = null
|
|
92
|
+
try {
|
|
93
|
+
const res = await fetch(statusUrl(), { cache: 'no-store' })
|
|
94
|
+
if (res.ok) next = (await res.json()).registration
|
|
95
|
+
} catch { /* not served yet */ }
|
|
96
|
+
if (stopped) return
|
|
97
|
+
if (next && next.state !== 'pending') return setReg(next)
|
|
98
|
+
if (++tries >= 30) return setReg({ state: 'unknown' })
|
|
99
|
+
setTimeout(poll, 700)
|
|
100
|
+
}
|
|
101
|
+
poll()
|
|
102
|
+
return () => { stopped = true }
|
|
103
|
+
}, [])
|
|
104
|
+
return reg
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// What to tell the user: [title or null, text]. null while the host is still working.
|
|
108
|
+
function describe(reg, t) {
|
|
109
|
+
if (!reg || reg.state === 'pending') return [null, t('pending')]
|
|
110
|
+
if (reg.state === 'off') return [t('readyTitle'), t('off')]
|
|
111
|
+
if (reg.state === 'unknown') return [t('readyTitle'), t('unknown', { cmd: SETUP })]
|
|
112
|
+
const clients = reg.clients ?? []
|
|
113
|
+
const done = clients.filter((c) => REGISTERED.includes(c.state)).map((c) => c.label)
|
|
114
|
+
const failed = clients.filter((c) => c.state === 'failed').map((c) => c.label)
|
|
115
|
+
const list = (names) => names.join(` ${t('and')} `)
|
|
116
|
+
const fix = (reg.error === 'no-node' ? t('noNode') : reg.error === 'no-clients' ? t('noClients') : '') + t('runSetup', { cmd: SETUP })
|
|
117
|
+
if (reg.ok) return [t('readyTitle'), t('ready', { clients: list(done) })]
|
|
118
|
+
if (done.length) return [t('failTitle'), `${t('ready', { clients: list(done) })} ${t('partial', { clients: list(failed) })}${fix}`]
|
|
119
|
+
return [t('failTitle'), (reg.error === 'no-node' || reg.error === 'no-clients' ? '' : t('failed')) + fix]
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Shown once, right after "Enable now".
|
|
123
|
+
function wrapActivation(t) {
|
|
124
|
+
return function DshBatonActivation({ onDismiss }) {
|
|
125
|
+
const [title, text] = describe(useRegistration(), t)
|
|
126
|
+
if (ui?.Modal && ui?.Button) {
|
|
127
|
+
return h(ui.Modal, {
|
|
128
|
+
open: true, title: title ?? t('readyTitle'), closeLabel: t('ok'), onClose: onDismiss,
|
|
129
|
+
footer: h(ui.Button, { variant: 'primary', 'data-modal-autofocus': true, onClick: onDismiss }, t('ok')),
|
|
130
|
+
}, h('p', null, text))
|
|
131
|
+
}
|
|
132
|
+
return h('div', { role: 'status', style: { margin: '12px 0', padding: 12, borderRadius: 8, border: '1px solid var(--dsw-alias-border-l3)', color: 'var(--dsw-alias-label-primary)' } },
|
|
133
|
+
title ? h('strong', null, title) : null,
|
|
134
|
+
h('p', { style: { margin: '6px 0' } }, text),
|
|
135
|
+
h('button', { type: 'button', onClick: onDismiss }, t('ok')))
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// The plugin's detail page: one line per client.
|
|
140
|
+
function wrapDetails(t) {
|
|
141
|
+
return function DshBatonStatus({ view }) {
|
|
142
|
+
const reg = useRegistration()
|
|
143
|
+
if (view === 'summary') return null
|
|
144
|
+
const [, text] = describe(reg, t)
|
|
145
|
+
const word = (s) => (REGISTERED.includes(s) ? t('registered') : s === 'not-installed' ? t('notInstalled') : t('notRegistered'))
|
|
146
|
+
const rows = (reg?.clients ?? []).map((c) => `${c.label}: ${word(c.state)}`)
|
|
147
|
+
return h('div', { style: { display: 'flex', flexDirection: 'column', gap: 4, color: 'var(--dsw-alias-label-secondary)' } },
|
|
148
|
+
h('strong', { style: { color: 'var(--dsw-alias-label-primary)' } }, t('status')),
|
|
149
|
+
rows.length ? h('span', null, rows.join(' · ')) : null,
|
|
150
|
+
h('span', null, text))
|
|
151
|
+
}
|
|
152
|
+
}
|
|
30
153
|
|
|
31
154
|
function senderOf(node) {
|
|
32
155
|
const rpcId = node?.data?.source?.rpcId
|
|
@@ -92,6 +215,8 @@ window.__ModuleLoader__.load({
|
|
|
92
215
|
// locale 'chat': the shipped renderer reads its strings through the `t` it is handed.
|
|
93
216
|
for (const key of KEYS) yield ctx.slots.register({ name: SLOT, key, priority: PRIORITY, locale: 'chat' }, wrapperFor(ctx, key, t))
|
|
94
217
|
})
|
|
218
|
+
ctx.slots.inject('plugins.bundle.activation', () => ctx.slots.register({ name: 'plugins.bundle.activation', key: PACKAGE, locale: NS }, wrapActivation(t)))
|
|
219
|
+
ctx.slots.inject('plugins.bundle.config', () => ctx.slots.register({ name: 'plugins.bundle.config', key: PACKAGE, locale: NS }, wrapDetails(t)))
|
|
95
220
|
}
|
|
96
221
|
return module.exports
|
|
97
222
|
},
|
package/host/lib/index.js
CHANGED
|
@@ -10,7 +10,9 @@ import net from 'node:net'
|
|
|
10
10
|
import os from 'node:os'
|
|
11
11
|
import path from 'node:path'
|
|
12
12
|
import crypto from 'node:crypto'
|
|
13
|
-
import {
|
|
13
|
+
import { createRequire } from 'node:module'
|
|
14
|
+
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
15
|
+
import { socketEndpoint, batonHome, setting, socketPath as socketPathSetting } from './socket.js'
|
|
14
16
|
|
|
15
17
|
export const name = 'dsh-baton-host'
|
|
16
18
|
export const inject = ['sessionController', 'workspaceRegistry']
|
|
@@ -85,6 +87,26 @@ function toolCallsOf(content) {
|
|
|
85
87
|
|
|
86
88
|
const iso = (t) => (typeof t === 'number' ? new Date(t).toISOString() : t ?? null)
|
|
87
89
|
|
|
90
|
+
// The MCP server package this plugin registers: the checkout or ~/.dsh-baton/app folder
|
|
91
|
+
// this host folder sits in, else the dsh-baton-mcp package installed with the plugin.
|
|
92
|
+
function serverRoot() {
|
|
93
|
+
const up = fileURLToPath(new URL('../../', import.meta.url))
|
|
94
|
+
if (fs.existsSync(path.join(up, 'src', 'register.mjs'))) return path.resolve(up)
|
|
95
|
+
try { return path.dirname(createRequire(import.meta.url).resolve('dsh-baton-mcp/package.json')) } catch { return null }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// The Harness install this process runs from, so its bundled Node (whose path changes
|
|
99
|
+
// with every Harness update) is never registered.
|
|
100
|
+
function harnessDir() {
|
|
101
|
+
const exe = process.execPath
|
|
102
|
+
const app = exe.match(/^(.*?\.app)(?=[\\/])/)
|
|
103
|
+
if (app) return app[1]
|
|
104
|
+
// Windows: the install folder, e.g. %LOCALAPPDATA%\Programs\DeepSeek Harness.
|
|
105
|
+
const i = exe.toLowerCase().indexOf('deepseek harness')
|
|
106
|
+
const end = i < 0 ? -1 : exe.slice(i).search(/[\\/]/)
|
|
107
|
+
return end > 0 ? exe.slice(0, i + end) : null
|
|
108
|
+
}
|
|
109
|
+
|
|
88
110
|
export function apply(ctx) {
|
|
89
111
|
// This code runs inside the Harness host process: an uncaught error here can
|
|
90
112
|
// take Harness down. Every event listener and timer goes through guard().
|
|
@@ -630,6 +652,40 @@ export function apply(ctx) {
|
|
|
630
652
|
}
|
|
631
653
|
|
|
632
654
|
|
|
655
|
+
// --- MCP registration -----------------------------------------------------
|
|
656
|
+
// Switching the plugin on registers the dsh-baton MCP server for Claude Code and Codex,
|
|
657
|
+
// the same entries `dsh-baton setup` writes, so adding the plugin is the whole install.
|
|
658
|
+
// It runs again on every Harness start: a plugin update brings the server along, and an
|
|
659
|
+
// entry that is already right is left untouched. The Plugins page reads the outcome from
|
|
660
|
+
// GET /dsh-baton/status (lib/client.js). DSH_BATON_AUTO_REGISTER=0 turns it off.
|
|
661
|
+
let registration = { state: 'pending' }
|
|
662
|
+
if (setting('AUTO_REGISTER') !== '0') {
|
|
663
|
+
setImmediate(guard('register', async () => {
|
|
664
|
+
const root = serverRoot()
|
|
665
|
+
if (!root) { registration = { state: 'done', ok: false, error: 'no-server', clients: [] }; return }
|
|
666
|
+
const { autoRegister } = await import(pathToFileURL(path.join(root, 'src', 'register.mjs')).href)
|
|
667
|
+
const result = autoRegister({ root, harnessDir: harnessDir() })
|
|
668
|
+
registration = { state: 'done', ...result }
|
|
669
|
+
const summary = result.clients.map((c) => `${c.label}: ${c.state}${c.error ? ` (${c.error})` : ''}`).join(', ')
|
|
670
|
+
ctx.logger?.[result.ok ? 'info' : 'error']?.(`dsh-baton-host MCP registration: ${summary || result.error}`)
|
|
671
|
+
}))
|
|
672
|
+
} else {
|
|
673
|
+
registration = { state: 'off' }
|
|
674
|
+
}
|
|
675
|
+
if (typeof ctx.inject === 'function') {
|
|
676
|
+
ctx.inject(['webServer'], (web) => {
|
|
677
|
+
web.effect(() => web.webServer.register({
|
|
678
|
+
kind: 'exact',
|
|
679
|
+
path: '/dsh-baton/status',
|
|
680
|
+
handler: (request, response) => {
|
|
681
|
+
const { state, ok, error, version, clients = [] } = registration
|
|
682
|
+
response.writeHead(200, { 'content-type': 'application/json', 'cache-control': 'no-store' })
|
|
683
|
+
response.end(JSON.stringify({ version: VERSION, registration: { state, ok, error, version, clients: clients.map(({ id, label, state }) => ({ id, label, state })) } }))
|
|
684
|
+
},
|
|
685
|
+
}), 'dsh-baton-host status route')
|
|
686
|
+
})
|
|
687
|
+
}
|
|
688
|
+
|
|
633
689
|
ctx.effect(() => {
|
|
634
690
|
const sock = socketPath()
|
|
635
691
|
let server = null
|
package/host/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-baton-host",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "DeepSeek Harness plugin for dsh-baton:
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"description": "DeepSeek Harness plugin for dsh-baton: adding it is the whole install. Switched on, it registers the dsh-baton MCP server for Claude Code and Codex, then takes the baton over a local, owner-only control socket, runs it in a specific session through the host's own session controller, and hands the result back when the turn is done.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"exports": {
|
|
@@ -19,6 +19,9 @@
|
|
|
19
19
|
"README.md",
|
|
20
20
|
"LICENSE"
|
|
21
21
|
],
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"dsh-baton-mcp": "0.5.0"
|
|
24
|
+
},
|
|
22
25
|
"license": "MIT",
|
|
23
26
|
"dsh": {
|
|
24
27
|
"manifestVersion": 1,
|
|
@@ -33,7 +36,8 @@
|
|
|
33
36
|
"inject": [
|
|
34
37
|
"@deepseek-ai/dsh-client-locale",
|
|
35
38
|
"@deepseek-ai/dsh-client-ui-renderer",
|
|
36
|
-
"@deepseek-ai/dsh-client-ui-chat"
|
|
39
|
+
"@deepseek-ai/dsh-client-ui-chat",
|
|
40
|
+
"@deepseek-ai/dsh-client-ui-primitives"
|
|
37
41
|
]
|
|
38
42
|
}
|
|
39
43
|
},
|
|
@@ -53,5 +57,8 @@
|
|
|
53
57
|
"url": "git+https://github.com/JialaoLiu/dsh-baton.git",
|
|
54
58
|
"directory": "host"
|
|
55
59
|
},
|
|
56
|
-
"homepage": "https://github.com/JialaoLiu/dsh-baton#readme"
|
|
60
|
+
"homepage": "https://github.com/JialaoLiu/dsh-baton#readme",
|
|
61
|
+
"scripts": {
|
|
62
|
+
"prepublishOnly": "node ../scripts/version.mjs"
|
|
63
|
+
}
|
|
57
64
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-baton-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"mcpName": "io.github.jialaoliu/dsh-baton",
|
|
4
5
|
"description": "Pass the baton from Claude Code, Codex or any MCP client to a local DeepSeek Harness: an MCP server that hands off tasks, continues a specific session, shows the run live in Harness and hands the result back when it is done.",
|
|
5
6
|
"type": "module",
|
|
6
7
|
"bin": {
|
|
@@ -18,7 +19,8 @@
|
|
|
18
19
|
"README.md",
|
|
19
20
|
"README.zh-CN.md",
|
|
20
21
|
"WINDOWS.md",
|
|
21
|
-
"LICENSE"
|
|
22
|
+
"LICENSE",
|
|
23
|
+
"server.json"
|
|
22
24
|
],
|
|
23
25
|
"engines": {
|
|
24
26
|
"node": ">=22.15"
|
|
@@ -35,7 +37,8 @@
|
|
|
35
37
|
"license": "MIT",
|
|
36
38
|
"scripts": {
|
|
37
39
|
"test": "node --test --test-concurrency=1 tests/*.test.mjs",
|
|
38
|
-
"test:live": "node tests/live.mjs"
|
|
40
|
+
"test:live": "node tests/live.mjs",
|
|
41
|
+
"prepublishOnly": "node scripts/version.mjs"
|
|
39
42
|
},
|
|
40
43
|
"repository": {
|
|
41
44
|
"type": "git",
|
package/server.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
|
+
"name": "io.github.jialaoliu/dsh-baton",
|
|
4
|
+
"title": "dsh-baton",
|
|
5
|
+
"description": "Hand tasks from Claude Code, Codex or any MCP client to a local DeepSeek Harness.",
|
|
6
|
+
"repository": {
|
|
7
|
+
"url": "https://github.com/JialaoLiu/dsh-baton",
|
|
8
|
+
"source": "github"
|
|
9
|
+
},
|
|
10
|
+
"version": "0.5.0",
|
|
11
|
+
"packages": [
|
|
12
|
+
{
|
|
13
|
+
"registryType": "npm",
|
|
14
|
+
"identifier": "dsh-baton-mcp",
|
|
15
|
+
"version": "0.5.0",
|
|
16
|
+
"transport": {
|
|
17
|
+
"type": "stdio"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
]
|
|
21
|
+
}
|
package/src/core.mjs
CHANGED
|
@@ -164,6 +164,8 @@ export async function hostAvailable() {
|
|
|
164
164
|
}
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
+
export const hostVersionSeen = () => hostVersion
|
|
168
|
+
|
|
167
169
|
// The plugin and the MCP server update separately (Harness and npx), so they can drift.
|
|
168
170
|
// A different major.minor gets a note asking to update the older side; patch releases mix.
|
|
169
171
|
const minorOf = (v) => String(v).split('.').slice(0, 2).join('.')
|
package/src/register.mjs
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
// Registering the dsh-baton MCP server for Claude Code and Codex, user-wide, by writing
|
|
2
|
+
// their config files directly. This needs no `claude` or `codex` on PATH (desktop-only
|
|
3
|
+
// installs have neither) and no hand-editing of TOML.
|
|
4
|
+
//
|
|
5
|
+
// Two callers: `dsh-baton setup` (src/setup.mjs), and the Harness plugin, which runs
|
|
6
|
+
// autoRegister() when it is switched on. So this module imports nothing that does work
|
|
7
|
+
// at load time: it runs inside the Harness host process as well.
|
|
8
|
+
import fs from 'node:fs'
|
|
9
|
+
import os from 'node:os'
|
|
10
|
+
import path from 'node:path'
|
|
11
|
+
import { execFileSync } from 'node:child_process'
|
|
12
|
+
import { batonHome } from '../host/lib/socket.js'
|
|
13
|
+
|
|
14
|
+
export const NAME = 'dsh-baton'
|
|
15
|
+
// The name before the rename: its entries are removed so the tools are not listed twice.
|
|
16
|
+
const OLD_NAME = 'dsh-bridge'
|
|
17
|
+
|
|
18
|
+
export const claudeConfigPath = (env = process.env, home = os.homedir()) =>
|
|
19
|
+
path.join(env.CLAUDE_CONFIG_DIR || home, '.claude.json')
|
|
20
|
+
export const codexConfigPath = (env = process.env, home = os.homedir()) =>
|
|
21
|
+
path.join(env.CODEX_HOME || path.join(home, '.codex'), 'config.toml')
|
|
22
|
+
|
|
23
|
+
// ~/.claude.json: the top-level "mcpServers" map is Claude Code's user scope.
|
|
24
|
+
export function editClaudeConfig(text, entry) {
|
|
25
|
+
const cfg = text.trim() ? JSON.parse(text) : {}
|
|
26
|
+
const servers = { ...(cfg.mcpServers ?? {}) }
|
|
27
|
+
delete servers[OLD_NAME]
|
|
28
|
+
if (entry) servers[NAME] = { type: 'stdio', command: entry.command, args: entry.args, env: {} }
|
|
29
|
+
else delete servers[NAME]
|
|
30
|
+
return JSON.stringify({ ...cfg, mcpServers: servers }, null, 2) + '\n'
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function readClaudeEntry(text) {
|
|
34
|
+
const e = text.trim() ? JSON.parse(text).mcpServers?.[NAME] : null
|
|
35
|
+
return e?.command ? { command: e.command, args: e.args ?? [] } : null
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const tomlString = (s) => (/['\n\r]/.test(s) ? JSON.stringify(s) : `'${s}'`)
|
|
39
|
+
// Our own table and its sub-tables ([mcp_servers.dsh-baton], [mcp_servers.dsh-baton.env], …),
|
|
40
|
+
// under the current name or the one before the rename.
|
|
41
|
+
const OWN_TABLE = /^\s*\[\s*mcp_servers\s*\.\s*(["']?)dsh-(?:baton|bridge)\1\s*(?:\.[^\]]*)?\]\s*(?:#.*)?$/
|
|
42
|
+
const ANY_TABLE = /^\s*\[/
|
|
43
|
+
|
|
44
|
+
// ~/.codex/config.toml: replace any earlier dsh-baton tables with one fresh table at the
|
|
45
|
+
// end. Codex stops a tool call after 300 s; the server keeps each wait under that, so no
|
|
46
|
+
// tool_timeout_sec is needed.
|
|
47
|
+
export function editCodexConfig(text, entry) {
|
|
48
|
+
const kept = []
|
|
49
|
+
let skipping = false
|
|
50
|
+
for (const line of text.split(/\r?\n/)) {
|
|
51
|
+
if (OWN_TABLE.test(line)) skipping = true
|
|
52
|
+
else if (ANY_TABLE.test(line)) skipping = false
|
|
53
|
+
if (!skipping) kept.push(line)
|
|
54
|
+
}
|
|
55
|
+
let out = kept.join('\n').replace(/\s+$/, '')
|
|
56
|
+
if (entry) {
|
|
57
|
+
out += `${out ? '\n\n' : ''}[mcp_servers.${NAME}]\ncommand = ${tomlString(entry.command)}\nargs = [${entry.args.map(tomlString).join(', ')}]`
|
|
58
|
+
}
|
|
59
|
+
return out ? out + '\n' : ''
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// The command and args of [mcp_servers.dsh-baton], as setup writes them (single-line strings).
|
|
63
|
+
export function readCodexEntry(text) {
|
|
64
|
+
const str = String.raw`'[^'\n]*'|"(?:[^"\\\n]|\\.)*"`
|
|
65
|
+
const unquote = (s) => (s.startsWith("'") ? s.slice(1, -1) : JSON.parse(s))
|
|
66
|
+
let inTable = false
|
|
67
|
+
let command = null, args = []
|
|
68
|
+
for (const line of text.split(/\r?\n/)) {
|
|
69
|
+
if (ANY_TABLE.test(line)) inTable = /^\s*\[\s*mcp_servers\s*\.\s*(["']?)dsh-baton\1\s*\]/.test(line)
|
|
70
|
+
else if (inTable) {
|
|
71
|
+
const c = line.match(new RegExp(`^\\s*command\\s*=\\s*(${str})`))
|
|
72
|
+
if (c) command = unquote(c[1])
|
|
73
|
+
const a = line.match(/^\s*args\s*=\s*\[(.*)\]/)
|
|
74
|
+
if (a) args = [...a[1].matchAll(new RegExp(str, 'g'))].map((m) => unquote(m[0]))
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return command ? { command, args } : null
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Write only when the content changes, keep one backup, and replace the file atomically.
|
|
81
|
+
export function rewrite(file, edit) {
|
|
82
|
+
const before = fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : ''
|
|
83
|
+
const after = edit(before)
|
|
84
|
+
if (after === before) return 'unchanged'
|
|
85
|
+
if (before) fs.copyFileSync(file, `${file}.bak-dsh-baton`)
|
|
86
|
+
fs.mkdirSync(path.dirname(file), { recursive: true })
|
|
87
|
+
const tmp = `${file}.tmp-${process.pid}`
|
|
88
|
+
fs.writeFileSync(tmp, after, { mode: before ? fs.statSync(file).mode : 0o600 })
|
|
89
|
+
fs.renameSync(tmp, file)
|
|
90
|
+
return 'updated'
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Which clients are installed for this user, and where their config lives.
|
|
94
|
+
export function clients(env = process.env, home = os.homedir()) {
|
|
95
|
+
const claude = claudeConfigPath(env, home)
|
|
96
|
+
const codex = codexConfigPath(env, home)
|
|
97
|
+
return [
|
|
98
|
+
{ id: 'claude', label: 'Claude Code', file: claude, present: fs.existsSync(claude) || fs.existsSync(path.join(home, '.claude')), edit: editClaudeConfig, read: readClaudeEntry },
|
|
99
|
+
{ id: 'codex', label: 'Codex', file: codex, present: fs.existsSync(path.dirname(codex)), edit: editCodexConfig, read: readCodexEntry },
|
|
100
|
+
]
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// The node to register: the one on PATH when it is this same binary, since that path
|
|
104
|
+
// survives upgrades (Homebrew's /opt/homebrew/bin/node, not the versioned Cellar path
|
|
105
|
+
// that process.execPath resolves to); otherwise the running binary itself.
|
|
106
|
+
export function stableNodePath(env = process.env, execPath = process.execPath) {
|
|
107
|
+
const real = (p) => { try { return fs.realpathSync(p) } catch { return null } }
|
|
108
|
+
const target = real(execPath)
|
|
109
|
+
const names = process.platform === 'win32' ? ['node.exe'] : ['node']
|
|
110
|
+
for (const dir of (env.PATH ?? env.Path ?? '').split(path.delimiter).filter(Boolean)) {
|
|
111
|
+
for (const name of names) {
|
|
112
|
+
const candidate = path.join(dir, name)
|
|
113
|
+
if (candidate !== execPath && target && real(candidate) === target) return candidate
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return execPath
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export const MIN_NODE = [22, 15]
|
|
120
|
+
export function nodeVersionOk(version) {
|
|
121
|
+
const [major, minor] = String(version).replace(/^v/, '').split('.').map(Number)
|
|
122
|
+
return major > MIN_NODE[0] || (major === MIN_NODE[0] && minor >= MIN_NODE[1])
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// A Node.js for the MCP server when the caller is not one itself: the Harness plugin runs
|
|
126
|
+
// on the app's own binary, whose path changes with every Harness update. So: the first
|
|
127
|
+
// `node` on PATH, then the usual install folders, never one inside the Harness app, and
|
|
128
|
+
// only a new enough one.
|
|
129
|
+
export function findNode({ env = process.env, platform = process.platform, exclude = [], version = nodeVersion } = {}) {
|
|
130
|
+
const exe = platform === 'win32' ? 'node.exe' : 'node'
|
|
131
|
+
const p = platform === 'win32' ? path.win32 : path.posix
|
|
132
|
+
const dirs = (env.PATH ?? env.Path ?? '').split(p.delimiter).filter(Boolean)
|
|
133
|
+
if (platform === 'win32') {
|
|
134
|
+
dirs.push(...[env.NVM_SYMLINK, env.ProgramFiles && p.join(env.ProgramFiles, 'nodejs'), env.LOCALAPPDATA && p.join(env.LOCALAPPDATA, 'Programs', 'nodejs')].filter(Boolean))
|
|
135
|
+
} else {
|
|
136
|
+
dirs.push('/opt/homebrew/bin', '/usr/local/bin', '/usr/bin')
|
|
137
|
+
}
|
|
138
|
+
const real = (x) => { try { return fs.realpathSync(x) } catch { return x } }
|
|
139
|
+
const fold = (s) => (platform === 'win32' ? s.toLowerCase() : s)
|
|
140
|
+
const skip = exclude.filter(Boolean).map((x) => fold(real(x)))
|
|
141
|
+
const seen = new Set()
|
|
142
|
+
for (const dir of dirs) {
|
|
143
|
+
const candidate = p.join(dir, exe)
|
|
144
|
+
if (seen.has(candidate)) continue
|
|
145
|
+
seen.add(candidate)
|
|
146
|
+
if (!fs.existsSync(candidate)) continue
|
|
147
|
+
const at = fold(real(candidate))
|
|
148
|
+
if (skip.some((x) => at === x || at.startsWith(x + p.sep))) continue
|
|
149
|
+
if (nodeVersionOk(version(candidate))) return candidate
|
|
150
|
+
}
|
|
151
|
+
return null
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function nodeVersion(bin) {
|
|
155
|
+
try {
|
|
156
|
+
return execFileSync(bin, ['--version'], { windowsHide: true, timeout: 5000, stdio: ['ignore', 'pipe', 'ignore'], env: { ...process.env, ELECTRON_RUN_AS_NODE: undefined } }).toString().trim()
|
|
157
|
+
} catch {
|
|
158
|
+
return ''
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// The stable folder the server runs from when it came from a package cache (npx's, or
|
|
163
|
+
// the Harness profile's): those can be cleared or replaced at any time.
|
|
164
|
+
export const appDir = (home = batonHome()) => path.join(home, 'app')
|
|
165
|
+
const APP_PARTS = ['src', 'host', 'patches', 'package.json', 'LICENSE']
|
|
166
|
+
|
|
167
|
+
export function installApp(from, to = appDir()) {
|
|
168
|
+
fs.mkdirSync(to, { recursive: true })
|
|
169
|
+
for (const part of APP_PARTS) {
|
|
170
|
+
const src = path.join(from, part)
|
|
171
|
+
if (!fs.existsSync(src)) continue
|
|
172
|
+
// Replace each part whole, so files dropped in a newer version do not linger.
|
|
173
|
+
fs.rmSync(path.join(to, part), { recursive: true, force: true })
|
|
174
|
+
fs.cpSync(src, path.join(to, part), { recursive: true })
|
|
175
|
+
}
|
|
176
|
+
return to
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const versionOf = (root) => { try { return JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8')).version } catch { return null } }
|
|
180
|
+
export function compareVersions(a, b) {
|
|
181
|
+
const parse = (v) => String(v).split('-')[0].split('.').map(Number)
|
|
182
|
+
const [x, y] = [parse(a), parse(b)]
|
|
183
|
+
for (let i = 0; i < 3; i++) if ((x[i] || 0) !== (y[i] || 0)) return (x[i] || 0) - (y[i] || 0)
|
|
184
|
+
// A prerelease sorts before its release.
|
|
185
|
+
return (String(b).includes('-') ? 1 : 0) - (String(a).includes('-') ? 1 : 0)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Run by the Harness plugin when it is switched on (and on every Harness start, so a
|
|
189
|
+
// plugin update brings the server along). `root` holds the MCP server package: the repo
|
|
190
|
+
// checkout the plugin was added from, or the dsh-baton-mcp package installed with it.
|
|
191
|
+
// - The server is copied to ~/.dsh-baton/app, unless root is a git checkout (registered
|
|
192
|
+
// as is, like `setup` from a clone) or that copy is already as new.
|
|
193
|
+
// - An entry that already starts this server, or a working one that starts another copy
|
|
194
|
+
// (a clone, say), is left alone; a missing or broken one is (re)written.
|
|
195
|
+
// Never throws: the result says what happened to each client.
|
|
196
|
+
export function autoRegister({ root, home = os.homedir(), env = process.env, node = null, harnessDir = null, find = findNode, app = appDir(batonHome(env, home)) } = {}) {
|
|
197
|
+
const result = { ok: false, version: versionOf(root), clients: [], error: null, node: null }
|
|
198
|
+
try {
|
|
199
|
+
let serverRoot = root
|
|
200
|
+
if (!fs.existsSync(path.join(root, '.git')) && path.resolve(root) !== path.resolve(app)) {
|
|
201
|
+
const installed = versionOf(app)
|
|
202
|
+
if (!installed || !fs.existsSync(path.join(app, 'src', 'server.mjs')) || compareVersions(installed, result.version) < 0) installApp(root, app)
|
|
203
|
+
serverRoot = app
|
|
204
|
+
}
|
|
205
|
+
const script = path.join(serverRoot, 'src', 'server.mjs')
|
|
206
|
+
const works = (e) => e && fs.existsSync(e.command) && e.args?.[0] && fs.existsSync(e.args[0])
|
|
207
|
+
const list = clients(env, home)
|
|
208
|
+
let bin = node
|
|
209
|
+
for (const c of list) {
|
|
210
|
+
if (!c.present) { result.clients.push({ id: c.id, label: c.label, state: 'not-installed' }); continue }
|
|
211
|
+
try {
|
|
212
|
+
const text = fs.existsSync(c.file) ? fs.readFileSync(c.file, 'utf8') : ''
|
|
213
|
+
const current = c.read(text)
|
|
214
|
+
if (works(current)) {
|
|
215
|
+
const same = path.resolve(current.args[0]) === path.resolve(script)
|
|
216
|
+
result.clients.push({ id: c.id, label: c.label, state: same ? 'unchanged' : 'kept', script: current.args[0] })
|
|
217
|
+
continue
|
|
218
|
+
}
|
|
219
|
+
bin ??= find({ env, exclude: [harnessDir] })
|
|
220
|
+
if (!bin) { result.clients.push({ id: c.id, label: c.label, state: 'failed', error: 'no-node' }); continue }
|
|
221
|
+
rewrite(c.file, (t) => c.edit(t, { command: bin, args: [script] }))
|
|
222
|
+
result.clients.push({ id: c.id, label: c.label, state: 'registered' })
|
|
223
|
+
} catch (e) {
|
|
224
|
+
result.clients.push({ id: c.id, label: c.label, state: 'failed', error: e.message })
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
result.node = bin
|
|
228
|
+
const present = result.clients.filter((c) => c.state !== 'not-installed')
|
|
229
|
+
result.ok = present.length > 0 && present.every((c) => c.state !== 'failed')
|
|
230
|
+
if (!present.length) result.error = 'no-clients'
|
|
231
|
+
else if (present.some((c) => c.error === 'no-node')) result.error = 'no-node'
|
|
232
|
+
} catch (e) {
|
|
233
|
+
result.error = e.message
|
|
234
|
+
}
|
|
235
|
+
return result
|
|
236
|
+
}
|
package/src/setup.mjs
CHANGED
|
@@ -1,67 +1,13 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
// (desktop-only installs have neither) and no hand-editing of TOML.
|
|
1
|
+
// `dsh-baton setup`: register the dsh-baton MCP server for Claude Code and Codex, user-wide
|
|
2
|
+
// (see register.mjs, which the Harness plugin also uses), and say what is left to do in Harness.
|
|
4
3
|
import fs from 'node:fs'
|
|
5
4
|
import os from 'node:os'
|
|
6
5
|
import path from 'node:path'
|
|
7
6
|
import { execFileSync } from 'node:child_process'
|
|
8
|
-
import { ROOT, config, hostAvailable } from './core.mjs'
|
|
7
|
+
import { ROOT, config, hostAvailable, hostVersionSeen as versionOfHost, versionWarning } from './core.mjs'
|
|
8
|
+
import { appDir, installApp, rewrite, stableNodePath, claudeConfigPath, codexConfigPath, editClaudeConfig, editCodexConfig } from './register.mjs'
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
// The name before the rename: setup removes its entries so the tools are not listed twice.
|
|
12
|
-
const OLD_NAME = 'dsh-bridge'
|
|
13
|
-
|
|
14
|
-
export const claudeConfigPath = (env = process.env, home = os.homedir()) =>
|
|
15
|
-
path.join(env.CLAUDE_CONFIG_DIR || home, '.claude.json')
|
|
16
|
-
export const codexConfigPath = (env = process.env, home = os.homedir()) =>
|
|
17
|
-
path.join(env.CODEX_HOME || path.join(home, '.codex'), 'config.toml')
|
|
18
|
-
|
|
19
|
-
// ~/.claude.json: the top-level "mcpServers" map is Claude Code's user scope.
|
|
20
|
-
export function editClaudeConfig(text, entry) {
|
|
21
|
-
const cfg = text.trim() ? JSON.parse(text) : {}
|
|
22
|
-
const servers = { ...(cfg.mcpServers ?? {}) }
|
|
23
|
-
delete servers[OLD_NAME]
|
|
24
|
-
if (entry) servers[NAME] = { type: 'stdio', command: entry.command, args: entry.args, env: {} }
|
|
25
|
-
else delete servers[NAME]
|
|
26
|
-
return JSON.stringify({ ...cfg, mcpServers: servers }, null, 2) + '\n'
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const tomlString = (s) => (/['\n\r]/.test(s) ? JSON.stringify(s) : `'${s}'`)
|
|
30
|
-
// Our own table and its sub-tables ([mcp_servers.dsh-baton], [mcp_servers.dsh-baton.env], …),
|
|
31
|
-
// under the current name or the one before the rename.
|
|
32
|
-
const OWN_TABLE = /^\s*\[\s*mcp_servers\s*\.\s*(["']?)dsh-(?:baton|bridge)\1\s*(?:\.[^\]]*)?\]\s*(?:#.*)?$/
|
|
33
|
-
const ANY_TABLE = /^\s*\[/
|
|
34
|
-
|
|
35
|
-
// ~/.codex/config.toml: replace any earlier dsh-baton tables with one fresh table at the
|
|
36
|
-
// end. Codex stops a tool call after 300 s; the server keeps each wait under that, so no
|
|
37
|
-
// tool_timeout_sec is needed.
|
|
38
|
-
export function editCodexConfig(text, entry) {
|
|
39
|
-
const kept = []
|
|
40
|
-
let skipping = false
|
|
41
|
-
for (const line of text.split(/\r?\n/)) {
|
|
42
|
-
if (OWN_TABLE.test(line)) skipping = true
|
|
43
|
-
else if (ANY_TABLE.test(line)) skipping = false
|
|
44
|
-
if (!skipping) kept.push(line)
|
|
45
|
-
}
|
|
46
|
-
let out = kept.join('\n').replace(/\s+$/, '')
|
|
47
|
-
if (entry) {
|
|
48
|
-
out += `${out ? '\n\n' : ''}[mcp_servers.${NAME}]\ncommand = ${tomlString(entry.command)}\nargs = [${entry.args.map(tomlString).join(', ')}]`
|
|
49
|
-
}
|
|
50
|
-
return out ? out + '\n' : ''
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// Write only when the content changes, keep one backup, and replace the file atomically.
|
|
54
|
-
function rewrite(file, edit) {
|
|
55
|
-
const before = fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : ''
|
|
56
|
-
const after = edit(before)
|
|
57
|
-
if (after === before) return 'unchanged'
|
|
58
|
-
if (before) fs.copyFileSync(file, `${file}.bak-dsh-baton`)
|
|
59
|
-
fs.mkdirSync(path.dirname(file), { recursive: true })
|
|
60
|
-
const tmp = `${file}.tmp-${process.pid}`
|
|
61
|
-
fs.writeFileSync(tmp, after, { mode: before ? fs.statSync(file).mode : 0o600 })
|
|
62
|
-
fs.renameSync(tmp, file)
|
|
63
|
-
return 'updated'
|
|
64
|
-
}
|
|
10
|
+
export { appDir, installApp, stableNodePath, claudeConfigPath, codexConfigPath, editClaudeConfig, editCodexConfig }
|
|
65
11
|
|
|
66
12
|
function copyToClipboard(text) {
|
|
67
13
|
try {
|
|
@@ -110,44 +56,46 @@ export function cleanStalePluginLinks(profile = path.join(config.dshHome, 'profi
|
|
|
110
56
|
return { removed, failed }
|
|
111
57
|
}
|
|
112
58
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
//
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
for (const dir of (env.PATH ?? env.Path ?? '').split(path.delimiter).filter(Boolean)) {
|
|
123
|
-
for (const name of names) {
|
|
124
|
-
const candidate = path.join(dir, name)
|
|
125
|
-
if (candidate !== execPath && target && real(candidate) === target) return candidate
|
|
126
|
-
}
|
|
59
|
+
// What setup says about the Harness plugin. The plugin is installed in Harness by its npm
|
|
60
|
+
// package name; a checkout of this repo (setup run from a git clone) is the exception and
|
|
61
|
+
// links its own host folder, so plugin changes can be tried before they are published.
|
|
62
|
+
export function pluginSteps({ current, old }, running, hostDir, fromSource = fs.existsSync(path.join(ROOT, '.git'))) {
|
|
63
|
+
const isLink = (spec) => /^(link|file):/.test(spec ?? '')
|
|
64
|
+
const ok = fromSource ? pluginFrom(current, hostDir) : current && !isLink(current)
|
|
65
|
+
if (ok && !old && running) {
|
|
66
|
+
const warning = versionWarning(running)
|
|
67
|
+
return warning ? ['Harness plugin: running, but out of step.', ` ${warning}`] : [`Harness plugin: running (${running}).`]
|
|
127
68
|
}
|
|
128
|
-
|
|
69
|
+
const steps = ['Last step, in DeepSeek Harness:']
|
|
70
|
+
// The plugin was renamed: Harness cannot load the old entry once its folder holds the new name.
|
|
71
|
+
if (old) steps.push(' The plugin is installed under its old name: first Plugins (插件) → dsh-bridge-host → Uninstall (卸载).')
|
|
72
|
+
if (current && !ok) {
|
|
73
|
+
steps.push(isLink(current)
|
|
74
|
+
? ` The plugin installed now comes from a local folder (${current.replace(/^(link|file):/, '')}): first Plugins (插件) → dsh-baton-host → Uninstall (卸载).`
|
|
75
|
+
: ' The plugin installed now comes from npm, not this checkout: first Plugins (插件) → dsh-baton-host → Uninstall (卸载).')
|
|
76
|
+
}
|
|
77
|
+
if (ok && !old) return ['Harness plugin: installed but not answering. Open Harness and check that dsh-baton-host is switched on in Plugins (插件).']
|
|
78
|
+
if (fromSource) {
|
|
79
|
+
const copied = copyToClipboard(hostDir)
|
|
80
|
+
steps.push(` Plugins (插件) → Add plugin (添加插件) → paste ${copied ? 'the path (already copied)' : 'this path'}: ${hostDir}`)
|
|
81
|
+
} else {
|
|
82
|
+
const copied = copyToClipboard(PLUGIN)
|
|
83
|
+
steps.push(` Plugins (插件) → Add plugin (添加插件) → registry: Official npm registry → package: ${PLUGIN}${copied ? ' (already copied)' : ''}`)
|
|
84
|
+
}
|
|
85
|
+
steps.push(' → Install (安装) → Enable now (立即启用)')
|
|
86
|
+
return steps
|
|
129
87
|
}
|
|
130
88
|
|
|
89
|
+
const PLUGIN = 'dsh-baton-host'
|
|
90
|
+
|
|
91
|
+
const pluginFrom = (source, hostDir) => source === `link:${hostDir}` || source === `link:${hostDir.split(path.sep).join('/')}`
|
|
92
|
+
|
|
131
93
|
// Run through npx, the package sits in npm's cache, which npm may clear or replace at any
|
|
132
94
|
// time. setup then copies it to a stable folder and registers that copy instead.
|
|
133
|
-
export const appDir = () => path.join(config.batonHome, 'app')
|
|
134
95
|
export const inNpxCache = (root = ROOT) => root.split(/[\\/]/).includes('_npx')
|
|
135
|
-
const APP_PARTS = ['src', 'host', 'patches', 'package.json', 'LICENSE']
|
|
136
|
-
|
|
137
|
-
export function installApp(from = ROOT, to = appDir()) {
|
|
138
|
-
fs.mkdirSync(to, { recursive: true })
|
|
139
|
-
for (const part of APP_PARTS) {
|
|
140
|
-
const src = path.join(from, part)
|
|
141
|
-
if (!fs.existsSync(src)) continue
|
|
142
|
-
// Replace each part whole, so files dropped in a newer version do not linger.
|
|
143
|
-
fs.rmSync(path.join(to, part), { recursive: true, force: true })
|
|
144
|
-
fs.cpSync(src, path.join(to, part), { recursive: true })
|
|
145
|
-
}
|
|
146
|
-
return to
|
|
147
|
-
}
|
|
148
96
|
|
|
149
97
|
export async function setup({ remove = false } = {}) {
|
|
150
|
-
const root = !remove && inNpxCache() ? installApp() : ROOT
|
|
98
|
+
const root = !remove && inNpxCache() ? installApp(ROOT) : ROOT
|
|
151
99
|
const entry = remove ? null : { command: stableNodePath(), args: [path.join(root, 'src', 'server.mjs')] }
|
|
152
100
|
const lines = []
|
|
153
101
|
if (root !== ROOT) lines.push(`Installed dsh-baton to ${root}`)
|
|
@@ -155,7 +103,10 @@ export async function setup({ remove = false } = {}) {
|
|
|
155
103
|
if (!remove && !present) return lines.push(`- ${label}: not installed, skipped`)
|
|
156
104
|
try {
|
|
157
105
|
const result = rewrite(file, (t) => edit(t, entry))
|
|
158
|
-
|
|
106
|
+
const what = remove
|
|
107
|
+
? (result === 'unchanged' ? 'not registered in' : 'removed from')
|
|
108
|
+
: 'registered in'
|
|
109
|
+
lines.push(`- ${label}: ${what} ${file}${!remove && result === 'unchanged' ? ' (already up to date)' : ''}`)
|
|
159
110
|
} catch (e) {
|
|
160
111
|
lines.push(`- ${label}: FAILED to edit ${file}: ${e.message}`)
|
|
161
112
|
process.exitCode = 1
|
|
@@ -190,21 +141,6 @@ export async function setup({ remove = false } = {}) {
|
|
|
190
141
|
}
|
|
191
142
|
lines.push('', 'MCP server registered user-wide: new conversations in Claude Code and Codex have the dsh_* tools.')
|
|
192
143
|
|
|
193
|
-
|
|
194
|
-
const { current: source, old } = installedPlugins()
|
|
195
|
-
const fromHere = pluginFrom(source, hostDir)
|
|
196
|
-
const running = await hostAvailable()
|
|
197
|
-
if (running && !old && (fromHere || !source)) {
|
|
198
|
-
lines.push('', 'Harness plugin: running.')
|
|
199
|
-
} else {
|
|
200
|
-
const copied = copyToClipboard(hostDir)
|
|
201
|
-
lines.push('', 'Last step, in DeepSeek Harness:')
|
|
202
|
-
// The plugin was renamed: Harness cannot load the old entry once its folder holds the new name.
|
|
203
|
-
if (old) lines.push(' The plugin is installed under its old name: first Plugins (插件) → dsh-bridge-host → Uninstall (卸载).')
|
|
204
|
-
if (source && !fromHere) lines.push(` The plugin installed now comes from another folder (${source}): first Plugins (插件) → dsh-baton-host → Uninstall (卸载).`)
|
|
205
|
-
lines.push(
|
|
206
|
-
` Plugins (插件) → Add plugin (添加插件) → paste ${copied ? 'the path (already copied)' : 'this path'}: ${hostDir}`,
|
|
207
|
-
' → Install (安装) → Enable now (立即启用)')
|
|
208
|
-
}
|
|
144
|
+
lines.push('', ...pluginSteps(installedPlugins(), await hostAvailable() ? versionOfHost() : null, path.join(root, 'host')))
|
|
209
145
|
return lines.join('\n')
|
|
210
146
|
}
|