dsh-baton-host 0.4.0 → 0.5.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 +10 -4
- package/lib/client.js +130 -5
- package/lib/index.js +57 -1
- package/package.json +7 -3
package/README.md
CHANGED
|
@@ -6,11 +6,15 @@ DeepSeek Harness 插件,配合 [dsh-baton](https://github.com/JialaoLiu/dsh-ba
|
|
|
6
6
|
|
|
7
7
|
## Install / 安装
|
|
8
8
|
|
|
9
|
-
Harness → Plugins / 插件 → Add plugin / 添加插件 →
|
|
9
|
+
Harness → Plugins / 插件 → Add plugin / 添加插件 → `dsh-baton-host` → Install / 安装 → Enable now / 立即启用
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
That is the whole install: switched on, the plugin registers the dsh-baton MCP server (the `dsh-baton-mcp` package, installed with it) for Claude Code and Codex, user-wide, and Harness says so. New conversations then have the `dsh_*` tools. If it could not register, run `npx -y dsh-baton-mcp setup`. Details: [dsh-baton](https://github.com/JialaoLiu/dsh-baton#install-details).
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
装好就完成了:插件启用时会为 Claude Code 和 Codex 注册 dsh-baton MCP 服务器(`dsh-baton-mcp` 包,随插件一起安装),Harness 里会提示。新开的对话就有 `dsh_*` 工具。没能注册的话,运行 `npx -y dsh-baton-mcp setup`。详见 [dsh-baton](https://github.com/JialaoLiu/dsh-baton/blob/main/README.zh-CN.md#安装细节)。
|
|
14
|
+
|
|
15
|
+
Update / 更新: uninstall, then add it again; the MCP server is updated with it / 先卸载再重新添加,MCP 服务器会一起更新.
|
|
16
|
+
|
|
17
|
+
Remove / 卸载: uninstall, then `npx -y dsh-baton-mcp setup --remove` / 卸载插件后运行 `npx -y dsh-baton-mcp setup --remove`.
|
|
14
18
|
|
|
15
19
|
## Socket protocol / 套接字协议
|
|
16
20
|
|
|
@@ -32,7 +36,9 @@ Newline-delimited JSON: request `{"id", "method", "params"}`, reply `{"id", "res
|
|
|
32
36
|
| `rename_workspace` | `workspace_id` or `path`, `title` |
|
|
33
37
|
| `list_runs` | `limit?` |
|
|
34
38
|
|
|
35
|
-
`DSH_BATON_SOCKET` / `DSH_BATON_HOME` in the Harness process environment move the socket.
|
|
39
|
+
`DSH_BATON_SOCKET` / `DSH_BATON_HOME` in the Harness process environment move the socket. `DSH_BATON_AUTO_REGISTER=0` turns off the MCP registration.
|
|
40
|
+
|
|
41
|
+
The Plugins page reads the registration outcome from `GET /dsh-baton/status` on the Harness web server.
|
|
36
42
|
|
|
37
43
|
## License
|
|
38
44
|
|
package/lib/client.js
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
// dsh-baton-host, browser half
|
|
2
|
-
//
|
|
1
|
+
// dsh-baton-host, browser half. Two jobs:
|
|
2
|
+
// - On the Plugins page: right after the plugin is switched on, and on its detail page,
|
|
3
|
+
// say whether the host half registered the MCP server for Claude Code and Codex
|
|
4
|
+
// (it serves the outcome at /dsh-baton/status).
|
|
5
|
+
// - In the chat: label messages sent through dsh-baton with their sender ("Sent by
|
|
6
|
+
// Claude from another chat"). The host half puts the sender
|
|
3
7
|
// into the prompt's request id ("dsh-baton.<sender>.<uuid>"); the chat records it as
|
|
4
8
|
// the user message's source.rpcId, so the label survives reloads with no extra state.
|
|
5
9
|
//
|
|
@@ -14,7 +18,10 @@ window.__ModuleLoader__.load({
|
|
|
14
18
|
id: 'dsh-baton-host',
|
|
15
19
|
factory: (require) => {
|
|
16
20
|
const module = { exports: {} }
|
|
17
|
-
const { createElement: h } = require('react')
|
|
21
|
+
const { createElement: h, useState, useEffect } = require('react')
|
|
22
|
+
// Harness's dialog and button; without them (an older or newer Harness), plain elements.
|
|
23
|
+
let ui = null
|
|
24
|
+
try { ui = require('@deepseek-ai/dsh-client-ui-primitives') } catch { /* fall back */ }
|
|
18
25
|
|
|
19
26
|
const SLOT = 'conversation.chat.node'
|
|
20
27
|
const KEYS = ['user', 'steering']
|
|
@@ -25,8 +32,124 @@ window.__ModuleLoader__.load({
|
|
|
25
32
|
// Codex shows as ChatGPT, the way the ChatGPT app labels its own cross-chat messages.
|
|
26
33
|
const NAMES = { claude: 'Claude', codex: 'ChatGPT' }
|
|
27
34
|
|
|
28
|
-
const
|
|
29
|
-
const
|
|
35
|
+
const PACKAGE = 'dsh-baton-host'
|
|
36
|
+
const SETUP = 'npx -y dsh-baton-mcp setup'
|
|
37
|
+
|
|
38
|
+
const zh = {
|
|
39
|
+
sentFrom: '由 {name} 从另一个聊天发送',
|
|
40
|
+
readyTitle: 'dsh-baton 已就绪',
|
|
41
|
+
failTitle: 'dsh-baton 还差一步',
|
|
42
|
+
pending: '正在为 Claude Code 和 Codex 注册 dsh-baton…',
|
|
43
|
+
ready: '已为 {clients} 注册 dsh-baton。在 Claude Code 或 Codex 里新开一个对话,就能用 dsh_* 工具。',
|
|
44
|
+
partial: '{clients} 没能注册:',
|
|
45
|
+
noNode: '没找到 Node.js 22.15 或更新版本。装好 Node.js 后,',
|
|
46
|
+
noClients: '没找到 Claude Code 或 Codex。装好之后,',
|
|
47
|
+
failed: '没能自动注册 MCP。',
|
|
48
|
+
runSetup: '请在终端运行 {cmd}。',
|
|
49
|
+
unknown: '如果新对话里没有 dsh_* 工具,请在终端运行 {cmd}。',
|
|
50
|
+
off: '已关闭自动注册(DSH_BATON_AUTO_REGISTER=0)。',
|
|
51
|
+
ok: '知道了',
|
|
52
|
+
status: 'MCP 注册',
|
|
53
|
+
registered: '已注册',
|
|
54
|
+
notInstalled: '未安装',
|
|
55
|
+
notRegistered: '未注册',
|
|
56
|
+
and: '和',
|
|
57
|
+
}
|
|
58
|
+
const en = {
|
|
59
|
+
sentFrom: 'Sent by {name} from another chat',
|
|
60
|
+
readyTitle: 'dsh-baton is ready',
|
|
61
|
+
failTitle: 'dsh-baton needs one more step',
|
|
62
|
+
pending: 'Registering dsh-baton for Claude Code and Codex…',
|
|
63
|
+
ready: 'dsh-baton is registered for {clients}. Start a new conversation in Claude Code or Codex to use the dsh_* tools.',
|
|
64
|
+
partial: 'Could not register for {clients}: ',
|
|
65
|
+
noNode: 'Node.js 22.15 or later was not found. Once it is installed, ',
|
|
66
|
+
noClients: 'Neither Claude Code nor Codex was found. Once one is installed, ',
|
|
67
|
+
failed: 'The MCP server could not be registered automatically. ',
|
|
68
|
+
runSetup: 'run {cmd} in a terminal.',
|
|
69
|
+
unknown: 'If new conversations have no dsh_* tools, run {cmd} in a terminal.',
|
|
70
|
+
off: 'Automatic registration is off (DSH_BATON_AUTO_REGISTER=0).',
|
|
71
|
+
ok: 'OK',
|
|
72
|
+
status: 'MCP registration',
|
|
73
|
+
registered: 'registered',
|
|
74
|
+
notInstalled: 'not installed',
|
|
75
|
+
notRegistered: 'not registered',
|
|
76
|
+
and: 'and',
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// --- Plugins page ------------------------------------------------------------
|
|
80
|
+
|
|
81
|
+
const statusUrl = () => new URL('dsh-baton/status', document.baseURI).pathname
|
|
82
|
+
const REGISTERED = ['registered', 'unchanged', 'kept']
|
|
83
|
+
|
|
84
|
+
// The host half registers right after it starts, so poll until it says it is done.
|
|
85
|
+
function useRegistration() {
|
|
86
|
+
const [reg, setReg] = useState(null)
|
|
87
|
+
useEffect(() => {
|
|
88
|
+
let stopped = false
|
|
89
|
+
let tries = 0
|
|
90
|
+
const poll = async () => {
|
|
91
|
+
let next = null
|
|
92
|
+
try {
|
|
93
|
+
const res = await fetch(statusUrl(), { cache: 'no-store' })
|
|
94
|
+
if (res.ok) next = (await res.json()).registration
|
|
95
|
+
} catch { /* not served yet */ }
|
|
96
|
+
if (stopped) return
|
|
97
|
+
if (next && next.state !== 'pending') return setReg(next)
|
|
98
|
+
if (++tries >= 30) return setReg({ state: 'unknown' })
|
|
99
|
+
setTimeout(poll, 700)
|
|
100
|
+
}
|
|
101
|
+
poll()
|
|
102
|
+
return () => { stopped = true }
|
|
103
|
+
}, [])
|
|
104
|
+
return reg
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// What to tell the user: [title or null, text]. null while the host is still working.
|
|
108
|
+
function describe(reg, t) {
|
|
109
|
+
if (!reg || reg.state === 'pending') return [null, t('pending')]
|
|
110
|
+
if (reg.state === 'off') return [t('readyTitle'), t('off')]
|
|
111
|
+
if (reg.state === 'unknown') return [t('readyTitle'), t('unknown', { cmd: SETUP })]
|
|
112
|
+
const clients = reg.clients ?? []
|
|
113
|
+
const done = clients.filter((c) => REGISTERED.includes(c.state)).map((c) => c.label)
|
|
114
|
+
const failed = clients.filter((c) => c.state === 'failed').map((c) => c.label)
|
|
115
|
+
const list = (names) => names.join(` ${t('and')} `)
|
|
116
|
+
const fix = (reg.error === 'no-node' ? t('noNode') : reg.error === 'no-clients' ? t('noClients') : '') + t('runSetup', { cmd: SETUP })
|
|
117
|
+
if (reg.ok) return [t('readyTitle'), t('ready', { clients: list(done) })]
|
|
118
|
+
if (done.length) return [t('failTitle'), `${t('ready', { clients: list(done) })} ${t('partial', { clients: list(failed) })}${fix}`]
|
|
119
|
+
return [t('failTitle'), (reg.error === 'no-node' || reg.error === 'no-clients' ? '' : t('failed')) + fix]
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Shown once, right after "Enable now".
|
|
123
|
+
function wrapActivation(t) {
|
|
124
|
+
return function DshBatonActivation({ onDismiss }) {
|
|
125
|
+
const [title, text] = describe(useRegistration(), t)
|
|
126
|
+
if (ui?.Modal && ui?.Button) {
|
|
127
|
+
return h(ui.Modal, {
|
|
128
|
+
open: true, title: title ?? t('readyTitle'), closeLabel: t('ok'), onClose: onDismiss,
|
|
129
|
+
footer: h(ui.Button, { variant: 'primary', 'data-modal-autofocus': true, onClick: onDismiss }, t('ok')),
|
|
130
|
+
}, h('p', null, text))
|
|
131
|
+
}
|
|
132
|
+
return h('div', { role: 'status', style: { margin: '12px 0', padding: 12, borderRadius: 8, border: '1px solid var(--dsw-alias-border-l3)', color: 'var(--dsw-alias-label-primary)' } },
|
|
133
|
+
title ? h('strong', null, title) : null,
|
|
134
|
+
h('p', { style: { margin: '6px 0' } }, text),
|
|
135
|
+
h('button', { type: 'button', onClick: onDismiss }, t('ok')))
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// The plugin's detail page: one line per client.
|
|
140
|
+
function wrapDetails(t) {
|
|
141
|
+
return function DshBatonStatus({ view }) {
|
|
142
|
+
const reg = useRegistration()
|
|
143
|
+
if (view === 'summary') return null
|
|
144
|
+
const [, text] = describe(reg, t)
|
|
145
|
+
const word = (s) => (REGISTERED.includes(s) ? t('registered') : s === 'not-installed' ? t('notInstalled') : t('notRegistered'))
|
|
146
|
+
const rows = (reg?.clients ?? []).map((c) => `${c.label}: ${word(c.state)}`)
|
|
147
|
+
return h('div', { style: { display: 'flex', flexDirection: 'column', gap: 4, color: 'var(--dsw-alias-label-secondary)' } },
|
|
148
|
+
h('strong', { style: { color: 'var(--dsw-alias-label-primary)' } }, t('status')),
|
|
149
|
+
rows.length ? h('span', null, rows.join(' · ')) : null,
|
|
150
|
+
h('span', null, text))
|
|
151
|
+
}
|
|
152
|
+
}
|
|
30
153
|
|
|
31
154
|
function senderOf(node) {
|
|
32
155
|
const rpcId = node?.data?.source?.rpcId
|
|
@@ -92,6 +215,8 @@ window.__ModuleLoader__.load({
|
|
|
92
215
|
// locale 'chat': the shipped renderer reads its strings through the `t` it is handed.
|
|
93
216
|
for (const key of KEYS) yield ctx.slots.register({ name: SLOT, key, priority: PRIORITY, locale: 'chat' }, wrapperFor(ctx, key, t))
|
|
94
217
|
})
|
|
218
|
+
ctx.slots.inject('plugins.bundle.activation', () => ctx.slots.register({ name: 'plugins.bundle.activation', key: PACKAGE, locale: NS }, wrapActivation(t)))
|
|
219
|
+
ctx.slots.inject('plugins.bundle.config', () => ctx.slots.register({ name: 'plugins.bundle.config', key: PACKAGE, locale: NS }, wrapDetails(t)))
|
|
95
220
|
}
|
|
96
221
|
return module.exports
|
|
97
222
|
},
|
package/lib/index.js
CHANGED
|
@@ -10,7 +10,9 @@ import net from 'node:net'
|
|
|
10
10
|
import os from 'node:os'
|
|
11
11
|
import path from 'node:path'
|
|
12
12
|
import crypto from 'node:crypto'
|
|
13
|
-
import {
|
|
13
|
+
import { createRequire } from 'node:module'
|
|
14
|
+
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
15
|
+
import { socketEndpoint, batonHome, setting, socketPath as socketPathSetting } from './socket.js'
|
|
14
16
|
|
|
15
17
|
export const name = 'dsh-baton-host'
|
|
16
18
|
export const inject = ['sessionController', 'workspaceRegistry']
|
|
@@ -85,6 +87,26 @@ function toolCallsOf(content) {
|
|
|
85
87
|
|
|
86
88
|
const iso = (t) => (typeof t === 'number' ? new Date(t).toISOString() : t ?? null)
|
|
87
89
|
|
|
90
|
+
// The MCP server package this plugin registers: the checkout or ~/.dsh-baton/app folder
|
|
91
|
+
// this host folder sits in, else the dsh-baton-mcp package installed with the plugin.
|
|
92
|
+
function serverRoot() {
|
|
93
|
+
const up = fileURLToPath(new URL('../../', import.meta.url))
|
|
94
|
+
if (fs.existsSync(path.join(up, 'src', 'register.mjs'))) return path.resolve(up)
|
|
95
|
+
try { return path.dirname(createRequire(import.meta.url).resolve('dsh-baton-mcp/package.json')) } catch { return null }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// The Harness install this process runs from, so its bundled Node (whose path changes
|
|
99
|
+
// with every Harness update) is never registered.
|
|
100
|
+
function harnessDir() {
|
|
101
|
+
const exe = process.execPath
|
|
102
|
+
const app = exe.match(/^(.*?\.app)(?=[\\/])/)
|
|
103
|
+
if (app) return app[1]
|
|
104
|
+
// Windows: the install folder, e.g. %LOCALAPPDATA%\Programs\DeepSeek Harness.
|
|
105
|
+
const i = exe.toLowerCase().indexOf('deepseek harness')
|
|
106
|
+
const end = i < 0 ? -1 : exe.slice(i).search(/[\\/]/)
|
|
107
|
+
return end > 0 ? exe.slice(0, i + end) : null
|
|
108
|
+
}
|
|
109
|
+
|
|
88
110
|
export function apply(ctx) {
|
|
89
111
|
// This code runs inside the Harness host process: an uncaught error here can
|
|
90
112
|
// take Harness down. Every event listener and timer goes through guard().
|
|
@@ -630,6 +652,40 @@ export function apply(ctx) {
|
|
|
630
652
|
}
|
|
631
653
|
|
|
632
654
|
|
|
655
|
+
// --- MCP registration -----------------------------------------------------
|
|
656
|
+
// Switching the plugin on registers the dsh-baton MCP server for Claude Code and Codex,
|
|
657
|
+
// the same entries `dsh-baton setup` writes, so adding the plugin is the whole install.
|
|
658
|
+
// It runs again on every Harness start: a plugin update brings the server along, and an
|
|
659
|
+
// entry that is already right is left untouched. The Plugins page reads the outcome from
|
|
660
|
+
// GET /dsh-baton/status (lib/client.js). DSH_BATON_AUTO_REGISTER=0 turns it off.
|
|
661
|
+
let registration = { state: 'pending' }
|
|
662
|
+
if (setting('AUTO_REGISTER') !== '0') {
|
|
663
|
+
setImmediate(guard('register', async () => {
|
|
664
|
+
const root = serverRoot()
|
|
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() })
|
|
668
|
+
registration = { state: 'done', ...result }
|
|
669
|
+
const summary = result.clients.map((c) => `${c.label}: ${c.state}${c.error ? ` (${c.error})` : ''}`).join(', ')
|
|
670
|
+
ctx.logger?.[result.ok ? 'info' : 'error']?.(`dsh-baton-host MCP registration: ${summary || result.error}`)
|
|
671
|
+
}))
|
|
672
|
+
} else {
|
|
673
|
+
registration = { state: 'off' }
|
|
674
|
+
}
|
|
675
|
+
if (typeof ctx.inject === 'function') {
|
|
676
|
+
ctx.inject(['webServer'], (web) => {
|
|
677
|
+
web.effect(() => web.webServer.register({
|
|
678
|
+
kind: 'exact',
|
|
679
|
+
path: '/dsh-baton/status',
|
|
680
|
+
handler: (request, response) => {
|
|
681
|
+
const { state, ok, error, version, clients = [] } = registration
|
|
682
|
+
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 })) } }))
|
|
684
|
+
},
|
|
685
|
+
}), 'dsh-baton-host status route')
|
|
686
|
+
})
|
|
687
|
+
}
|
|
688
|
+
|
|
633
689
|
ctx.effect(() => {
|
|
634
690
|
const sock = socketPath()
|
|
635
691
|
let server = null
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-baton-host",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "DeepSeek Harness plugin for dsh-baton:
|
|
3
|
+
"version": "0.5.0",
|
|
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",
|
|
7
7
|
"exports": {
|
|
@@ -19,6 +19,9 @@
|
|
|
19
19
|
"README.md",
|
|
20
20
|
"LICENSE"
|
|
21
21
|
],
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"dsh-baton-mcp": "0.5.0"
|
|
24
|
+
},
|
|
22
25
|
"license": "MIT",
|
|
23
26
|
"dsh": {
|
|
24
27
|
"manifestVersion": 1,
|
|
@@ -33,7 +36,8 @@
|
|
|
33
36
|
"inject": [
|
|
34
37
|
"@deepseek-ai/dsh-client-locale",
|
|
35
38
|
"@deepseek-ai/dsh-client-ui-renderer",
|
|
36
|
-
"@deepseek-ai/dsh-client-ui-chat"
|
|
39
|
+
"@deepseek-ai/dsh-client-ui-chat",
|
|
40
|
+
"@deepseek-ai/dsh-client-ui-primitives"
|
|
37
41
|
]
|
|
38
42
|
}
|
|
39
43
|
},
|