dsh-baton-mcp 0.6.8 → 0.6.9
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 +2 -2
- package/README.zh-CN.md +2 -2
- package/host/package.json +2 -2
- package/package.json +1 -1
- package/server.json +2 -2
- package/src/cli.mjs +5 -0
- package/src/core.mjs +41 -3
- package/src/mcp.mjs +6 -1
package/README.md
CHANGED
|
@@ -53,7 +53,7 @@ Switched on, the plugin registers dsh-baton for Claude Code and Codex, for every
|
|
|
53
53
|
|
|
54
54
|
That is all. To hand a task over, end the request with "hand it to DeepSeek Harness". Updating, removing, Windows, and registering by hand when the plugin could not, are [further down](#install-details).
|
|
55
55
|
|
|
56
|
-
**
|
|
56
|
+
**Installed on the day of a release?** For about a day after a release, Harness installs the previous version, and its install dialog says so. Open the plugin's page and press **Update to x.y.z** (更新到), or add the plugin with its version, such as `dsh-baton-host@0.6.8`.
|
|
57
57
|
|
|
58
58
|
<p align="center"><picture><source media="(prefers-color-scheme: dark)" srcset="./assets/readme/en/divider-dark.svg"><img src="./assets/readme/en/divider.svg" width="100%" alt=""></picture></p>
|
|
59
59
|
|
|
@@ -284,7 +284,7 @@ When you update or remove the plugin, come from dsh-bridge, run from source, or
|
|
|
284
284
|
|
|
285
285
|
**`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.
|
|
286
286
|
|
|
287
|
-
**Updating.** Harness
|
|
287
|
+
**Updating.** On Harness 0.2.2 and later: Plugins → dsh-baton-host → **Update to x.y.z** (更新到) under Version; if it still says up to date, press **Check again** (重新检查) first. Harness 0.2.1 has no update button for this plugin: **Uninstall**, then **Add plugin** → `dsh-baton-host`. For about a day after a release, Harness installs the previous version instead (pnpm's `minimumReleaseAge` holds back versions published very recently, and the install dialog says so); to get the new one at once, type the version: `dsh-baton-host@0.6.8`. A pre-release must always be added by its exact version, such as `dsh-baton-host@0.6.0-next.0`: Harness resolves `@next` to a broken placeholder. Plain `dsh-baton-host` gets the latest release. 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, naming the older side and the newer side's exact version to install.
|
|
288
288
|
|
|
289
289
|
**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.
|
|
290
290
|
|
package/README.zh-CN.md
CHANGED
|
@@ -53,7 +53,7 @@ dsh-baton 是一个 MCP 服务器加一个 Harness 小插件。主 Agent 用一
|
|
|
53
53
|
|
|
54
54
|
就这些。要转交任务,在需求最后加一句“交给 DeepSeek Harness 去做”。更新、卸载、Windows,以及插件没能注册时怎么手动注册,在[后面](#安装细节)。
|
|
55
55
|
|
|
56
|
-
|
|
56
|
+
**刚发版就安装?** 新版本发布后大约一天内,Harness 会装上一个版本,安装弹窗里会说明。打开插件详情页点 **更新到 x.y.z**,或者添加插件时带上版本号,比如 `dsh-baton-host@0.6.8`。
|
|
57
57
|
|
|
58
58
|
<p align="center"><picture><source media="(prefers-color-scheme: dark)" srcset="./assets/readme/zh/divider-dark.svg"><img src="./assets/readme/zh/divider.svg" width="100%" alt=""></picture></p>
|
|
59
59
|
|
|
@@ -284,7 +284,7 @@ Harness 插件连不上时:
|
|
|
284
284
|
|
|
285
285
|
**`npx -y dsh-baton-mcp setup`** 在终端里做同样的注册:用于插件没能注册的情况,或者从 MCP 目录过来的用户。插件已经注册过时它什么都不改;插件没装的话,它会提示去添加。
|
|
286
286
|
|
|
287
|
-
**更新**。Harness
|
|
287
|
+
**更新**。Harness 0.2.2 及以上:插件 → dsh-baton-host → 版本一栏里的 **更新到 x.y.z**;如果还显示已是最新版本,先点 **重新检查**。Harness 0.2.1 没有这个插件的更新按钮:**卸载**,再**添加插件** → `dsh-baton-host`。新版本发布后大约一天内,Harness 会装上一个版本(pnpm 的 `minimumReleaseAge` 会暂缓刚发布的版本,安装弹窗里会说明);想马上用新版,输入带版本号的名字:`dsh-baton-host@0.6.8`。预发布版必须写确切版本号添加,比如 `dsh-baton-host@0.6.0-next.0`:Harness 会把 `@next` 解析成一个坏掉的占位包。只输入 `dsh-baton-host` 装的是最新正式版。重新启用后,插件会把 MCP 服务器也更新到同一版本,新开的对话就会用上。万一两边版本对不上,第一次发现不一致的工具调用会提示,并写明哪一边旧、另一边的确切版本号。
|
|
288
288
|
|
|
289
289
|
**卸载**。Harness → 插件 → dsh-baton-host → **卸载**,再运行 `npx -y dsh-baton-mcp setup --remove`。插件分不清"被卸载"和"Harness 退出",所以 MCP 注册交给这条命令删,它也会删掉 `~/.dsh-baton/app`;运行记录留在 `~/.dsh-baton`,不需要的话自己删掉。
|
|
290
290
|
|
package/host/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-baton-host",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.9",
|
|
4
4
|
"description": "DeepSeek Harness plugin for dsh-baton: hand coding tasks from Claude Code or Codex to DeepSeek Harness on your machine. Adding it is the whole install: it registers the dsh-baton MCP server for Claude Code and Codex, runs each task live in the Harness window, wakes the main agent when the run ends and hands back the changed files, the project's own test results and a requirement-by-requirement checklist.",
|
|
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.6.
|
|
23
|
+
"dsh-baton-mcp": "0.6.9"
|
|
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.6.
|
|
3
|
+
"version": "0.6.9",
|
|
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.6.
|
|
10
|
+
"version": "0.6.9",
|
|
11
11
|
"packages": [
|
|
12
12
|
{
|
|
13
13
|
"registryType": "npm",
|
|
14
14
|
"identifier": "dsh-baton-mcp",
|
|
15
|
-
"version": "0.6.
|
|
15
|
+
"version": "0.6.9",
|
|
16
16
|
"transport": {
|
|
17
17
|
"type": "stdio"
|
|
18
18
|
}
|
package/src/cli.mjs
CHANGED
|
@@ -91,6 +91,7 @@ async function main() {
|
|
|
91
91
|
const total_s = Number(flags.timeout) || 24 * 3600
|
|
92
92
|
const waitStarted = Date.now()
|
|
93
93
|
const st = await core.api.wait({ run_id: rest[0], timeout_s: total_s })
|
|
94
|
+
const stop = core.stopHint(st)
|
|
94
95
|
// A Harness goal goes on after the run's own round: so does this wait, until the goal
|
|
95
96
|
// stops (or the next report, with --until turn), with every round since the run began.
|
|
96
97
|
if (core.goalGoesOn(st)) {
|
|
@@ -113,6 +114,10 @@ async function main() {
|
|
|
113
114
|
// The hint points at evidence: it goes with it (files changed, checks run, other runs in the folder).
|
|
114
115
|
...(st.evidence ? { evidence: st.evidence } : {}),
|
|
115
116
|
...(st.state === 'completed' && !st.decided_for_user ? { hint: core.endedHint(st) } : {}),
|
|
117
|
+
// Stopped by the user, cancelled, or cut off by a Harness restart: say so, or the client
|
|
118
|
+
// takes it for a failure in the work and sends it again.
|
|
119
|
+
...(st.harness_down ? { harness_down: true, hint: core.HARNESS_DOWN_HINT.replace('call dsh_wait again with the same run_id', 'run this same wait command again') } : {}),
|
|
120
|
+
...(stop ? { ...(st.turn_end ? { turn_end: st.turn_end } : {}), ...stop } : {}),
|
|
116
121
|
...(st.needs_input ? { needs_input: st.needs_input, question: st.question, ...(st.question ? { relay: core.relayText(st.question) } : {}), hint: st.question ? 'The run asks the user something. If the user allowed you to decide, choose by your own judgment and answer with dsh_answer (or dsh-baton answer), passing reason, and tell the user what you chose and why. Otherwise show the user relay, written in the user\'s language, with its first line filled in with your own pick and a reason from your own judgment of the question and the task (harness_suggested is only the asker\'s pick, never a reason by itself), and ask the user first. Then wait again.' : 'The run waits for a person in the Harness UI. Tell the user, then wait again.' } : {}) })
|
|
117
122
|
process.exitCode = st.state === 'completed' ? 0 : st.needs_input ? 3 : 1
|
|
118
123
|
return
|
package/src/core.mjs
CHANGED
|
@@ -147,7 +147,8 @@ function hostCallAt(endpoint, method, params = {}, { timeoutMs = 15000 } = {}) {
|
|
|
147
147
|
msg.error ? finish(new Error(String(msg.error))) : finish(null, msg.result)
|
|
148
148
|
})
|
|
149
149
|
conn.on('error', (e) => finish(Object.assign(e, { hostDown: true })))
|
|
150
|
-
|
|
150
|
+
// Harness quitting closes the connection mid-call: marked so a wait can wait for it to come back.
|
|
151
|
+
conn.on('close', () => finish(Object.assign(new Error(`dsh-baton-host closed the connection before answering ${method}`), { hostClosed: true })))
|
|
151
152
|
})
|
|
152
153
|
}
|
|
153
154
|
|
|
@@ -623,12 +624,33 @@ const NEEDS_INPUT_HINT = (st) => st.question
|
|
|
623
624
|
? `The run stopped to ask the user ${st.question.count} question(s) (in question). Unless the user let you decide, show the user relay, written in the user's language, with its first line filled in with your own pick and reason (not the asker's pick and not its option description repeated), then ask which they want. ${ASKED} After answering, wait for the run again.`
|
|
624
625
|
: 'The run waits for something only a person can do in the Harness UI (an approval, for example). Tell the user, then wait for the run again.'
|
|
625
626
|
|
|
627
|
+
// A run the user stopped in the Harness UI: its turn ended aborted, by the user. With the usual
|
|
628
|
+
// hint, Codex took that for a failure and sent the task again each time the user stopped it
|
|
629
|
+
// (Windows, 2026-11-08). The UI stop leaves background jobs of the session running; dsh_cancel
|
|
630
|
+
// stops them too.
|
|
631
|
+
export const stoppedByUser = (st) => st?.state === 'failed' && st.turn_end?.kind === 'aborted' && st.turn_end?.reason?.kind === 'user'
|
|
632
|
+
export const USER_STOPPED_HINT = 'The user stopped this run in Harness (stopped_by_user). It is not a failure to repair: do not send it again, continue it or send fixes. Tell the user it was stopped and what evidence shows it got done, then ask what to do next. Background jobs it started may still be running in that session; stop them with dsh_cancel and its session_id only if the user asks.'
|
|
633
|
+
// Ended by something other than the work: Harness restarting under the run, or a cancel (dsh_cancel,
|
|
634
|
+
// or in Harness: the user stopping the session drops its queued messages, or removing one). Sending
|
|
635
|
+
// again on one's own would override the user here too.
|
|
636
|
+
export const INTERRUPTED_HINT = 'Harness restarted or quit while this run was going, and its turn was stopped (interrupted). It is not a failure in the work: do not send it again or continue it on your own. Tell the user Harness was restarted and what evidence shows the run got done, then ask whether to run it again or continue in the same session.'
|
|
637
|
+
export const CANCELLED_HINT = 'This run was cancelled before it finished: with dsh_cancel, or in Harness, where the user stopped the session or removed the queued message. If you did not cancel it yourself, the user did: do not send it again. Tell the user it was cancelled and ask what to do next.'
|
|
638
|
+
// The hint for a run that did not end by its own work, or null for one that did.
|
|
639
|
+
export function stopHint(st) {
|
|
640
|
+
if (stoppedByUser(st)) return { stopped_by_user: true, hint: USER_STOPPED_HINT }
|
|
641
|
+
if (st?.state === 'interrupted') return { hint: INTERRUPTED_HINT }
|
|
642
|
+
if (st?.state === 'cancelled') return { hint: CANCELLED_HINT }
|
|
643
|
+
return null
|
|
644
|
+
}
|
|
645
|
+
|
|
626
646
|
// The result of a run that ended, kept short: the client already knows what it sent, and a
|
|
627
647
|
// completed run's event log is only the work behind final_text (dsh_status still has it).
|
|
628
648
|
// A run that did not complete keeps its last events, to show where it stopped.
|
|
629
649
|
function ended({ recent_events, message_preview, backend, ...st }) {
|
|
630
650
|
const out = { ...st, ...(st.state !== 'completed' && recent_events?.length ? { recent_events: recent_events.slice(-5) } : {}) }
|
|
631
651
|
if (outOfBalance(st)) return { ...out, hint: QUOTA_HINT }
|
|
652
|
+
const stop = stopHint(st)
|
|
653
|
+
if (stop) return { ...out, ...stop }
|
|
632
654
|
// decided_for_user first, so a client that skims to final_text still meets it.
|
|
633
655
|
return st.decided_for_user
|
|
634
656
|
? { hint: `${DECIDED_HINT} ${endedHint(st)}`, decided_for_user: st.decided_for_user, ...out }
|
|
@@ -757,6 +779,10 @@ export async function followThrough(params, { total_s, chunk_s = 300, onChunk, s
|
|
|
757
779
|
}
|
|
758
780
|
}
|
|
759
781
|
|
|
782
|
+
// How often a wait looks for Harness again while it is not running.
|
|
783
|
+
const HOST_RETRY_MS = Number(process.env.DSH_BATON_HOST_RETRY_MS) || 2000
|
|
784
|
+
export const HARNESS_DOWN_HINT = 'DeepSeek Harness is not running (closed, or restarting), so the run cannot be followed right now; Harness ends a running turn when it quits. Do not send the task again. Tell the user Harness is closed and to open it; then call dsh_wait again with the same run_id: it reports how the run ended (interrupted, if Harness quit while it ran).'
|
|
785
|
+
|
|
760
786
|
export const api = {
|
|
761
787
|
async workspaces() {
|
|
762
788
|
if (!(await useHost())) throw new Error('Listing workspaces needs the dsh-baton-host plugin running inside DeepSeek Harness.')
|
|
@@ -828,8 +854,20 @@ export const api = {
|
|
|
828
854
|
return changed ? { ...st, changed_files: changed } : st
|
|
829
855
|
},
|
|
830
856
|
async wait({ run_id, timeout_s = 300 }) {
|
|
831
|
-
if (isHostRun(run_id)) return withEvidence(
|
|
832
|
-
|
|
857
|
+
if (!isHostRun(run_id)) return withEvidence(tag('headless', await waitRun(run_id, { timeout_s })))
|
|
858
|
+
// Harness quit or restarting: wait for it to come back within the same call (the plugin then
|
|
859
|
+
// reports the run as interrupted) instead of failing, which had Codex retry every few seconds.
|
|
860
|
+
const deadline = Date.now() + timeout_s * 1000
|
|
861
|
+
for (;;) {
|
|
862
|
+
const left = Math.max(1, Math.ceil((deadline - Date.now()) / 1000))
|
|
863
|
+
try {
|
|
864
|
+
return withEvidence(shaped(tag('host', await hostCall('wait', { run_id, timeout_s: left }, { timeoutMs: (left + 15) * 1000 }))))
|
|
865
|
+
} catch (error) {
|
|
866
|
+
if (!(error?.hostDown || error?.hostClosed)) throw error
|
|
867
|
+
if (Date.now() + HOST_RETRY_MS >= deadline) return { run_id, state: 'unknown', timed_out: true, harness_down: true, backend: 'host' }
|
|
868
|
+
await new Promise((r) => setTimeout(r, HOST_RETRY_MS))
|
|
869
|
+
}
|
|
870
|
+
}
|
|
833
871
|
},
|
|
834
872
|
async cancel({ run_id, session_id }) {
|
|
835
873
|
if (run_id && !isHostRun(run_id)) return tag('headless', cancelRun(run_id))
|
package/src/mcp.mjs
CHANGED
|
@@ -122,6 +122,7 @@ async function waitInChunks({ run_id }, progress, signal, meta) {
|
|
|
122
122
|
// Never spin: a chunk that came back early still takes a moment before the next one.
|
|
123
123
|
if (Date.now() - t0 < 500) await new Promise((r) => setTimeout(r, 500))
|
|
124
124
|
if (Date.now() >= deadline) {
|
|
125
|
+
if (st.harness_down) return { run_id, state: st.state, timed_out: true, harness_down: true, hint: core.HARNESS_DOWN_HINT }
|
|
125
126
|
return {
|
|
126
127
|
run_id, state: st.state, timed_out: true,
|
|
127
128
|
...(st.waiting_on ? { waiting_on: st.waiting_on } : {}),
|
|
@@ -182,6 +183,7 @@ async function waitAnyInChunks(run_ids, progress, signal, meta) {
|
|
|
182
183
|
if (Date.now() - t0 < 500) await new Promise((r) => setTimeout(r, 500))
|
|
183
184
|
if (Date.now() >= deadline) {
|
|
184
185
|
const asking = running.filter((r) => r.state === 'waiting').map((r) => r.run_id)
|
|
186
|
+
if ([...answers.values()].some((st) => st.harness_down)) return { run_ids, timed_out: true, harness_down: true, still_running: running, hint: `${core.HARNESS_DOWN_HINT.replace('the same run_id', 'the same run_ids')}` }
|
|
185
187
|
return {
|
|
186
188
|
run_ids, timed_out: true, still_running: running,
|
|
187
189
|
hint: `None of these runs has ended yet. Call dsh_wait again with the same run_ids, with no progress check or progress note in between (work the user asked you to do meanwhile is fine).${asking.length ? ` ${asking.join(', ')} still wait${asking.length === 1 ? 's' : ''} for a person (see dsh_status for the question): answer with dsh_answer if you may, or tell the user.` : ''}`,
|
|
@@ -200,12 +202,15 @@ function anyResult({ done, running }) {
|
|
|
200
202
|
: res
|
|
201
203
|
})
|
|
202
204
|
const ids = running.map((r) => r.run_id)
|
|
205
|
+
// Stopped by the user, cancelled or cut off by a Harness restart: not work to send back.
|
|
206
|
+
const stopped = ended.filter((r) => r.stopped_by_user || r.state === 'interrupted' || r.state === 'cancelled').map((r) => r.run_id)
|
|
207
|
+
const leave = stopped.length ? ` ${stopped.join(', ')} did not end by ${stopped.length === 1 ? 'its' : 'their'} own work (stopped by the user, cancelled or cut off by a Harness restart, see ${stopped.length === 1 ? 'its' : 'each'} hint): send ${stopped.length === 1 ? 'it' : 'them'} nothing more on your own and leave ${stopped.length === 1 ? 'it' : 'them'} out of the next wait.` : ''
|
|
203
208
|
const next = ids.length
|
|
204
209
|
? `Then call dsh_wait again with run_ids: the ones still running (${ids.join(', ')}) plus the run_id of every fix you sent, so whichever ends next comes back first. Do not wait for one run on its own while others are going.`
|
|
205
210
|
: 'None of these runs is still going.'
|
|
206
211
|
return {
|
|
207
212
|
ended, still_running: running,
|
|
208
|
-
hint: `${ended.length} of the runs ${ended.length === 1 ? 'has' : 'have'} ended or stopped to ask (ended; each has its own hint). Deal with ${ended.length === 1 ? 'it' : 'them'} now: review against what you asked, and send any fixes with dsh_send to the same session_id right away, or answer a question
|
|
213
|
+
hint: `${ended.length} of the runs ${ended.length === 1 ? 'has' : 'have'} ended or stopped to ask (ended; each has its own hint). Deal with ${ended.length === 1 ? 'it' : 'them'} now: review against what you asked, and send any fixes with dsh_send to the same session_id right away, or answer a question.${leave} ${next}`,
|
|
209
214
|
}
|
|
210
215
|
}
|
|
211
216
|
|