dsh-baton-mcp 0.5.1 → 0.5.2

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
@@ -7,9 +7,13 @@
7
7
  </p>
8
8
 
9
9
  <p align="center">
10
- macOS · Windows (<a href="WINDOWS.md">guide</a>) · Node.js ≥ 22.15 · zero dependencies · MIT
10
+ macOS · Windows (<a href="WINDOWS.md">guide</a>) · no Node.js needed · zero dependencies · MIT
11
11
  </p>
12
12
 
13
+ Claude Code and Codex each hand a task to DeepSeek Harness, and its subagents build this rainy-night street in 1 h 41 min (40 s, sound on):
14
+
15
+ https://github.com/user-attachments/assets/72e2fdff-275e-48bc-8e5a-39ffb025a0a8
16
+
13
17
  <p align="center">
14
18
  <img src="./assets/readme/features.svg" width="100%" alt="Create workspaces and sessions, continue a session, run live in the Harness UI labelled with the sender, get woken when it is done, no extra API key, macOS and Windows with zero dependencies">
15
19
  </p>
@@ -40,7 +44,7 @@ Codex and Claude take turns in the same Harness session, and every message is la
40
44
  <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
45
  </p>
42
46
 
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.
47
+ Requires DeepSeek Harness ≥ 0.2.1-rc.2 (signed in) and Claude Code or Codex. Nothing else: no Node.js, no terminal. The steps are the same on macOS and Windows.
44
48
 
45
49
  **1. Add the plugin in Harness:** **Plugins** (插件) → **Add plugin** (添加插件) → type `dsh-baton-host` → **Install** (安装) → **Enable now** (立即启用).
46
50
 
@@ -48,7 +52,7 @@ Switched on, the plugin registers dsh-baton for Claude Code and Codex, for every
48
52
 
49
53
  **2. Check:** in a **new** Claude Code or Codex conversation, ask it to call `dsh_list_workspaces`. It lists your Harness workspaces.
50
54
 
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).
55
+ That is all. If Harness says it could not register, run `npx -y dsh-baton-mcp setup` in a terminal (this needs Node.js ≥ 22.15); it does the same registration. Updating, removing and Windows details are [further down](#install-details).
52
56
 
53
57
  ## How it works
54
58
 
@@ -134,7 +138,7 @@ When the plugin is unreachable, the MCP server falls back to Harness's `dsh head
134
138
 
135
139
  ## Install details
136
140
 
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.
141
+ **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 starts the server with your own Node.js ≥ 22.15 when it finds one on `PATH`; without one, with Harness's own runtime (the Harness app in Node mode, `ELECTRON_RUN_AS_NODE=1`), so nothing else has to be installed. Either way 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
142
 
139
143
  **`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
144
 
package/README.zh-CN.md CHANGED
@@ -7,7 +7,7 @@
7
7
  </p>
8
8
 
9
9
  <p align="center">
10
- macOS · Windows(<a href="WINDOWS.md">说明</a>)· Node.js ≥ 22.15 · 零依赖 · MIT
10
+ macOS · Windows(<a href="WINDOWS.md">说明</a>)· 不用装 Node.js · 零依赖 · MIT
11
11
  </p>
12
12
 
13
13
  <p align="center">
@@ -40,7 +40,7 @@ Codex 和 Claude 轮流往同一个 Harness 对话发消息,每条都标明是
40
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;装了 Claude Code 或 Codex。macOS 和 Windows 步骤完全相同。
43
+ 需要:DeepSeek Harness ≥ 0.2.1-rc.2 并已登录;装了 Claude Code 或 Codex。别的都不用装,不用 Node.js,也不用开终端。macOS 和 Windows 步骤完全相同。
44
44
 
45
45
  **1. 在 Harness 里添加插件:** 插件 → 添加插件 → 输入 `dsh-baton-host` → 安装 → 立即启用。
46
46
 
@@ -48,7 +48,7 @@ Codex 和 Claude 轮流往同一个 Harness 对话发消息,每条都标明是
48
48
 
49
49
  **2. 检查:**在 Claude Code 或 Codex 里**新开**一个对话,让它调用 `dsh_list_workspaces`,会列出你的 Harness 工作区。
50
50
 
51
- 就这些。如果 Harness 提示没能注册(比如找不到 Node.js),在终端运行 `npx -y dsh-baton-mcp setup`,它做的是同样的注册。更新、卸载和 Windows 说明在[后面](#安装细节)。
51
+ 就这些。如果 Harness 提示没能注册,在终端运行 `npx -y dsh-baton-mcp setup`(这需要 Node.js ≥ 22.15),它做的是同样的注册。更新、卸载和 Windows 说明在[后面](#安装细节)。
52
52
 
53
53
  ## 工作原理
54
54
 
@@ -136,7 +136,7 @@ dsh-baton rename-workspace ~/code/new-app 新名字
136
136
 
137
137
  ## 安装细节
138
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`。
139
+ **插件启用时做了什么。**它把 MCP 服务器复制到 `~/.dsh-baton/app` 这个固定的文件夹,再直接把 MCP 注册写进 `~/.claude.json` 和 `~/.codex/config.toml`,各留一份 `*.bak-dsh-baton` 备份。电脑上有 Node.js ≥ 22.15(在 `PATH` 里能找到)就用它启动服务器;没有的话,就用 Harness 自带的运行环境(Harness 应用本身以 Node 模式运行,`ELECTRON_RUN_AS_NODE=1`),不用另外安装任何东西。两种情况下,只装了桌面应用、`claude` / `codex` 不在 `PATH` 里也能用。每次 Harness 启动都会再检查一遍:已经正确的注册、或者指向另一份可用 dsh-baton 的注册,都不会改动。在 Harness 里输入包名后,安装源会自动切到「npm 官方源」(英文界面是 "Official npm registry"),不用另外选择,也不用重启。不想让插件自动注册,给 Harness 设置 `DSH_BATON_AUTO_REGISTER=0`。
140
140
 
141
141
  **`npx -y dsh-baton-mcp setup`** 在终端里做同样的注册:用于插件没能注册的情况,或者从 MCP 目录过来的用户。插件已经注册过时它什么都不改;插件没装的话,它会提示去添加。
142
142
 
package/WINDOWS.md CHANGED
@@ -22,7 +22,7 @@ Verified on Windows 11 with DeepSeek Harness 0.2.1-rc.2 (desktop app), Node.js 2
22
22
 
23
23
  Exactly as in the [README](README.md#quick-start): in Harness add the plugin `dsh-baton-host` and switch it on. Updating and removing are the same too ([Install details](README.md#install-details)). On Windows:
24
24
 
25
- - 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.
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`). Without Node.js it registers `DeepSeek Harness.exe` itself with `ELECTRON_RUN_AS_NODE=1` (Node mode, no window), so nothing else needs installing, 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
26
  - Claude Code runs the background `wait_command` in Git Bash.
27
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.
28
28
 
@@ -96,6 +96,6 @@ The suite runs on Windows with named pipes. Two POSIX-only checks are skipped: f
96
96
  - **套接字改为命名管道。** Windows 上 Node 不能监听 Unix 套接字文件,所以同一路径被映射成按用户区分的命名管道 `\\.\pipe\dsh-baton-<哈希>`。插件和 MCP 两边算法一致,`DSH_BATON_SOCKET` 仍然可用,Harness 退出后管道自动消失。
97
97
  - **headless 用 Harness 自带的 CLI。** Harness ≥ 0.2.1 自带 `resources\runtime\cli\bin\dsh.cmd`;更早的版本只有打包在 `app.asar` 里的 CLI,由 Harness 自己的 exe 以 Node 模式运行。会按顺序自动查找:`DSH_BIN`、`PATH` 里的 `dsh`、默认安装位置,以及注册表里的自定义安装目录。
98
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`。
99
+ - **插件启用时自动注册 MCP。** 它把 MCP 服务器复制到 `%USERPROFILE%\.dsh-baton\app`,再直接写 Claude Code 和 Codex 的用户级配置(用 `PATH`、`%ProgramFiles%\nodejs` 或 nvm-windows 里找到的 `node.exe` 绝对路径;没装 Node.js 时,改用 `DeepSeek Harness.exe` 本身以 Node 模式运行,不弹窗口,不用另外安装任何东西),桌面版的 `claude` / `codex` 不在 `PATH` 里也没关系。之后新开对话就有 `dsh_*` 工具;Codex 每次最多等 270 秒,不用再配超时。没能注册时,在 PowerShell 里运行 `npx -y dsh-baton-mcp setup`。
100
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,插件页的卸载和添加就正常了;如果插件被关掉了,记得重新打开。
101
101
  - **headless 需要独立凭据。** 要么登录账户,要么设置 `DEEPSEEK_API_KEY`。桌面版里保存的 Key,headless CLI 读不到。
package/host/lib/index.js CHANGED
@@ -663,8 +663,10 @@ export function apply(ctx) {
663
663
  setImmediate(guard('register', async () => {
664
664
  const root = serverRoot()
665
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() })
666
+ const { autoRegister, harnessRuntime } = await import(pathToFileURL(path.join(root, 'src', 'register.mjs')).href)
667
+ // Inside the desktop app, its own binary can stand in for a missing Node.js.
668
+ const fallback = process.versions.electron && harnessRuntime ? harnessRuntime(process.execPath) : null
669
+ const result = autoRegister({ root, harnessDir: harnessDir(), fallback })
668
670
  registration = { state: 'done', ...result }
669
671
  const summary = result.clients.map((c) => `${c.label}: ${c.state}${c.error ? ` (${c.error})` : ''}`).join(', ')
670
672
  ctx.logger?.[result.ok ? 'info' : 'error']?.(`dsh-baton-host MCP registration: ${summary || result.error}`)
@@ -678,9 +680,9 @@ export function apply(ctx) {
678
680
  kind: 'exact',
679
681
  path: '/dsh-baton/status',
680
682
  handler: (request, response) => {
681
- const { state, ok, error, version, clients = [] } = registration
683
+ const { state, ok, error, version, runtime, clients = [] } = registration
682
684
  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 })) } }))
685
+ response.end(JSON.stringify({ version: VERSION, registration: { state, ok, error, version, runtime, clients: clients.map(({ id, label, state }) => ({ id, label, state })) } }))
684
686
  },
685
687
  }), 'dsh-baton-host status route')
686
688
  })
package/host/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-baton-host",
3
- "version": "0.5.1",
3
+ "version": "0.5.2",
4
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",
@@ -20,7 +20,7 @@
20
20
  "LICENSE"
21
21
  ],
22
22
  "dependencies": {
23
- "dsh-baton-mcp": "0.5.1"
23
+ "dsh-baton-mcp": "0.5.2"
24
24
  },
25
25
  "license": "MIT",
26
26
  "dsh": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-baton-mcp",
3
- "version": "0.5.1",
3
+ "version": "0.5.2",
4
4
  "mcpName": "io.github.JialaoLiu/dsh-baton",
5
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.",
6
6
  "type": "module",
package/server.json CHANGED
@@ -7,12 +7,12 @@
7
7
  "url": "https://github.com/JialaoLiu/dsh-baton",
8
8
  "source": "github"
9
9
  },
10
- "version": "0.5.1",
10
+ "version": "0.5.2",
11
11
  "packages": [
12
12
  {
13
13
  "registryType": "npm",
14
14
  "identifier": "dsh-baton-mcp",
15
- "version": "0.5.1",
15
+ "version": "0.5.2",
16
16
  "transport": {
17
17
  "type": "stdio"
18
18
  }
package/src/register.mjs CHANGED
@@ -25,7 +25,7 @@ export function editClaudeConfig(text, entry) {
25
25
  const cfg = text.trim() ? JSON.parse(text) : {}
26
26
  const servers = { ...(cfg.mcpServers ?? {}) }
27
27
  delete servers[OLD_NAME]
28
- if (entry) servers[NAME] = { type: 'stdio', command: entry.command, args: entry.args, env: {} }
28
+ if (entry) servers[NAME] = { type: 'stdio', command: entry.command, args: entry.args, env: entry.env ?? {} }
29
29
  else delete servers[NAME]
30
30
  return JSON.stringify({ ...cfg, mcpServers: servers }, null, 2) + '\n'
31
31
  }
@@ -55,6 +55,8 @@ export function editCodexConfig(text, entry) {
55
55
  let out = kept.join('\n').replace(/\s+$/, '')
56
56
  if (entry) {
57
57
  out += `${out ? '\n\n' : ''}[mcp_servers.${NAME}]\ncommand = ${tomlString(entry.command)}\nargs = [${entry.args.map(tomlString).join(', ')}]`
58
+ const env = Object.entries(entry.env ?? {})
59
+ if (env.length) out += `\n\n[mcp_servers.${NAME}.env]\n${env.map(([k, v]) => `${k} = ${tomlString(v)}`).join('\n')}`
58
60
  }
59
61
  return out ? out + '\n' : ''
60
62
  }
@@ -185,6 +187,11 @@ export function compareVersions(a, b) {
185
187
  return (String(b).includes('-') ? 1 : 0) - (String(a).includes('-') ? 1 : 0)
186
188
  }
187
189
 
190
+ // Without a Node.js of the user's own, the server runs on Harness's: the app binary in
191
+ // Node mode, the way Harness runs its own scripts. Its path is the app's own (it moves
192
+ // only if the app does), and if it ever breaks, the next Harness start rewrites it.
193
+ export const harnessRuntime = (exe) => ({ command: exe, env: { ELECTRON_RUN_AS_NODE: '1' } })
194
+
188
195
  // Run by the Harness plugin when it is switched on (and on every Harness start, so a
189
196
  // plugin update brings the server along). `root` holds the MCP server package: the repo
190
197
  // checkout the plugin was added from, or the dsh-baton-mcp package installed with it.
@@ -192,9 +199,11 @@ export function compareVersions(a, b) {
192
199
  // as is, like `setup` from a clone) or that copy is already as new.
193
200
  // - An entry that already starts this server, or a working one that starts another copy
194
201
  // (a clone, say), is left alone; a missing or broken one is (re)written.
202
+ // - It starts the server with the user's Node.js when there is a new enough one, else
203
+ // with `fallback` (Harness's own runtime, see harnessRuntime).
195
204
  // 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 }
205
+ export function autoRegister({ root, home = os.homedir(), env = process.env, node = null, harnessDir = null, fallback = null, find = findNode, app = appDir(batonHome(env, home)) } = {}) {
206
+ const result = { ok: false, version: versionOf(root), clients: [], error: null, node: null, runtime: null }
198
207
  try {
199
208
  let serverRoot = root
200
209
  if (!fs.existsSync(path.join(root, '.git')) && path.resolve(root) !== path.resolve(app)) {
@@ -205,7 +214,7 @@ export function autoRegister({ root, home = os.homedir(), env = process.env, nod
205
214
  const script = path.join(serverRoot, 'src', 'server.mjs')
206
215
  const works = (e) => e && fs.existsSync(e.command) && e.args?.[0] && fs.existsSync(e.args[0])
207
216
  const list = clients(env, home)
208
- let bin = node
217
+ let launch = node ? { command: node } : null
209
218
  for (const c of list) {
210
219
  if (!c.present) { result.clients.push({ id: c.id, label: c.label, state: 'not-installed' }); continue }
211
220
  try {
@@ -216,15 +225,19 @@ export function autoRegister({ root, home = os.homedir(), env = process.env, nod
216
225
  result.clients.push({ id: c.id, label: c.label, state: same ? 'unchanged' : 'kept', script: current.args[0] })
217
226
  continue
218
227
  }
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] }))
228
+ if (!launch) {
229
+ const found = find({ env, exclude: [harnessDir] })
230
+ launch = found ? { command: found } : fallback
231
+ }
232
+ if (!launch) { result.clients.push({ id: c.id, label: c.label, state: 'failed', error: 'no-node' }); continue }
233
+ rewrite(c.file, (t) => c.edit(t, { ...launch, args: [script] }))
222
234
  result.clients.push({ id: c.id, label: c.label, state: 'registered' })
223
235
  } catch (e) {
224
236
  result.clients.push({ id: c.id, label: c.label, state: 'failed', error: e.message })
225
237
  }
226
238
  }
227
- result.node = bin
239
+ result.node = launch?.command ?? null
240
+ result.runtime = !launch ? null : launch === fallback ? 'harness' : 'node'
228
241
  const present = result.clients.filter((c) => c.state !== 'not-installed')
229
242
  result.ok = present.length > 0 && present.every((c) => c.state !== 'failed')
230
243
  if (!present.length) result.error = 'no-clients'
package/src/server.mjs CHANGED
@@ -36,10 +36,12 @@ const ARCHIVED = 'Sessions not in dsh_list_sessions are archived or gone: do not
36
36
  // A shell command that blocks until a run ends and prints its reply. Run in the
37
37
  // background by a client that wakes the model when the command exits (Claude Code),
38
38
  // it waits without spending any turns. Plain "<node>" "<cli>" wait <id> form: it runs
39
- // in POSIX shells and in Git Bash, which Claude Code uses on Windows too.
39
+ // in POSIX shells and in Git Bash, which Claude Code uses on Windows too. Registered on
40
+ // Harness's own binary (no Node.js installed), the command keeps it in Node mode.
40
41
  const quote = (p) => `"${p}"`
42
+ const nodeMode = process.versions.electron ? 'ELECTRON_RUN_AS_NODE=1 ' : ''
41
43
  const waitCommand = (runId) =>
42
- /^[\w.-]+$/.test(runId ?? '') ? `${quote(stableNodePath())} ${quote(path.join(core.ROOT, 'src', 'cli.mjs'))} wait ${runId}` : undefined
44
+ /^[\w.-]+$/.test(runId ?? '') ? `${nodeMode}${quote(stableNodePath())} ${quote(path.join(core.ROOT, 'src', 'cli.mjs'))} wait ${runId}` : undefined
43
45
 
44
46
  // Wait in short chunks, reporting progress between them (clients that support it can
45
47
  // show it or reset their timer). An unfinished wait returns only the state, not the run's
package/src/setup.mjs CHANGED
@@ -4,7 +4,7 @@ import fs from 'node:fs'
4
4
  import os from 'node:os'
5
5
  import path from 'node:path'
6
6
  import { execFileSync } from 'node:child_process'
7
- import { ROOT, config, hostAvailable, hostVersionSeen as versionOfHost, versionWarning } from './core.mjs'
7
+ import { ROOT, VERSION, config, hostAvailable, hostVersionSeen as versionOfHost, versionWarning } from './core.mjs'
8
8
  import { appDir, installApp, rewrite, stableNodePath, claudeConfigPath, codexConfigPath, editClaudeConfig, editCodexConfig } from './register.mjs'
9
9
 
10
10
  export { appDir, installApp, stableNodePath, claudeConfigPath, codexConfigPath, editClaudeConfig, editCodexConfig }
@@ -94,11 +94,21 @@ const pluginFrom = (source, hostDir) => source === `link:${hostDir}` || source =
94
94
  // time. setup then copies it to a stable folder and registers that copy instead.
95
95
  export const inNpxCache = (root = ROOT) => root.split(/[\\/]/).includes('_npx')
96
96
 
97
+ export function sameVersionAt(dir, version = VERSION) {
98
+ try {
99
+ return JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8')).version === version && fs.existsSync(path.join(dir, 'src', 'server.mjs'))
100
+ } catch {
101
+ return false
102
+ }
103
+ }
104
+
97
105
  export async function setup({ remove = false } = {}) {
98
- const root = !remove && inNpxCache() ? installApp(ROOT) : ROOT
106
+ // The same version is already in place (the Harness plugin copied it, say): leave it.
107
+ const copy = !remove && inNpxCache() && !sameVersionAt(appDir())
108
+ const root = !remove && inNpxCache() ? (copy ? installApp(ROOT) : appDir()) : ROOT
99
109
  const entry = remove ? null : { command: stableNodePath(), args: [path.join(root, 'src', 'server.mjs')] }
100
110
  const lines = []
101
- if (root !== ROOT) lines.push(`Installed dsh-baton to ${root}`)
111
+ if (root !== ROOT) lines.push(copy ? `Installed dsh-baton to ${root}` : `dsh-baton ${VERSION} is already in ${root}`)
102
112
  const register = (label, file, present, edit) => {
103
113
  if (!remove && !present) return lines.push(`- ${label}: not installed, skipped`)
104
114
  try {