dsh-baton-mcp 0.3.0 → 0.3.1

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/WINDOWS.md CHANGED
@@ -34,7 +34,7 @@ Nothing else to configure: Claude Code waits for a run in the background (Git Ba
34
34
 
35
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
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
+ 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
38
 
39
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`.
40
40
 
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.3.1",
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",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-baton-mcp",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
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": {
package/src/core.mjs CHANGED
@@ -150,15 +150,28 @@ 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
+ // The plugin and the MCP server update separately (Harness and npx), so they can drift.
168
+ // A different major.minor gets a note asking to update the older side; patch releases mix.
169
+ const minorOf = (v) => String(v).split('.').slice(0, 2).join('.')
170
+ export function versionWarning(plugin = hostVersion, server = VERSION) {
171
+ if (!plugin || minorOf(plugin) === minorOf(server)) return null
172
+ 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).`
173
+ }
174
+
162
175
  // Who a relayed message is from, shown on it in the Harness chat: DSH_BATON_SENDER if set,
163
176
  // else the MCP client's name (claude-code, claude-ai → claude; codex-mcp-client → codex).
164
177
  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
@@ -85,6 +85,31 @@ 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
+
88
113
  const pluginFrom = (source, hostDir) => source === `link:${hostDir}` || source === `link:${hostDir.split(path.sep).join('/')}`
89
114
 
90
115
  // The node to register: the one on PATH when it is this same binary, since that path
@@ -143,13 +168,24 @@ export async function setup({ remove = false } = {}) {
143
168
 
144
169
  if (remove) {
145
170
  const plugins = installedPlugins()
146
- lines.push('', `To remove the Harness plugin: Harness → Plugins → ${plugins.old && !plugins.current ? 'dsh-bridge-host' : 'dsh-baton-host'} → Uninstall.`)
171
+ if (plugins.current || plugins.old) {
172
+ lines.push('', `To remove the Harness plugin: Harness → Plugins → ${plugins.current ? 'dsh-baton-host' : 'dsh-bridge-host'} → Uninstall.`)
173
+ } else {
174
+ lines.push('', 'Harness plugin: not installed.')
175
+ const links = cleanStalePluginLinks()
176
+ for (const l of links.removed) lines.push(`Removed the leftover plugin link ${l}.`)
177
+ for (const l of links.failed) lines.push(`Could not remove the leftover plugin link ${l}: quit Harness and delete it.`)
178
+ }
147
179
  const app = appDir()
148
180
  if (fs.existsSync(app)) {
149
181
  // Harness loads the plugin from this folder: delete it only once the plugin is gone.
150
182
  if (pluginFrom(plugins.current, path.join(app, 'host'))) lines.push(`Then run this again to delete ${app}.`)
151
183
  else { fs.rmSync(app, { recursive: true, force: true }); lines.push(`Deleted ${app}.`) }
152
184
  }
185
+ const home = config.batonHome
186
+ if (['runs', 'host-runs'].some((d) => fs.existsSync(path.join(home, d)))) {
187
+ lines.push(`Run records are kept in ${home} (runs, host-runs); delete that folder to remove them too.`)
188
+ }
153
189
  return lines.join('\n')
154
190
  }
155
191
  lines.push('', 'MCP server registered user-wide: new conversations in Claude Code and Codex have the dsh_* tools.')