dsh-baton-host 0.6.2 → 0.6.4-next.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 CHANGED
@@ -1,5 +1,5 @@
1
1
  <p align="center">
2
- <img src="https://raw.githubusercontent.com/JialaoLiu/dsh-baton/main/assets/readme/hero.gif" width="100%" alt="dsh-baton: pass the baton from Claude Code or Codex to DeepSeek Harness, watch it run live, and get it handed back when it's done">
2
+ <img src="https://raw.githubusercontent.com/JialaoLiu/dsh-baton/main/assets/readme/en/hero.gif" width="100%" alt="dsh-baton: pass the baton from Claude Code or Codex to DeepSeek Harness, watch it run live, and get it handed back when it's done">
3
3
  </p>
4
4
 
5
5
  <p align="center">
@@ -17,13 +17,13 @@ The DeepSeek Harness plugin for [dsh-baton](https://github.com/JialaoLiu/dsh-bat
17
17
  DeepSeek Harness 插件,配合 [dsh-baton](https://github.com/JialaoLiu/dsh-baton/blob/main/README.zh-CN.md) 使用。它运行在 Harness 内部,只开一个当前用户才能访问的本地套接字,让 MCP 客户端能新建工作区和对话、续接指定对话、等待任务完成。所有操作都走 Harness 官方会话服务,界面实时可见。
18
18
 
19
19
  <p align="center">
20
- <img src="https://raw.githubusercontent.com/JialaoLiu/dsh-baton/main/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">
20
+ <img src="https://raw.githubusercontent.com/JialaoLiu/dsh-baton/main/assets/readme/en/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">
21
21
  </p>
22
22
 
23
23
  ## Install / 安装
24
24
 
25
25
  <p align="center">
26
- <img src="https://raw.githubusercontent.com/JialaoLiu/dsh-baton/main/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">
26
+ <img src="https://raw.githubusercontent.com/JialaoLiu/dsh-baton/main/assets/readme/en/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">
27
27
  </p>
28
28
 
29
29
  Harness → Plugins / 插件 → Add plugin / 添加插件 → `dsh-baton-host` → Install / 安装 → Enable now / 立即启用
@@ -39,7 +39,7 @@ Remove / 卸载: uninstall, then `npx -y dsh-baton-mcp setup --remove` / 卸载
39
39
  ## How it works / 工作原理
40
40
 
41
41
  <p align="center">
42
- <img src="https://raw.githubusercontent.com/JialaoLiu/dsh-baton/main/assets/readme/architecture.svg" width="100%" alt="MCP clients call dsh-baton-mcp over stdio; it reaches the dsh-baton-host plugin inside Harness through an owner-only local socket; the plugin drives Harness sessions live, with dsh headless as fallback">
42
+ <img src="https://raw.githubusercontent.com/JialaoLiu/dsh-baton/main/assets/readme/en/architecture.svg" width="100%" alt="MCP clients call dsh-baton-mcp over stdio; it reaches the dsh-baton-host plugin inside Harness through an owner-only local socket; the plugin drives Harness sessions live, with dsh headless as fallback">
43
43
  </p>
44
44
 
45
45
  ## Socket protocol / 套接字协议
package/lib/client.js CHANGED
@@ -54,8 +54,6 @@ window.__ModuleLoader__.load({
54
54
  notInstalled: '未安装',
55
55
  notRegistered: '未注册',
56
56
  and: '和',
57
- hookTitle: 'Codex 还差一步',
58
- hookText: '打开 Codex → Settings → Hooks,信任 Stop 钩子“dsh-baton: waiting for the Harness run”。信任后,Codex 等 DeepSeek 时不再消耗额度。',
59
57
  }
60
58
  const en = {
61
59
  sentFrom: 'Sent by {name} from another chat',
@@ -76,8 +74,6 @@ window.__ModuleLoader__.load({
76
74
  notInstalled: 'not installed',
77
75
  notRegistered: 'not registered',
78
76
  and: 'and',
79
- hookTitle: 'One more step for Codex',
80
- hookText: 'Open Codex → Settings → Hooks and trust the Stop hook “dsh-baton: waiting for the Harness run”. Once trusted, Codex no longer uses your quota while it waits for DeepSeek.',
81
77
  }
82
78
 
83
79
  // --- Plugins page ------------------------------------------------------------
@@ -98,12 +94,7 @@ window.__ModuleLoader__.load({
98
94
  if (res.ok) next = (await res.json()).registration
99
95
  } catch { /* not served yet */ }
100
96
  if (stopped) return
101
- if (next && next.state !== 'pending') {
102
- setReg(next)
103
- // Waiting for the user to trust the Codex hook: look again, so the note goes once they do.
104
- if (untrusted(next)) setTimeout(poll, 4000)
105
- return
106
- }
97
+ if (next && next.state !== 'pending') return setReg(next)
107
98
  if (++tries >= 30) return setReg({ state: 'unknown' })
108
99
  setTimeout(poll, 700)
109
100
  }
@@ -113,30 +104,6 @@ window.__ModuleLoader__.load({
113
104
  return reg
114
105
  }
115
106
 
116
- const untrusted = (reg) => Boolean(reg?.codexHook?.installed && !reg.codexHook.trusted)
117
-
118
- // Codex runs dsh-baton's Stop hook only once the user trusts it in Codex; until then
119
- // Codex spends model requests while it waits. Shown until they do, in Harness's accent.
120
- function TrustNote({ reg, t }) {
121
- if (!untrusted(reg)) return null
122
- const accent = 'var(--dsw-alias-state-business-primary)'
123
- return h('div', {
124
- role: 'note', 'data-dsh-baton-trust': '',
125
- style: {
126
- display: 'flex', gap: 10, alignItems: 'flex-start', marginTop: 8, padding: '10px 12px', borderRadius: 8,
127
- background: `color-mix(in srgb, ${accent} 7%, transparent)`,
128
- border: `1px solid color-mix(in srgb, ${accent} 22%, transparent)`,
129
- color: 'var(--dsw-alias-label-secondary)', fontSize: 13, lineHeight: '20px', textAlign: 'start',
130
- },
131
- },
132
- h('svg', { width: 16, height: 16, viewBox: '0 0 16 16', fill: 'none', 'aria-hidden': true, style: { flex: 'none', marginTop: 2, color: accent } },
133
- h('path', { d: 'M8 1.75 2.75 3.9v3.6c0 3.1 2.2 5.6 5.25 6.75 3.05-1.15 5.25-3.65 5.25-6.75V3.9z', stroke: 'currentColor', strokeWidth: 1.3, strokeLinejoin: 'round' }),
134
- h('path', { d: 'm5.6 8.1 1.7 1.7 3.2-3.4', stroke: 'currentColor', strokeWidth: 1.3, strokeLinecap: 'round', strokeLinejoin: 'round' })),
135
- h('div', null,
136
- h('div', { style: { color: 'var(--dsw-alias-label-primary)', fontWeight: 600 } }, t('hookTitle')),
137
- h('div', null, t('hookText'))))
138
- }
139
-
140
107
  // What to tell the user: [title or null, text]. null while the host is still working.
141
108
  function describe(reg, t) {
142
109
  if (!reg || reg.state === 'pending') return [null, t('pending')]
@@ -161,12 +128,11 @@ window.__ModuleLoader__.load({
161
128
  return h(ui.Modal, {
162
129
  open: true, title: title ?? t('readyTitle'), closeLabel: t('ok'), onClose: onDismiss,
163
130
  footer: h(ui.Button, { variant: 'primary', 'data-modal-autofocus': true, onClick: onDismiss }, t('ok')),
164
- }, h('p', null, text), h(TrustNote, { reg, t }))
131
+ }, h('p', null, text))
165
132
  }
166
133
  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)' } },
167
134
  title ? h('strong', null, title) : null,
168
135
  h('p', { style: { margin: '6px 0' } }, text),
169
- h(TrustNote, { reg, t }),
170
136
  h('button', { type: 'button', onClick: onDismiss }, t('ok')))
171
137
  }
172
138
  }
@@ -182,8 +148,7 @@ window.__ModuleLoader__.load({
182
148
  return h('div', { style: { display: 'flex', flexDirection: 'column', gap: 4, color: 'var(--dsw-alias-label-secondary)' } },
183
149
  h('strong', { style: { color: 'var(--dsw-alias-label-primary)' } }, t('status')),
184
150
  rows.length ? h('span', null, rows.join(' · ')) : null,
185
- h('span', null, text),
186
- h(TrustNote, { reg, t }))
151
+ h('span', null, text))
187
152
  }
188
153
  }
189
154
 
package/lib/index.js CHANGED
@@ -88,11 +88,21 @@ function toolCallsOf(content) {
88
88
  const iso = (t) => (typeof t === 'number' ? new Date(t).toISOString() : t ?? null)
89
89
 
90
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.
91
+ // this host folder sits in, else the dsh-baton-mcp package installed with the plugin. That
92
+ // one must be this plugin's own version (its exact dependency): a copy of another version
93
+ // left nested in the plugin folder (pnpm moves a link: plugin's folder in on uninstall) is
94
+ // not installed over ~/.dsh-baton/app.
92
95
  function serverRoot() {
93
96
  const up = fileURLToPath(new URL('../../', import.meta.url))
94
97
  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 }
98
+ // Looked up from here (a nested copy first), then from the folder the plugin is installed in.
99
+ for (const from of [import.meta.url, pathToFileURL(path.join(fileURLToPath(new URL('../../', import.meta.url)), 'x.js')).href]) {
100
+ try {
101
+ const pkg = createRequire(from).resolve('dsh-baton-mcp/package.json')
102
+ if (JSON.parse(fs.readFileSync(pkg, 'utf8')).version === VERSION) return path.dirname(pkg)
103
+ } catch { /* not there */ }
104
+ }
105
+ return null
96
106
  }
97
107
 
98
108
  // The Harness install this process runs from, so its bundled Node (whose path changes
@@ -134,6 +144,8 @@ export function apply(ctx) {
134
144
  let settings = null
135
145
  // Harness's default-model service (see defaultModel()), when this Harness has one.
136
146
  let defaultModelService = null
147
+ // Harness's DeepSeek account service, when this Harness has one: read for the balance.
148
+ let accountService = null
137
149
  const runs = new Map() // runId -> run
138
150
  let stopping = false // set when the plugin unloads: release every wait()
139
151
  const waiters = new Map() // runId -> Set<resolve>
@@ -834,6 +846,30 @@ export function apply(ctx) {
834
846
  return { model: shownModel(await sessionModel(sessionId)), source: isNew ? 'default' : 'session' }
835
847
  }
836
848
 
849
+ // --- balance ------------------------------------------------------------------
850
+ // A run on the DeepSeek account with no balance fails within a second, and clients
851
+ // tended to send it again and again. So the send is refused up front when Harness's
852
+ // account service says every wallet is empty. Anything else (not signed in, an API key
853
+ // provider, the read failing) lets the send go ahead. A balance that was there is
854
+ // trusted for a minute.
855
+ const ACCOUNT_PROVIDER = 'deepseek-account'
856
+ const BALANCE_TTL_MS = 60_000
857
+ let balanceSeen = 0
858
+ async function emptyBalance() {
859
+ if (!accountService || typeof accountService.getBalance !== 'function' || Date.now() - balanceSeen < BALANCE_TTL_MS) return null
860
+ try {
861
+ const client = { version: VERSION, locale: 'en', timezoneOffsetSeconds: -new Date().getTimezoneOffset() * 60 }
862
+ const b = await Promise.race([accountService.getBalance(client), new Promise((resolve) => setTimeout(resolve, 5000, null))])
863
+ if (b?.status !== 'ready') return null
864
+ const wallets = [...(b.value ?? []), ...(b.bonusWallets ?? [])]
865
+ if (wallets.some((w) => Number(w.balance) > 0)) { balanceSeen = Date.now(); return null }
866
+ return wallets.map((w) => `${w.currency} ${w.balance}`).join(', ') || 'none'
867
+ } catch (error) {
868
+ logError('read balance', error)
869
+ return null
870
+ }
871
+ }
872
+
837
873
  async function send({ message, session_id, cwd, workspace_id, mode: asked, sender, model, reasoning_effort }) {
838
874
  if (!message || !message.trim()) throw new Error('message is required')
839
875
  if (asked != null && asked !== 'followup' && asked !== 'steer') throw new Error('mode must be followup or steer')
@@ -860,6 +896,13 @@ export function apply(ctx) {
860
896
  const chosen = await applyModel(sessionId, plan, isNew).catch((error) => {
861
897
  throw new Error(`${isNew ? `Session ${sessionId} was created, but no message was sent: ` : 'No message was sent: '}the model could not be set (${error?.message ?? error})`)
862
898
  })
899
+ // A session that never picked a model runs on Harness's default.
900
+ let provider = chosen.model?.provider
901
+ if (!provider) try { provider = defaultModel()?.currentSelection?.()?.provider } catch { /* unknown: no check */ }
902
+ if (provider === ACCOUNT_PROVIDER) {
903
+ const empty = await emptyBalance()
904
+ if (empty) throw new Error(`${isNew ? `Session ${sessionId} was created, but no message was sent` : 'No message was sent'}: the DeepSeek account has no balance left (${empty}), so the run would fail at once. Do not send again until the user has topped up (Harness → Settings → Account, or platform.deepseek.com).`)
905
+ }
863
906
  const ahead = activeRunsFor(sessionId).map((r) => r.id)
864
907
  const run = {
865
908
  id: `hrun-${Date.now().toString(36)}-${crypto.randomBytes(3).toString('hex')}`,
@@ -1061,21 +1104,17 @@ export function apply(ctx) {
1061
1104
  // entry that is already right is left untouched. The Plugins page reads the outcome from
1062
1105
  // GET /dsh-baton/status (lib/client.js). DSH_BATON_AUTO_REGISTER=0 turns it off.
1063
1106
  let registration = { state: 'pending' }
1064
- // Whether Codex's Stop hook is trusted yet: read on each status request, so the Plugins
1065
- // page notices the user trusting it in Codex.
1066
- let hookStatus = null
1067
1107
  if (setting('AUTO_REGISTER') !== '0') {
1068
1108
  setImmediate(guard('register', async () => {
1069
1109
  const root = serverRoot()
1070
1110
  if (!root) { registration = { state: 'done', ok: false, error: 'no-server', clients: [] }; return }
1071
- const { autoRegister, harnessRuntime, codexHookStatus } = await import(pathToFileURL(path.join(root, 'src', 'register.mjs')).href)
1072
- hookStatus = codexHookStatus ?? null
1111
+ const { autoRegister, harnessRuntime } = await import(pathToFileURL(path.join(root, 'src', 'register.mjs')).href)
1073
1112
  // Inside the desktop app, its own binary can stand in for a missing Node.js.
1074
1113
  const fallback = process.versions.electron && harnessRuntime ? harnessRuntime(process.execPath) : null
1075
1114
  const result = autoRegister({ root, harnessDir: harnessDir(), fallback })
1076
1115
  registration = { state: 'done', ...result }
1077
1116
  const summary = result.clients.map((c) => `${c.label}: ${c.state}${c.error ? ` (${c.error})` : ''}`).join(', ')
1078
- ctx.logger?.[result.ok ? 'info' : 'error']?.(`dsh-baton-host MCP registration: ${summary || result.error}${result.codexHook ? `; Codex Stop hook: ${result.codexHook}` : ''}`)
1117
+ ctx.logger?.[result.ok ? 'info' : 'error']?.(`dsh-baton-host MCP registration: ${summary || result.error}${result.codexHook && result.codexHook !== 'none' ? `; old Codex Stop hook: ${result.codexHook}` : ''}`)
1079
1118
  }))
1080
1119
  } else {
1081
1120
  registration = { state: 'off' }
@@ -1085,6 +1124,10 @@ export function apply(ctx) {
1085
1124
  settings = child.settings
1086
1125
  child.effect(() => () => { if (settings === child.settings) settings = null }, 'dsh-baton-host settings')
1087
1126
  })
1127
+ ctx.inject(['accountController'], (child) => {
1128
+ accountService = child.accountController
1129
+ child.effect(() => () => { if (accountService === child.accountController) accountService = null }, 'dsh-baton-host account')
1130
+ })
1088
1131
  ctx.inject(['agentDefaultModel'], (child) => {
1089
1132
  defaultModelService = child.agentDefaultModel
1090
1133
  child.effect(() => () => { if (defaultModelService === child.agentDefaultModel) defaultModelService = null }, 'dsh-baton-host default model')
@@ -1096,9 +1139,7 @@ export function apply(ctx) {
1096
1139
  handler: (request, response) => {
1097
1140
  const { state, ok, error, version, runtime, clients = [] } = registration
1098
1141
  response.writeHead(200, { 'content-type': 'application/json', 'cache-control': 'no-store' })
1099
- let codexHook = null
1100
- try { codexHook = hookStatus?.() ?? null } catch { /* config unreadable: say nothing */ }
1101
- response.end(JSON.stringify({ version: VERSION, registration: { state, ok, error, version, runtime, clients: clients.map(({ id, label, state }) => ({ id, label, state })), codexHook } }))
1142
+ response.end(JSON.stringify({ version: VERSION, registration: { state, ok, error, version, runtime, clients: clients.map(({ id, label, state }) => ({ id, label, state })) } }))
1102
1143
  },
1103
1144
  }), 'dsh-baton-host status route')
1104
1145
  })
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-baton-host",
3
- "version": "0.6.2",
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.",
3
+ "version": "0.6.4-next.0",
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",
7
7
  "exports": {
@@ -20,7 +20,7 @@
20
20
  "LICENSE"
21
21
  ],
22
22
  "dependencies": {
23
- "dsh-baton-mcp": "0.6.2"
23
+ "dsh-baton-mcp": "0.6.4-next.0"
24
24
  },
25
25
  "license": "MIT",
26
26
  "dsh": {