dsh-baton-mcp 0.3.0 → 0.4.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
@@ -37,7 +37,7 @@ 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: run npx -y dsh-baton-mcp setup. Step 2: in Harness open Plugins, Add plugin, enter dsh-baton-host from the npm registry, Install, Enable now. Step 3: in a new chat, ask for dsh_list_workspaces">
41
41
  </p>
42
42
 
43
43
  Requires DeepSeek Harness ≥ 0.2.1-rc.2 (signed in), Node.js ≥ 22.15. On Windows, see [WINDOWS.md](WINDOWS.md).
@@ -50,9 +50,9 @@ npx -y dsh-baton-mcp setup
50
50
 
51
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
52
 
53
- **2. Paste in Harness.** `setup` copies the plugin folder path to your clipboard:
53
+ **2. Add the plugin in Harness**, by its npm package name (`setup` copies it to your clipboard):
54
54
 
55
- **Plugins** (插件) → **Add plugin** (添加插件) → paste → **Install** (安装) → **Enable now** (立即启用)
55
+ **Plugins** (插件) → **Add plugin** (添加插件) → registry **Official npm registry** → `dsh-baton-host` → **Install** (安装) → **Enable now** (立即启用)
56
56
 
57
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.
58
58
 
@@ -63,13 +63,18 @@ dsh-baton has no dependencies. `setup` copies itself to `~/.dsh-baton/app`, a fo
63
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
64
  - Codex asks you to approve MCP tool calls: approve the `dsh-baton` tools. For unattended `codex exec` runs, add `--approve-for-me`.
65
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.
66
+ **Updating:** the MCP server and the plugin update separately, and both carry the same version.
67
+ - MCP server: `npx -y dsh-baton-mcp@latest setup`. New conversations use it.
68
+ - Plugin: Harness offers an update button only for its own official plugins, so for this one: Plugins → dsh-baton-host → **Uninstall**, then **Add plugin** → `dsh-baton-host` again.
69
+ - `setup` says when the two are out of step, and so does the first tool call that notices it.
67
70
 
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`.
71
+ **Removing:** Harness → Plugins → dsh-baton-host → **Uninstall**, and `npx -y dsh-baton-mcp setup --remove`. That removes the MCP entries and `~/.dsh-baton/app`; run records stay in `~/.dsh-baton` until you delete it.
69
72
 
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.
73
+ **Upgrading from dsh-bridge**: run `setup` as above. It replaces the old `dsh-bridge` MCP entries; in Harness, uninstall `dsh-bridge-host`, then add `dsh-baton-host` as in step 2. `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
74
 
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`.
75
+ **Installed from a local folder before 0.4?** Earlier versions had you paste `~/.dsh-baton/app/host`. That still works, but it does not update with npm: uninstall it and add `dsh-baton-host` by name. `setup` points this out.
76
+
77
+ **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 and has you add the plugin from the clone's `host` folder (Add plugin → paste the path it copies), so plugin changes run before they are published; update with `git pull`.
73
78
 
74
79
  <details>
75
80
  <summary>Manual registration and the Claude desktop app</summary>
package/README.zh-CN.md CHANGED
@@ -37,7 +37,7 @@ 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="第一步:运行 npx -y dsh-baton-mcp setup。第二步:Harness → 插件 → 添加插件 → 从 npm 输入 dsh-baton-host → 安装 → 立即启用。第三步:新开对话,让它调用 dsh_list_workspaces">
41
41
  </p>
42
42
 
43
43
  需要:DeepSeek Harness ≥ 0.2.1-rc.2 并已登录;Node.js ≥ 22.15。Windows 见 [WINDOWS.md](WINDOWS.md)。
@@ -50,9 +50,9 @@ npx -y dsh-baton-mcp setup
50
50
 
51
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
52
 
53
- **2. 在 Harness 里粘贴。**`setup` 已经把插件文件夹路径复制到剪贴板:
53
+ **2. 在 Harness 里按包名添加插件**(`setup` 已经把包名复制到剪贴板):
54
54
 
55
- **插件** → **添加插件** → 粘贴 → **安装** → **立即启用**
55
+ **插件** → **添加插件** → 来源选 **Official npm registry** → 输入 `dsh-baton-host` → **安装** → **立即启用**
56
56
 
57
57
  **3. 检查。**在 Claude Code 或 Codex 里**新开**一个对话,让它调用 `dsh_list_workspaces`,应该会列出你的 Harness 工作区。
58
58
 
@@ -63,13 +63,18 @@ dsh-baton 没有任何依赖。`setup` 会把自己复制到 `~/.dsh-baton/app`
63
63
  - **Codex** 在 `dsh_wait` 里等待,每次最多 270 秒,刚好低于它 300 秒的工具超时。没做完时只返回状态,Codex 会直接再调用,不会去总结进度。Codex 不会收到 `wait_command`:它的终端无法在命令结束时唤醒它,只能轮询。
64
64
  - Codex 调用 MCP 工具时会请你批准,批准 `dsh-baton` 的工具即可;无人值守的 `codex exec` 加上 `--approve-for-me`。
65
65
 
66
- **更新:** 再运行一次 `npx -y dsh-baton-mcp@latest setup`,然后重启 Harness 以加载插件改动;新开的对话会用上 MCP 服务器的改动。
66
+ **更新:** MCP 服务器和插件分开更新,两者版本号相同。
67
+ - MCP 服务器:`npx -y dsh-baton-mcp@latest setup`,新开的对话就会用上。
68
+ - 插件:Harness 只给官方插件提供"更新"按钮,所以这个插件要:插件 → dsh-baton-host → **卸载**,再**添加插件** → `dsh-baton-host`。
69
+ - 两边版本对不上时,`setup` 会提示,第一次发现不一致的工具调用也会提示。
67
70
 
68
- **卸载:** `npx -y dsh-baton-mcp setup --remove`,再到 Harness → 插件 → dsh-baton-host → 卸载,最后再运行一次 `setup --remove` 删除 `~/.dsh-baton/app`。
71
+ **卸载:** Harness → 插件 → dsh-baton-host → **卸载**,再运行 `npx -y dsh-baton-mcp setup --remove`。它会删掉 MCP 注册和 `~/.dsh-baton/app`;运行记录留在 `~/.dsh-baton`,不需要的话自己删掉。
69
72
 
70
- **从 dsh-bridge 升级:**照上面运行 `setup`。它会替换掉旧的 `dsh-bridge` MCP 注册;在 Harness 里先卸载 `dsh-bridge-host`,再添加新路径。`DSH_BRIDGE_*` 环境变量和已有的 `~/.dsh-bridge` 文件夹暂时继续可用。工具前缀变成 `mcp__dsh-baton__…`,权限白名单里写了旧前缀的要改一下。
73
+ **从 dsh-bridge 升级:**照上面运行 `setup`。它会替换掉旧的 `dsh-bridge` MCP 注册;在 Harness 里先卸载 `dsh-bridge-host`,再按第 2 步添加 `dsh-baton-host`。`DSH_BRIDGE_*` 环境变量和已有的 `~/.dsh-bridge` 文件夹暂时继续可用。工具前缀变成 `mcp__dsh-baton__…`,权限白名单里写了旧前缀的要改一下。
71
74
 
72
- **从源码安装**(想跟进 `main` 或改代码):`git clone https://github.com/JialaoLiu/dsh-baton.git`,再运行 `node dsh-baton/src/cli.mjs setup`。这时 setup 注册的就是克隆目录本身,更新用 `git pull`。
75
+ **0.4 之前从本地文件夹装的插件:**旧版本让你粘贴 `~/.dsh-baton/app/host`。这样装的仍然能用,但不会跟着 npm 更新:卸载它,再按包名添加 `dsh-baton-host`。`setup` 会提示这一点。
76
+
77
+ **从源码安装**(想跟进 `main` 或改代码):`git clone https://github.com/JialaoLiu/dsh-baton.git`,再运行 `node dsh-baton/src/cli.mjs setup`。这时 setup 注册的就是克隆目录本身,插件也从克隆目录的 `host` 文件夹添加(添加插件 → 粘贴它复制的路径),这样插件改动在发布前就能跑起来;更新用 `git pull`。
73
78
 
74
79
  <details>
75
80
  <summary>手动注册与 Claude 桌面应用</summary>
package/WINDOWS.md CHANGED
@@ -26,17 +26,19 @@ Requires DeepSeek Harness ≥ 0.2.1-rc.2 and Node.js ≥ 22.15. In PowerShell:
26
26
  npx -y dsh-baton-mcp setup
27
27
  ```
28
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.
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's package name to the clipboard.
30
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.
31
+ Then, in Harness: **Plugins / 插件** → **Add plugin / 添加插件** → registry **Official npm registry** → `dsh-baton-host` → **Install / 安装** → **Enable now / 立即启用**. The pipe appears right away (see [Verify](#verify)); if it does not, restart Harness.
32
32
 
33
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
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.
35
+ Update: `npx -y dsh-baton-mcp@latest setup` for the MCP server; for the plugin, Harness → Plugins → dsh-baton-host → Uninstall, then Add plugin → `dsh-baton-host` again (Harness has no update button for non-official plugins).
36
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`; it is no longer in the profile's dependencies, so delete it with Harness quit.
37
+ Remove: Harness → Plugins → dsh-baton-host → Uninstall, and `npx -y dsh-baton-mcp setup --remove`.
38
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`.
39
+ 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 `dsh-baton-host` by name. A plugin added earlier from `.dsh-baton\app\host` (0.3) also works, but `setup` suggests swapping it for the npm package, which is the one that gets updates. 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.
40
+
41
+ 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`; setup then has you add the plugin from `C:\dev\dsh-baton\host` (Add plugin → paste the path it copies).
40
42
 
41
43
  ### If the Plugins page cannot uninstall or install it
42
44
 
@@ -107,7 +109,7 @@ The suite runs on Windows with named pipes. Two POSIX-only checks are skipped: f
107
109
 
108
110
  - **套接字改为命名管道。** Windows 上 Node 不能监听 Unix 套接字文件,所以同一路径被映射成按用户区分的命名管道 `\\.\pipe\dsh-baton-<哈希>`。插件和 MCP 两边算法一致,`DSH_BATON_SOCKET` 仍然可用,Harness 退出后管道自动消失。
109
111
  - **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` 已把路径复制到剪贴板)。
112
+ - **插件用 Harness 插件页按包名安装。** 插件 → 添加插件 → 来源选 Official npm registry → 输入 `dsh-baton-host` → 安装 → 立即启用(`setup` 已把包名复制到剪贴板)。更新插件要先卸载再重新添加,Harness 只给官方插件提供更新按钮。
111
113
  - **一条命令注册 MCP。** `npx -y dsh-baton-mcp setup` 把自己复制到 `%USERPROFILE%\.dsh-baton\app`,再直接写 Claude Code 和 Codex 的用户级配置(用 `node` 的绝对路径),桌面版的 `claude` / `codex` 不在 `PATH` 里也没关系。之后新开对话就有 `dsh_*` 工具;Codex 每次最多等 270 秒,不用再配超时。
112
114
  - **插件页卸不掉或装不上时**(`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
115
  - **headless 需要独立凭据。** 要么登录账户,要么设置 `DEEPSEEK_API_KEY`。桌面版里保存的 Key,headless CLI 读不到。
package/host/README.md CHANGED
@@ -1,14 +1,16 @@
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 / 添加插件 → registry Official npm registry → `dsh-baton-host` → Install / 安装 → Enable now / 立即启用
10
10
 
11
- Then connect the MCP server / 然后接入 MCP 服务器: see [dsh-baton](../README.md).
11
+ Update / 更新: uninstall, then add it again / 先卸载再重新添加. Keep it on the same version as `dsh-baton-mcp` / 版本与 `dsh-baton-mcp` 保持一致.
12
+
13
+ Then connect the MCP server / 然后接入 MCP 服务器: see [dsh-baton](https://github.com/JialaoLiu/dsh-baton#readme).
12
14
 
13
15
  ## Socket protocol / 套接字协议
14
16
 
package/host/lib/index.js CHANGED
@@ -15,7 +15,7 @@ import { socketEndpoint, batonHome, socketPath as socketPathSetting } from './so
15
15
  export const name = 'dsh-baton-host'
16
16
  export const inject = ['sessionController', 'workspaceRegistry']
17
17
 
18
- const VERSION = '0.3.0'
18
+ const VERSION = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version
19
19
  const MAX_EVENTS = 200
20
20
  const MAX_RUNS = 100
21
21
  const QUIET_CONFIRM_MS = 2000
package/host/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-baton-host",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
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.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -53,5 +53,8 @@
53
53
  "url": "git+https://github.com/JialaoLiu/dsh-baton.git",
54
54
  "directory": "host"
55
55
  },
56
- "homepage": "https://github.com/JialaoLiu/dsh-baton#readme"
56
+ "homepage": "https://github.com/JialaoLiu/dsh-baton#readme",
57
+ "scripts": {
58
+ "prepublishOnly": "node ../scripts/version.mjs"
59
+ }
57
60
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-baton-mcp",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "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
5
  "type": "module",
6
6
  "bin": {
@@ -35,7 +35,8 @@
35
35
  "license": "MIT",
36
36
  "scripts": {
37
37
  "test": "node --test --test-concurrency=1 tests/*.test.mjs",
38
- "test:live": "node tests/live.mjs"
38
+ "test:live": "node tests/live.mjs",
39
+ "prepublishOnly": "node scripts/version.mjs"
39
40
  },
40
41
  "repository": {
41
42
  "type": "git",
package/src/core.mjs CHANGED
@@ -150,15 +150,30 @@ function hostCallAt(endpoint, method, params = {}, { timeoutMs = 15000 } = {}) {
150
150
  })
151
151
  }
152
152
 
153
+ export const VERSION = JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf8')).version
154
+
155
+ // The plugin version from the last successful ping.
156
+ let hostVersion = null
157
+
153
158
  export async function hostAvailable() {
154
159
  try {
155
- await hostCall('ping', {}, { timeoutMs: 2000 })
160
+ hostVersion = (await hostCall('ping', {}, { timeoutMs: 2000 }))?.version ?? null
156
161
  return true
157
162
  } catch {
158
163
  return false
159
164
  }
160
165
  }
161
166
 
167
+ export const hostVersionSeen = () => hostVersion
168
+
169
+ // The plugin and the MCP server update separately (Harness and npx), so they can drift.
170
+ // A different major.minor gets a note asking to update the older side; patch releases mix.
171
+ const minorOf = (v) => String(v).split('.').slice(0, 2).join('.')
172
+ export function versionWarning(plugin = hostVersion, server = VERSION) {
173
+ if (!plugin || minorOf(plugin) === minorOf(server)) return null
174
+ return `The Harness plugin dsh-baton-host is ${plugin} but this MCP server is ${server}. Update the older one: the server with \`npx -y dsh-baton-mcp@latest setup\`, the plugin in Harness (Plugins → dsh-baton-host → Uninstall, then Add plugin → dsh-baton-host).`
175
+ }
176
+
162
177
  // Who a relayed message is from, shown on it in the Harness chat: DSH_BATON_SENDER if set,
163
178
  // else the MCP client's name (claude-code, claude-ai → claude; codex-mcp-client → codex).
164
179
  export function senderOf(clientName = setting('SENDER')) {
package/src/server.mjs CHANGED
@@ -5,7 +5,7 @@ import path from 'node:path'
5
5
  import * as core from './core.mjs'
6
6
  import { stableNodePath } from './setup.mjs'
7
7
 
8
- const SERVER = { name: 'dsh-baton', version: '0.3.0' }
8
+ const SERVER = { name: 'dsh-baton', version: core.VERSION }
9
9
  const PROTOCOLS = ['2025-06-18', '2025-03-26', '2024-11-05']
10
10
 
11
11
  const str = (description) => ({ type: 'string', description })
@@ -154,6 +154,7 @@ const TOOLS = [
154
154
  ]
155
155
 
156
156
  let clientName = null
157
+ let versionWarned = false
157
158
 
158
159
  function send(msg) {
159
160
  process.stdout.write(JSON.stringify(msg) + '\n')
@@ -184,7 +185,11 @@ async function handle(req) {
184
185
  const progress = token === undefined ? undefined : (done, message) =>
185
186
  send({ jsonrpc: '2.0', method: 'notifications/progress', params: { progressToken: token, progress: done, message } })
186
187
  const result = await tool.run(req.params.arguments || {}, { progress })
187
- return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }
188
+ const content = [{ type: 'text', text: JSON.stringify(result, null, 2) }]
189
+ // Said once per server process, the first time a call finds the versions apart.
190
+ const warning = !versionWarned && core.versionWarning()
191
+ if (warning) { versionWarned = true; content.push({ type: 'text', text: `Note for the user: ${warning}` }) }
192
+ return { content }
188
193
  } catch (e) {
189
194
  return { content: [{ type: 'text', text: e.message }], isError: true }
190
195
  }
package/src/setup.mjs CHANGED
@@ -5,7 +5,7 @@ import fs from 'node:fs'
5
5
  import os from 'node:os'
6
6
  import path from 'node:path'
7
7
  import { execFileSync } from 'node:child_process'
8
- import { ROOT, config, hostAvailable } from './core.mjs'
8
+ import { ROOT, config, hostAvailable, hostVersionSeen as versionOfHost, versionWarning } from './core.mjs'
9
9
 
10
10
  const NAME = 'dsh-baton'
11
11
  // The name before the rename: setup removes its entries so the tools are not listed twice.
@@ -85,6 +85,63 @@ function installedPlugins() {
85
85
  }
86
86
  }
87
87
 
88
+ // With pnpm's hoisted layout, uninstalling the plugin can leave its node_modules link
89
+ // behind. Remove such a link once the profile no longer lists the plugin; only the
90
+ // link itself goes, never the folder it points to. Returns the links removed and the
91
+ // ones that could not be.
92
+ export function cleanStalePluginLinks(profile = path.join(config.dshHome, 'profiles', 'desktop')) {
93
+ let deps
94
+ try { deps = JSON.parse(fs.readFileSync(path.join(profile, 'package.json'), 'utf8')).dependencies ?? {} } catch { return { removed: [], failed: [] } }
95
+ const removed = [], failed = []
96
+ for (const name of ['dsh-baton-host', 'dsh-bridge-host']) {
97
+ const link = path.join(profile, 'node_modules', name)
98
+ if (deps[name]) continue
99
+ let st
100
+ try { st = fs.lstatSync(link) } catch { continue }
101
+ if (!st.isSymbolicLink()) continue
102
+ try {
103
+ // A Windows junction is a directory link: rmdir removes the link, not its target.
104
+ try { fs.unlinkSync(link) } catch { fs.rmdirSync(link) }
105
+ removed.push(link)
106
+ } catch {
107
+ failed.push(link)
108
+ }
109
+ }
110
+ return { removed, failed }
111
+ }
112
+
113
+ // What setup says about the Harness plugin. The plugin is installed in Harness by its npm
114
+ // package name; a checkout of this repo (setup run from a git clone) is the exception and
115
+ // links its own host folder, so plugin changes can be tried before they are published.
116
+ export function pluginSteps({ current, old }, running, hostDir, fromSource = fs.existsSync(path.join(ROOT, '.git'))) {
117
+ const isLink = (spec) => /^(link|file):/.test(spec ?? '')
118
+ const ok = fromSource ? pluginFrom(current, hostDir) : current && !isLink(current)
119
+ if (ok && !old && running) {
120
+ const warning = versionWarning(running)
121
+ return warning ? ['Harness plugin: running, but out of step.', ` ${warning}`] : [`Harness plugin: running (${running}).`]
122
+ }
123
+ const steps = ['Last step, in DeepSeek Harness:']
124
+ // The plugin was renamed: Harness cannot load the old entry once its folder holds the new name.
125
+ if (old) steps.push(' The plugin is installed under its old name: first Plugins (插件) → dsh-bridge-host → Uninstall (卸载).')
126
+ if (current && !ok) {
127
+ steps.push(isLink(current)
128
+ ? ` The plugin installed now comes from a local folder (${current.replace(/^(link|file):/, '')}): first Plugins (插件) → dsh-baton-host → Uninstall (卸载).`
129
+ : ' The plugin installed now comes from npm, not this checkout: first Plugins (插件) → dsh-baton-host → Uninstall (卸载).')
130
+ }
131
+ if (ok && !old) return ['Harness plugin: installed but not answering. Open Harness and check that dsh-baton-host is switched on in Plugins (插件).']
132
+ if (fromSource) {
133
+ const copied = copyToClipboard(hostDir)
134
+ steps.push(` Plugins (插件) → Add plugin (添加插件) → paste ${copied ? 'the path (already copied)' : 'this path'}: ${hostDir}`)
135
+ } else {
136
+ const copied = copyToClipboard(PLUGIN)
137
+ steps.push(` Plugins (插件) → Add plugin (添加插件) → registry: Official npm registry → package: ${PLUGIN}${copied ? ' (already copied)' : ''}`)
138
+ }
139
+ steps.push(' → Install (安装) → Enable now (立即启用)')
140
+ return steps
141
+ }
142
+
143
+ const PLUGIN = 'dsh-baton-host'
144
+
88
145
  const pluginFrom = (source, hostDir) => source === `link:${hostDir}` || source === `link:${hostDir.split(path.sep).join('/')}`
89
146
 
90
147
  // The node to register: the one on PATH when it is this same binary, since that path
@@ -143,32 +200,28 @@ export async function setup({ remove = false } = {}) {
143
200
 
144
201
  if (remove) {
145
202
  const plugins = installedPlugins()
146
- lines.push('', `To remove the Harness plugin: Harness → Plugins → ${plugins.old && !plugins.current ? 'dsh-bridge-host' : 'dsh-baton-host'} → Uninstall.`)
203
+ if (plugins.current || plugins.old) {
204
+ lines.push('', `To remove the Harness plugin: Harness → Plugins → ${plugins.current ? 'dsh-baton-host' : 'dsh-bridge-host'} → Uninstall.`)
205
+ } else {
206
+ lines.push('', 'Harness plugin: not installed.')
207
+ const links = cleanStalePluginLinks()
208
+ for (const l of links.removed) lines.push(`Removed the leftover plugin link ${l}.`)
209
+ for (const l of links.failed) lines.push(`Could not remove the leftover plugin link ${l}: quit Harness and delete it.`)
210
+ }
147
211
  const app = appDir()
148
212
  if (fs.existsSync(app)) {
149
213
  // Harness loads the plugin from this folder: delete it only once the plugin is gone.
150
214
  if (pluginFrom(plugins.current, path.join(app, 'host'))) lines.push(`Then run this again to delete ${app}.`)
151
215
  else { fs.rmSync(app, { recursive: true, force: true }); lines.push(`Deleted ${app}.`) }
152
216
  }
217
+ const home = config.batonHome
218
+ if (['runs', 'host-runs'].some((d) => fs.existsSync(path.join(home, d)))) {
219
+ lines.push(`Run records are kept in ${home} (runs, host-runs); delete that folder to remove them too.`)
220
+ }
153
221
  return lines.join('\n')
154
222
  }
155
223
  lines.push('', 'MCP server registered user-wide: new conversations in Claude Code and Codex have the dsh_* tools.')
156
224
 
157
- const hostDir = path.join(root, 'host')
158
- const { current: source, old } = installedPlugins()
159
- const fromHere = pluginFrom(source, hostDir)
160
- const running = await hostAvailable()
161
- if (running && !old && (fromHere || !source)) {
162
- lines.push('', 'Harness plugin: running.')
163
- } else {
164
- const copied = copyToClipboard(hostDir)
165
- lines.push('', 'Last step, in DeepSeek Harness:')
166
- // The plugin was renamed: Harness cannot load the old entry once its folder holds the new name.
167
- if (old) lines.push(' The plugin is installed under its old name: first Plugins (插件) → dsh-bridge-host → Uninstall (卸载).')
168
- if (source && !fromHere) lines.push(` The plugin installed now comes from another folder (${source}): first Plugins (插件) → dsh-baton-host → Uninstall (卸载).`)
169
- lines.push(
170
- ` Plugins (插件) → Add plugin (添加插件) → paste ${copied ? 'the path (already copied)' : 'this path'}: ${hostDir}`,
171
- ' → Install (安装) → Enable now (立即启用)')
172
- }
225
+ lines.push('', ...pluginSteps(installedPlugins(), await hostAvailable() ? versionOfHost() : null, path.join(root, 'host')))
173
226
  return lines.join('\n')
174
227
  }