dsh-baton-host 0.6.0 → 0.6.2
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 +40 -4
- package/lib/index.js +156 -6
- package/package.json +2 -2
package/lib/client.js
CHANGED
|
@@ -54,6 +54,8 @@ 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 时不再消耗额度。',
|
|
57
59
|
}
|
|
58
60
|
const en = {
|
|
59
61
|
sentFrom: 'Sent by {name} from another chat',
|
|
@@ -74,6 +76,8 @@ window.__ModuleLoader__.load({
|
|
|
74
76
|
notInstalled: 'not installed',
|
|
75
77
|
notRegistered: 'not registered',
|
|
76
78
|
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.',
|
|
77
81
|
}
|
|
78
82
|
|
|
79
83
|
// --- Plugins page ------------------------------------------------------------
|
|
@@ -94,7 +98,12 @@ window.__ModuleLoader__.load({
|
|
|
94
98
|
if (res.ok) next = (await res.json()).registration
|
|
95
99
|
} catch { /* not served yet */ }
|
|
96
100
|
if (stopped) return
|
|
97
|
-
if (next && next.state !== 'pending')
|
|
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
|
+
}
|
|
98
107
|
if (++tries >= 30) return setReg({ state: 'unknown' })
|
|
99
108
|
setTimeout(poll, 700)
|
|
100
109
|
}
|
|
@@ -104,6 +113,30 @@ window.__ModuleLoader__.load({
|
|
|
104
113
|
return reg
|
|
105
114
|
}
|
|
106
115
|
|
|
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
|
+
|
|
107
140
|
// What to tell the user: [title or null, text]. null while the host is still working.
|
|
108
141
|
function describe(reg, t) {
|
|
109
142
|
if (!reg || reg.state === 'pending') return [null, t('pending')]
|
|
@@ -122,16 +155,18 @@ window.__ModuleLoader__.load({
|
|
|
122
155
|
// Shown once, right after "Enable now".
|
|
123
156
|
function wrapActivation(t) {
|
|
124
157
|
return function DshBatonActivation({ onDismiss }) {
|
|
125
|
-
const
|
|
158
|
+
const reg = useRegistration()
|
|
159
|
+
const [title, text] = describe(reg, t)
|
|
126
160
|
if (ui?.Modal && ui?.Button) {
|
|
127
161
|
return h(ui.Modal, {
|
|
128
162
|
open: true, title: title ?? t('readyTitle'), closeLabel: t('ok'), onClose: onDismiss,
|
|
129
163
|
footer: h(ui.Button, { variant: 'primary', 'data-modal-autofocus': true, onClick: onDismiss }, t('ok')),
|
|
130
|
-
}, h('p', null, text))
|
|
164
|
+
}, h('p', null, text), h(TrustNote, { reg, t }))
|
|
131
165
|
}
|
|
132
166
|
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
167
|
title ? h('strong', null, title) : null,
|
|
134
168
|
h('p', { style: { margin: '6px 0' } }, text),
|
|
169
|
+
h(TrustNote, { reg, t }),
|
|
135
170
|
h('button', { type: 'button', onClick: onDismiss }, t('ok')))
|
|
136
171
|
}
|
|
137
172
|
}
|
|
@@ -147,7 +182,8 @@ window.__ModuleLoader__.load({
|
|
|
147
182
|
return h('div', { style: { display: 'flex', flexDirection: 'column', gap: 4, color: 'var(--dsw-alias-label-secondary)' } },
|
|
148
183
|
h('strong', { style: { color: 'var(--dsw-alias-label-primary)' } }, t('status')),
|
|
149
184
|
rows.length ? h('span', null, rows.join(' · ')) : null,
|
|
150
|
-
h('span', null, text)
|
|
185
|
+
h('span', null, text),
|
|
186
|
+
h(TrustNote, { reg, t }))
|
|
151
187
|
}
|
|
152
188
|
}
|
|
153
189
|
|
package/lib/index.js
CHANGED
|
@@ -132,6 +132,8 @@ export function apply(ctx) {
|
|
|
132
132
|
const registry = ctx.workspaceRegistry
|
|
133
133
|
// Harness's settings service, when this Harness has one: read for its busy-send setting.
|
|
134
134
|
let settings = null
|
|
135
|
+
// Harness's default-model service (see defaultModel()), when this Harness has one.
|
|
136
|
+
let defaultModelService = null
|
|
135
137
|
const runs = new Map() // runId -> run
|
|
136
138
|
let stopping = false // set when the plugin unloads: release every wait()
|
|
137
139
|
const waiters = new Map() // runId -> Set<resolve>
|
|
@@ -142,6 +144,7 @@ export function apply(ctx) {
|
|
|
142
144
|
run_id: run.id, session_id: run.sessionId, state: run.state, started_at: run.startedAt, ended_at: run.endedAt,
|
|
143
145
|
message_preview: run.preview, final_text: run.finalText, turn_end: run.turnEnd, error: run.error,
|
|
144
146
|
...(run.sender ? { sender: run.sender } : {}),
|
|
147
|
+
...(run.model ? { model: run.model } : {}),
|
|
145
148
|
...(run.waitingOn ? { waiting_on: run.waitingOn } : {}),
|
|
146
149
|
...(run.supersededBy ? { superseded_by: run.supersededBy } : {}),
|
|
147
150
|
...(run.humanInputs?.length ? { human_interjections: run.humanInputs } : {}),
|
|
@@ -677,6 +680,7 @@ export function apply(ctx) {
|
|
|
677
680
|
session_id: item.sessionId, cwd: item.cwd ?? null, title: item.title ?? item.projections?.values?.title ?? null,
|
|
678
681
|
updated_at: iso(item.updatedAt), running: !!item.running, blank: !!item.blank,
|
|
679
682
|
bridge_runs: activeRunsFor(item.sessionId).map((r) => r.id),
|
|
683
|
+
model: shownModel(item.projections?.values?.modelSelection?.next ?? item.projections?.values?.modelSelection?.lastUsed),
|
|
680
684
|
}
|
|
681
685
|
if (want && (!row.cwd || normalizePath(row.cwd) !== want)) continue
|
|
682
686
|
if (q && !`${row.title ?? ''} ${row.cwd ?? ''} ${row.session_id}`.toLowerCase().includes(q)) continue
|
|
@@ -704,7 +708,133 @@ export function apply(ctx) {
|
|
|
704
708
|
return { session_id, ...(isArchived(session_id) ? { archived: true } : {}), cwd: meta?.cwd ?? null, title, total_messages: messages.length, messages: messages.slice(-last) }
|
|
705
709
|
}
|
|
706
710
|
|
|
707
|
-
|
|
711
|
+
// --- models -----------------------------------------------------------------
|
|
712
|
+
// Harness's own catalog decides which names exist: "V4 Pro", "v4-pro" and "DeepSeek-V4-Pro"
|
|
713
|
+
// all name the model whose id or name reads that way once case, spaces, hyphens,
|
|
714
|
+
// underscores and a leading "deepseek" are left out. No model name is written here.
|
|
715
|
+
|
|
716
|
+
const MODELS_UNSUPPORTED = 'This DeepSeek Harness version cannot choose a model per session: its session controller has no modelCatalog/selectModel. Leave out model and reasoning_effort, or update Harness.'
|
|
717
|
+
const modelKey = (s) => String(s ?? '').toLowerCase().replace(/[\s_-]+/g, '').replace(/^deepseek/, '')
|
|
718
|
+
// Harness's default-model service comes through inject (below); reading it off ctx
|
|
719
|
+
// without that throws in Harness, so that read is only a fallback.
|
|
720
|
+
const defaultModel = () => {
|
|
721
|
+
if (defaultModelService) return defaultModelService
|
|
722
|
+
try { return ctx.agentDefaultModel ?? null } catch { return null }
|
|
723
|
+
}
|
|
724
|
+
const canSelect = () => typeof controller.modelCatalog === 'function' && typeof controller.selectModel === 'function'
|
|
725
|
+
const shownModel = (s) => (s ? { provider: s.provider, model: s.model, reasoning_effort: s.reasoningEffort ?? null } : null)
|
|
726
|
+
|
|
727
|
+
async function catalog() {
|
|
728
|
+
if (typeof controller.modelCatalog !== 'function') throw new Error(MODELS_UNSUPPORTED)
|
|
729
|
+
return controller.modelCatalog()
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
async function models() {
|
|
733
|
+
const c = await catalog()
|
|
734
|
+
return {
|
|
735
|
+
default: shownModel(c.default),
|
|
736
|
+
models: c.groups.flatMap((g) => g.models.map((m) => ({
|
|
737
|
+
provider: g.id, model: m.id, name: m.name ?? m.id,
|
|
738
|
+
...(m.reasoning ? { reasoning_efforts: m.reasoning.efforts.map((e) => e.id), default_effort: m.reasoning.defaultEffort ?? null } : {}),
|
|
739
|
+
}))),
|
|
740
|
+
...(c.failures?.length ? { failures: c.failures.map((f) => ({ provider: f.id, error: f.message })) } : {}),
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
const listed = (c) => c.groups.flatMap((g) => g.models.map((m) => `${g.id}/${m.id}${m.name && m.name !== m.id ? ` (${m.name})` : ''}`)).join(', ') || 'none'
|
|
745
|
+
|
|
746
|
+
// One model, or an error naming what is there: never a near miss in its place.
|
|
747
|
+
// "provider/model" picks between providers that offer the same model.
|
|
748
|
+
function matchModel(c, asked) {
|
|
749
|
+
const slash = String(asked).lastIndexOf('/')
|
|
750
|
+
const provider = slash > 0 ? modelKey(String(asked).slice(0, slash)) : null
|
|
751
|
+
const want = modelKey(slash > 0 ? String(asked).slice(slash + 1) : asked)
|
|
752
|
+
const hits = c.groups.flatMap((g) => (provider && modelKey(g.id) !== provider ? [] : g.models
|
|
753
|
+
.filter((m) => modelKey(m.id) === want || modelKey(m.name) === want)
|
|
754
|
+
.map((m) => ({ provider: g.id, entry: m }))))
|
|
755
|
+
if (!want || !hits.length) throw new Error(`No model in Harness matches ${JSON.stringify(asked)}. Available: ${listed(c)}.`)
|
|
756
|
+
if (hits.length > 1) throw new Error(`${JSON.stringify(asked)} matches several models: ${hits.map((h) => `${h.provider}/${h.entry.id}`).join(', ')}. Pass one of them as provider/model.`)
|
|
757
|
+
return hits[0]
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
function matchEffort(hit, asked) {
|
|
761
|
+
const efforts = hit.entry.reasoning?.efforts ?? []
|
|
762
|
+
if (!efforts.length) throw new Error(`${hit.provider}/${hit.entry.id} has no reasoning effort to choose.`)
|
|
763
|
+
const e = efforts.find((x) => modelKey(x.id) === modelKey(asked) || modelKey(x.name) === modelKey(asked))
|
|
764
|
+
if (!e) throw new Error(`${hit.provider}/${hit.entry.id} has no reasoning effort ${JSON.stringify(asked)}; it has ${efforts.map((x) => x.id).join(', ')}.`)
|
|
765
|
+
return e.id
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
// What a session will run with next: its own choice or last request, else Harness's default.
|
|
769
|
+
async function sessionModel(sessionId) {
|
|
770
|
+
try {
|
|
771
|
+
const p = typeof controller.projections === 'function' ? await controller.projections({ sessionId }, signal()) : null
|
|
772
|
+
const s = p?.values?.modelSelection
|
|
773
|
+
return s?.next ?? s?.lastUsed ?? null
|
|
774
|
+
} catch { return null }
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
// selectModel also saves the pick as Harness's default model (in the background).
|
|
778
|
+
// Every pick goes through this queue and the default it found is saved back after
|
|
779
|
+
// it. Harness commits default saves in the order they are made, so the restore lands
|
|
780
|
+
// after the pick's save. The default is read once per busy spell: a pick that starts
|
|
781
|
+
// while another is still restoring would otherwise read that other pick as "the default".
|
|
782
|
+
let modelQueue = Promise.resolve()
|
|
783
|
+
let busyPicks = 0
|
|
784
|
+
let keptDefault = null
|
|
785
|
+
function keepingDefault(fn) {
|
|
786
|
+
const dm = defaultModel()
|
|
787
|
+
if (typeof dm?.currentSelection !== 'function' || typeof dm?.saveSelection !== 'function') {
|
|
788
|
+
return Promise.reject(new Error('This DeepSeek Harness version does not let dsh-baton keep its default model while choosing one for a session, so no model was chosen. Leave out model and reasoning_effort, or update Harness.'))
|
|
789
|
+
}
|
|
790
|
+
busyPicks++
|
|
791
|
+
const step = modelQueue.then(async () => {
|
|
792
|
+
keptDefault ??= dm.currentSelection()
|
|
793
|
+
try { return await fn(keptDefault) } finally {
|
|
794
|
+
await dm.saveSelection(keptDefault).catch((error) => logError('restore default model', error))
|
|
795
|
+
}
|
|
796
|
+
}).finally(() => { if (--busyPicks === 0) keptDefault = null })
|
|
797
|
+
modelQueue = step.catch(() => {})
|
|
798
|
+
return step
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
// Work out the model a send asks for before anything is created, so a name that
|
|
802
|
+
// matches nothing (or several) fails the send with the list of what is there.
|
|
803
|
+
async function planModel(sessionId, { model, reasoning_effort }) {
|
|
804
|
+
if (model == null && reasoning_effort == null) return null
|
|
805
|
+
if (!canSelect()) throw new Error(MODELS_UNSUPPORTED)
|
|
806
|
+
const c = await catalog()
|
|
807
|
+
let hit
|
|
808
|
+
if (model != null) hit = matchModel(c, model)
|
|
809
|
+
else {
|
|
810
|
+
// Effort alone: the model the session runs with now (a new one: Harness's default).
|
|
811
|
+
const current = (sessionId && (await sessionModel(sessionId))) || c.default
|
|
812
|
+
const entry = c.groups.find((g) => g.id === current?.provider)?.models.find((m) => m.id === current?.model)
|
|
813
|
+
if (!entry) throw new Error(`The model ${current?.provider}/${current?.model} is not in Harness's catalog now; pass model too. Available: ${listed(c)}.`)
|
|
814
|
+
hit = { provider: current.provider, entry }
|
|
815
|
+
}
|
|
816
|
+
return { provider: hit.provider, model: hit.entry.id, ...(reasoning_effort != null ? { reasoningEffort: matchEffort(hit, reasoning_effort) } : {}) }
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
// Put the session on its model before the prompt goes in. Without a plan, a new session
|
|
820
|
+
// is pinned to the default as it stands, so a pick running for another send cannot leak
|
|
821
|
+
// into it (Harness reads the default only when the first request is built); an existing
|
|
822
|
+
// one keeps its own. The pin is best effort: if it fails, the session just uses the default.
|
|
823
|
+
async function applyModel(sessionId, plan, isNew) {
|
|
824
|
+
if (plan) {
|
|
825
|
+
const { selected } = await keepingDefault(() => controller.selectModel({ sessionId, ...plan }))
|
|
826
|
+
return { model: shownModel(selected), source: 'requested' }
|
|
827
|
+
}
|
|
828
|
+
if (isNew && canSelect() && defaultModel()) {
|
|
829
|
+
try {
|
|
830
|
+
const { selected } = await keepingDefault((kept) => controller.selectModel({ sessionId, ...kept }))
|
|
831
|
+
return { model: shownModel(selected), source: 'default' }
|
|
832
|
+
} catch (error) { logError('pin default model', error) }
|
|
833
|
+
}
|
|
834
|
+
return { model: shownModel(await sessionModel(sessionId)), source: isNew ? 'default' : 'session' }
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
async function send({ message, session_id, cwd, workspace_id, mode: asked, sender, model, reasoning_effort }) {
|
|
708
838
|
if (!message || !message.trim()) throw new Error('message is required')
|
|
709
839
|
if (asked != null && asked !== 'followup' && asked !== 'steer') throw new Error('mode must be followup or steer')
|
|
710
840
|
// Without a mode, a message to a busy session does what Harness's own busy-send setting says.
|
|
@@ -718,18 +848,25 @@ export function apply(ctx) {
|
|
|
718
848
|
if (sessionId && isArchived(sessionId)) {
|
|
719
849
|
throw new Error(`Session ${sessionId} is archived. Unarchive it in Harness, or start a new session with cwd or workspace_id.`)
|
|
720
850
|
}
|
|
721
|
-
if (!sessionId)
|
|
722
|
-
|
|
851
|
+
if (!sessionId && !cwd && !workspace_id) throw new Error('A new session needs cwd or workspace_id. Pass session_id to continue an existing one.')
|
|
852
|
+
const plan = await planModel(sessionId, { model, reasoning_effort })
|
|
853
|
+
const isNew = !sessionId
|
|
854
|
+
if (isNew) {
|
|
723
855
|
// A folder that is not a workspace yet is registered first, so the new session shows in the sidebar.
|
|
724
856
|
workspace = workspace_id ? { id: workspace_id } : await createWorkspace({ path: cwd, create_dir: false })
|
|
725
857
|
sessionId = (await controller.create({ workspaceId: workspace.id })).sessionId
|
|
726
858
|
}
|
|
859
|
+
// A model that cannot be set fails the send before the prompt goes in.
|
|
860
|
+
const chosen = await applyModel(sessionId, plan, isNew).catch((error) => {
|
|
861
|
+
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
|
+
})
|
|
727
863
|
const ahead = activeRunsFor(sessionId).map((r) => r.id)
|
|
728
864
|
const run = {
|
|
729
865
|
id: `hrun-${Date.now().toString(36)}-${crypto.randomBytes(3).toString('hex')}`,
|
|
730
866
|
sessionId, requestId: requestIdFor(sender), ...(sender ? { sender } : {}), mode, state: 'queued', sawPrompt: false,
|
|
731
867
|
startedAt: new Date().toISOString(), endedAt: null, preview: message.slice(0, 200),
|
|
732
868
|
events: [], finalText: null, turnEnd: null, error: null,
|
|
869
|
+
...(chosen.model ? { model: chosen.model } : {}),
|
|
733
870
|
}
|
|
734
871
|
remember(run)
|
|
735
872
|
try {
|
|
@@ -742,6 +879,8 @@ export function apply(ctx) {
|
|
|
742
879
|
...publicRun(run),
|
|
743
880
|
mode,
|
|
744
881
|
mode_source: modeSource,
|
|
882
|
+
model: chosen.model,
|
|
883
|
+
model_source: chosen.source,
|
|
745
884
|
...(ahead.length ? { other_active_runs: ahead } : {}),
|
|
746
885
|
...(workspace ? { workspace } : {}),
|
|
747
886
|
}
|
|
@@ -861,6 +1000,7 @@ export function apply(ctx) {
|
|
|
861
1000
|
create_workspace: createWorkspace,
|
|
862
1001
|
list_sessions: listSessions,
|
|
863
1002
|
read_session: readSession,
|
|
1003
|
+
models,
|
|
864
1004
|
send,
|
|
865
1005
|
status: ({ run_id, tail }) => publicRun(getRun(run_id), tail ?? 20),
|
|
866
1006
|
wait,
|
|
@@ -921,17 +1061,21 @@ export function apply(ctx) {
|
|
|
921
1061
|
// entry that is already right is left untouched. The Plugins page reads the outcome from
|
|
922
1062
|
// GET /dsh-baton/status (lib/client.js). DSH_BATON_AUTO_REGISTER=0 turns it off.
|
|
923
1063
|
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
|
|
924
1067
|
if (setting('AUTO_REGISTER') !== '0') {
|
|
925
1068
|
setImmediate(guard('register', async () => {
|
|
926
1069
|
const root = serverRoot()
|
|
927
1070
|
if (!root) { registration = { state: 'done', ok: false, error: 'no-server', clients: [] }; return }
|
|
928
|
-
const { autoRegister, harnessRuntime } = await import(pathToFileURL(path.join(root, 'src', 'register.mjs')).href)
|
|
1071
|
+
const { autoRegister, harnessRuntime, codexHookStatus } = await import(pathToFileURL(path.join(root, 'src', 'register.mjs')).href)
|
|
1072
|
+
hookStatus = codexHookStatus ?? null
|
|
929
1073
|
// Inside the desktop app, its own binary can stand in for a missing Node.js.
|
|
930
1074
|
const fallback = process.versions.electron && harnessRuntime ? harnessRuntime(process.execPath) : null
|
|
931
1075
|
const result = autoRegister({ root, harnessDir: harnessDir(), fallback })
|
|
932
1076
|
registration = { state: 'done', ...result }
|
|
933
1077
|
const summary = result.clients.map((c) => `${c.label}: ${c.state}${c.error ? ` (${c.error})` : ''}`).join(', ')
|
|
934
|
-
ctx.logger?.[result.ok ? 'info' : 'error']?.(`dsh-baton-host MCP registration: ${summary || result.error}`)
|
|
1078
|
+
ctx.logger?.[result.ok ? 'info' : 'error']?.(`dsh-baton-host MCP registration: ${summary || result.error}${result.codexHook ? `; Codex Stop hook: ${result.codexHook}` : ''}`)
|
|
935
1079
|
}))
|
|
936
1080
|
} else {
|
|
937
1081
|
registration = { state: 'off' }
|
|
@@ -941,6 +1085,10 @@ export function apply(ctx) {
|
|
|
941
1085
|
settings = child.settings
|
|
942
1086
|
child.effect(() => () => { if (settings === child.settings) settings = null }, 'dsh-baton-host settings')
|
|
943
1087
|
})
|
|
1088
|
+
ctx.inject(['agentDefaultModel'], (child) => {
|
|
1089
|
+
defaultModelService = child.agentDefaultModel
|
|
1090
|
+
child.effect(() => () => { if (defaultModelService === child.agentDefaultModel) defaultModelService = null }, 'dsh-baton-host default model')
|
|
1091
|
+
})
|
|
944
1092
|
ctx.inject(['webServer'], (web) => {
|
|
945
1093
|
web.effect(() => web.webServer.register({
|
|
946
1094
|
kind: 'exact',
|
|
@@ -948,7 +1096,9 @@ export function apply(ctx) {
|
|
|
948
1096
|
handler: (request, response) => {
|
|
949
1097
|
const { state, ok, error, version, runtime, clients = [] } = registration
|
|
950
1098
|
response.writeHead(200, { 'content-type': 'application/json', 'cache-control': 'no-store' })
|
|
951
|
-
|
|
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 } }))
|
|
952
1102
|
},
|
|
953
1103
|
}), 'dsh-baton-host status route')
|
|
954
1104
|
})
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-baton-host",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.2",
|
|
4
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",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"LICENSE"
|
|
21
21
|
],
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"dsh-baton-mcp": "0.6.
|
|
23
|
+
"dsh-baton-mcp": "0.6.2"
|
|
24
24
|
},
|
|
25
25
|
"license": "MIT",
|
|
26
26
|
"dsh": {
|