dsh-baton-mcp 0.5.1 → 0.5.3

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.3",
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.3"
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.3",
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.3",
11
11
  "packages": [
12
12
  {
13
13
  "registryType": "npm",
14
14
  "identifier": "dsh-baton-mcp",
15
- "version": "0.5.1",
15
+ "version": "0.5.3",
16
16
  "transport": {
17
17
  "type": "stdio"
18
18
  }
package/src/register.mjs CHANGED
@@ -25,14 +25,15 @@ 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
  }
32
32
 
33
33
  export function readClaudeEntry(text) {
34
34
  const e = text.trim() ? JSON.parse(text).mcpServers?.[NAME] : null
35
- return e?.command ? { command: e.command, args: e.args ?? [] } : null
35
+ if (!e?.command) return null
36
+ return { command: e.command, args: e.args ?? [], ...(e.env && Object.keys(e.env).length ? { env: e.env } : {}) }
36
37
  }
37
38
 
38
39
  const tomlString = (s) => (/['\n\r]/.test(s) ? JSON.stringify(s) : `'${s}'`)
@@ -55,26 +56,35 @@ export function editCodexConfig(text, entry) {
55
56
  let out = kept.join('\n').replace(/\s+$/, '')
56
57
  if (entry) {
57
58
  out += `${out ? '\n\n' : ''}[mcp_servers.${NAME}]\ncommand = ${tomlString(entry.command)}\nargs = [${entry.args.map(tomlString).join(', ')}]`
59
+ const env = Object.entries(entry.env ?? {})
60
+ if (env.length) out += `\n\n[mcp_servers.${NAME}.env]\n${env.map(([k, v]) => `${k} = ${tomlString(v)}`).join('\n')}`
58
61
  }
59
62
  return out ? out + '\n' : ''
60
63
  }
61
64
 
62
- // The command and args of [mcp_servers.dsh-baton], as setup writes them (single-line strings).
65
+ // The command, args and env of [mcp_servers.dsh-baton] (and its .env table), as setup
66
+ // writes them (single-line strings).
63
67
  export function readCodexEntry(text) {
64
68
  const str = String.raw`'[^'\n]*'|"(?:[^"\\\n]|\\.)*"`
65
69
  const unquote = (s) => (s.startsWith("'") ? s.slice(1, -1) : JSON.parse(s))
66
- let inTable = false
70
+ let table = null
67
71
  let command = null, args = []
72
+ const env = {}
68
73
  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) {
74
+ if (ANY_TABLE.test(line)) {
75
+ const m = line.match(/^\s*\[\s*mcp_servers\s*\.\s*(["']?)dsh-baton\1\s*(\.\s*env\s*)?\]/)
76
+ table = m ? (m[2] ? 'env' : 'main') : null
77
+ } else if (table === 'main') {
71
78
  const c = line.match(new RegExp(`^\\s*command\\s*=\\s*(${str})`))
72
79
  if (c) command = unquote(c[1])
73
80
  const a = line.match(/^\s*args\s*=\s*\[(.*)\]/)
74
81
  if (a) args = [...a[1].matchAll(new RegExp(str, 'g'))].map((m) => unquote(m[0]))
82
+ } else if (table === 'env') {
83
+ const kv = line.match(new RegExp(`^\\s*([\\w-]+)\\s*=\\s*(${str})`))
84
+ if (kv) env[kv[1]] = unquote(kv[2])
75
85
  }
76
86
  }
77
- return command ? { command, args } : null
87
+ return command ? { command, args, ...(Object.keys(env).length ? { env } : {}) } : null
78
88
  }
79
89
 
80
90
  // Write only when the content changes, keep one backup, and replace the file atomically.
@@ -122,10 +132,10 @@ export function nodeVersionOk(version) {
122
132
  return major > MIN_NODE[0] || (major === MIN_NODE[0] && minor >= MIN_NODE[1])
123
133
  }
124
134
 
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.
135
+ // The user's own Node.js for the MCP server, preferred over Harness's runtime (see
136
+ // harnessRuntime): the first `node` on PATH, then the usual install folders, never the
137
+ // launcher scripts inside the Harness app (they only work from Harness itself), and only
138
+ // a new enough one.
129
139
  export function findNode({ env = process.env, platform = process.platform, exclude = [], version = nodeVersion } = {}) {
130
140
  const exe = platform === 'win32' ? 'node.exe' : 'node'
131
141
  const p = platform === 'win32' ? path.win32 : path.posix
@@ -185,6 +195,11 @@ export function compareVersions(a, b) {
185
195
  return (String(b).includes('-') ? 1 : 0) - (String(a).includes('-') ? 1 : 0)
186
196
  }
187
197
 
198
+ // Without a Node.js of the user's own, the server runs on Harness's: the app binary in
199
+ // Node mode, the way Harness runs its own scripts. Its path is the app's own (it moves
200
+ // only if the app does), and if it ever breaks, the next Harness start rewrites it.
201
+ export const harnessRuntime = (exe) => ({ command: exe, env: { ELECTRON_RUN_AS_NODE: '1' } })
202
+
188
203
  // Run by the Harness plugin when it is switched on (and on every Harness start, so a
189
204
  // plugin update brings the server along). `root` holds the MCP server package: the repo
190
205
  // checkout the plugin was added from, or the dsh-baton-mcp package installed with it.
@@ -192,9 +207,11 @@ export function compareVersions(a, b) {
192
207
  // as is, like `setup` from a clone) or that copy is already as new.
193
208
  // - An entry that already starts this server, or a working one that starts another copy
194
209
  // (a clone, say), is left alone; a missing or broken one is (re)written.
210
+ // - It starts the server with the user's Node.js when there is a new enough one, else
211
+ // with `fallback` (Harness's own runtime, see harnessRuntime).
195
212
  // 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 }
213
+ export function autoRegister({ root, home = os.homedir(), env = process.env, node = null, harnessDir = null, fallback = null, find = findNode, app = appDir(batonHome(env, home)) } = {}) {
214
+ const result = { ok: false, version: versionOf(root), clients: [], error: null, node: null, runtime: null }
198
215
  try {
199
216
  let serverRoot = root
200
217
  if (!fs.existsSync(path.join(root, '.git')) && path.resolve(root) !== path.resolve(app)) {
@@ -203,28 +220,42 @@ export function autoRegister({ root, home = os.homedir(), env = process.env, nod
203
220
  serverRoot = app
204
221
  }
205
222
  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])
223
+ // Whether an entry can start a server. A bare command (`npx -y dsh-baton-mcp`, as MCP
224
+ // directories suggest) is the user's own choice and resolved on the client's PATH: kept.
225
+ // Harness's own binary only works in Node mode.
226
+ const bare = (cmd) => !/[\\/]/.test(cmd)
227
+ const works = (e) => {
228
+ if (!e?.command) return false
229
+ // `node /old/path/server.mjs` with that file gone is still broken.
230
+ if (bare(e.command)) return !e.args?.[0] || bare(e.args[0]) || fs.existsSync(e.args[0])
231
+ if (!fs.existsSync(e.command) || !e.args?.[0] || !fs.existsSync(e.args[0])) return false
232
+ return !fallback || path.resolve(e.command) !== path.resolve(fallback.command) || e.env?.ELECTRON_RUN_AS_NODE === '1'
233
+ }
207
234
  const list = clients(env, home)
208
- let bin = node
235
+ let launch = node ? { command: node } : null
209
236
  for (const c of list) {
210
237
  if (!c.present) { result.clients.push({ id: c.id, label: c.label, state: 'not-installed' }); continue }
211
238
  try {
212
239
  const text = fs.existsSync(c.file) ? fs.readFileSync(c.file, 'utf8') : ''
213
240
  const current = c.read(text)
214
241
  if (works(current)) {
215
- const same = path.resolve(current.args[0]) === path.resolve(script)
242
+ const same = !bare(current.command) && path.resolve(current.args[0]) === path.resolve(script)
216
243
  result.clients.push({ id: c.id, label: c.label, state: same ? 'unchanged' : 'kept', script: current.args[0] })
217
244
  continue
218
245
  }
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] }))
246
+ if (!launch) {
247
+ const found = find({ env, exclude: [harnessDir] })
248
+ launch = found ? { command: found } : fallback
249
+ }
250
+ if (!launch) { result.clients.push({ id: c.id, label: c.label, state: 'failed', error: 'no-node' }); continue }
251
+ rewrite(c.file, (t) => c.edit(t, { ...launch, args: [script] }))
222
252
  result.clients.push({ id: c.id, label: c.label, state: 'registered' })
223
253
  } catch (e) {
224
254
  result.clients.push({ id: c.id, label: c.label, state: 'failed', error: e.message })
225
255
  }
226
256
  }
227
- result.node = bin
257
+ result.node = launch?.command ?? null
258
+ result.runtime = !launch ? null : launch === fallback ? 'harness' : 'node'
228
259
  const present = result.clients.filter((c) => c.state !== 'not-installed')
229
260
  result.ok = present.length > 0 && present.every((c) => c.state !== 'failed')
230
261
  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 {