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 CHANGED
@@ -3,7 +3,7 @@
3
3
  </p>
4
4
 
5
5
  <p align="center">
6
- English | <a href="README.zh-CN.md">中文</a> · Unofficial; not affiliated with DeepSeek.
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: run npx -y dsh-baton-mcp setup. Step 2: in Harness open Plugins, Add plugin, paste, Install, Enable now. Step 3: in a new chat, ask for dsh_list_workspaces">
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. On Windows, see [WINDOWS.md](WINDOWS.md).
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. Run setup** in a terminal:
45
+ **1. Add the plugin in Harness:** **Plugins** (插件) → **Add plugin** (添加插件) → type `dsh-baton-host` → **Install** (安装) → **Enable now** (立即启用).
46
46
 
47
- ```bash
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
- **3. Check.** Start a **new** conversation in Claude Code or Codex and ask it to call `dsh_list_workspaces`. It lists your Harness workspaces.
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
- **Or let Claude Code or Codex do it:** paste *"Run `npx -y dsh-baton-mcp setup`, then tell me the Harness step it prints."*
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> | 中文 · 非官方项目,与 DeepSeek 无关。
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="第一步:运行 npx -y dsh-baton-mcp setup。第二步:Harness → 插件 → 添加插件 → 粘贴 → 安装 → 立即启用。第三步:新开对话,让它调用 dsh_list_workspaces">
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 见 [WINDOWS.md](WINDOWS.md)。
43
+ 需要:DeepSeek Harness ≥ 0.2.1-rc.2 并已登录;Node.js ≥ 22.15;装了 Claude Code 或 Codex。macOS 和 Windows 步骤完全相同。
44
44
 
45
- **1. 运行 setup**,在终端里:
45
+ **1. 在 Harness 里添加插件:** 插件 → 添加插件 → 输入 `dsh-baton-host` → 安装 → 立即启用。
46
46
 
47
- ```bash
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
- **3. 检查。**在 Claude Code 或 Codex 里**新开**一个对话,让它调用 `dsh_list_workspaces`,应该会列出你的 Harness 工作区。
49
+ **2. 检查:**在 Claude Code 或 Codex 里**新开**一个对话,让它调用 `dsh_list_workspaces`,会列出你的 Harness 工作区。
58
50
 
59
- **也可以让 Claude Code 或 Codex 帮你装:** 发给它 *"运行 `npx -y dsh-baton-mcp setup`,然后告诉我它提示的 Harness 那一步。"*
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
- Requires DeepSeek Harness ≥ 0.2.1-rc.2 and Node.js ≥ 22.15. In PowerShell:
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
- ```powershell
26
- npx -y dsh-baton-mcp setup
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
- - **插件用 Harness 插件页安装。** 插件 → 添加插件 → 粘贴 `%USERPROFILE%\.dsh-baton\app\host` → 安装 → 立即启用(`setup` 已把路径复制到剪贴板)。
111
- - **一条命令注册 MCP。** `npx -y dsh-baton-mcp setup` 把自己复制到 `%USERPROFILE%\.dsh-baton\app`,再直接写 Claude Code 和 Codex 的用户级配置(用 `node` 的绝对路径),桌面版的 `claude` / `codex` 不在 `PATH` 里也没关系。之后新开对话就有 `dsh_*` 工具;Codex 每次最多等 270 秒,不用再配超时。
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](../README.md). 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](../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.
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](../README.zh-CN.md) 使用。它运行在 Harness 内部,只开一个当前用户才能访问的本地套接字,让 MCP 客户端能新建工作区和对话、续接指定对话、等待任务完成。所有操作都走 Harness 官方会话服务,界面实时可见。
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
- Then connect the MCP server / 然后接入 MCP 服务器: see [dsh-baton](../README.md).
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
 
@@ -1,5 +1,9 @@
1
- // dsh-baton-host, browser half: labels chat messages sent through dsh-baton with
2
- // their sender ("Sent by Claude from another chat"). The host half puts the sender
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 zh = { sentFrom: '由 {name} 从另一个聊天发送' }
29
- const en = { sentFrom: 'Sent by {name} from another chat' }
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 { socketEndpoint, batonHome, socketPath as socketPathSetting } from './socket.js'
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.3.1",
4
- "description": "DeepSeek Harness plugin for dsh-baton: takes the baton from Claude Code or Codex 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.",
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.1",
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('.')
@@ -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
- // One-step setup: register the dsh-baton MCP server for Claude Code and Codex, user-wide,
2
- // by writing their config files directly. This needs no `claude` or `codex` on PATH
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
- const NAME = 'dsh-baton'
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
- const pluginFrom = (source, hostDir) => source === `link:${hostDir}` || source === `link:${hostDir.split(path.sep).join('/')}`
114
-
115
- // The node to register: the one on PATH when it is this same binary, since that path
116
- // survives upgrades (Homebrew's /opt/homebrew/bin/node, not the versioned Cellar path
117
- // that process.execPath resolves to); otherwise the running binary itself.
118
- export function stableNodePath(env = process.env, execPath = process.execPath) {
119
- const real = (p) => { try { return fs.realpathSync(p) } catch { return null } }
120
- const target = real(execPath)
121
- const names = process.platform === 'win32' ? ['node.exe'] : ['node']
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
- return execPath
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
- lines.push(`- ${label}: ${remove ? 'removed from' : 'registered in'} ${file}${result === 'unchanged' ? ' (already up to date)' : ''}`)
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
- const hostDir = path.join(root, 'host')
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
  }