dsh-baton-host 0.4.0 → 0.5.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/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 / 添加插件 → registry Official npm registry → `dsh-baton-host` → Install / 安装 → Enable now / 立即启用
9
+ Harness → Plugins / 插件 → Add plugin / 添加插件 → `dsh-baton-host` → Install / 安装 → Enable now / 立即启用
10
10
 
11
- Update / 更新: uninstall, then add it again / 先卸载再重新添加. Keep it on the same version as `dsh-baton-mcp` / 版本与 `dsh-baton-mcp` 保持一致.
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
- Then connect the MCP server / 然后接入 MCP 服务器: see [dsh-baton](https://github.com/JialaoLiu/dsh-baton#readme).
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: labels chat messages sent through dsh-baton with
2
- // their sender ("Sent by Claude from another chat"). The host half puts the sender
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 zh = { sentFrom: '由 {name} 从另一个聊天发送' }
29
- const en = { sentFrom: 'Sent by {name} from another chat' }
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 { socketEndpoint, batonHome, socketPath as socketPathSetting } from './socket.js'
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.0",
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.",
3
+ "version": "0.5.1",
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.1"
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
  },