dsh-baton-host 0.6.3 → 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/lib/client.js +3 -38
- package/lib/index.js +52 -11
- package/package.json +3 -3
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)
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
4
|
-
"description": "DeepSeek Harness plugin for dsh-baton:
|
|
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.
|
|
23
|
+
"dsh-baton-mcp": "0.6.4-next.0"
|
|
24
24
|
},
|
|
25
25
|
"license": "MIT",
|
|
26
26
|
"dsh": {
|