@goodandready/dsh-moa 0.2.62 → 0.2.64

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/events.js ADDED
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Cordis Event Bus Definitions and Helpers for dsh-moa
3
+ */
4
+
5
+ export const MOA_EVENTS = {
6
+ RUN_START: 'moa/run-start',
7
+ CANDIDATE_COMPLETE: 'moa/candidate-complete',
8
+ CRITIQUE_COMPLETE: 'moa/critique-complete',
9
+ JUDGE_VERDICT: 'moa/judge-verdict',
10
+ RUN_FINISH: 'moa/run-finish',
11
+ }
12
+
13
+ /**
14
+ * Dispatches an event onto the Cordis event bus with best-effort error isolation.
15
+ * If ctx or ctx.emit is not available, or an event listener throws, the pipeline is not interrupted.
16
+ *
17
+ * @param {object} [ctx] - Cordis context or mock context
18
+ * @param {string} eventName - One of MOA_EVENTS
19
+ * @param {object} payload - Event data payload
20
+ */
21
+ export function emitEvent(ctx, eventName, payload) {
22
+ if (!ctx || !eventName) return
23
+ try {
24
+ if (typeof ctx.emit === 'function') {
25
+ ctx.emit(eventName, payload)
26
+ }
27
+ } catch (_err) {
28
+ /* best-effort event dispatch: listener exceptions must never abort pipeline */
29
+ }
30
+ }
package/lib/index.js CHANGED
@@ -22,6 +22,9 @@ import { createLiveCanvasClient } from './live-canvas.js'
22
22
  import { registerPluginUpdater } from './updater.js'
23
23
  import { bestEffort } from './best-effort.js'
24
24
  import { getMoaHistory } from './history.js'
25
+ import { runMoAPipeline } from './moa-runner.js'
26
+ import { MoaService } from './moa-service.js'
27
+ import { MOA_EVENTS, emitEvent } from './events.js'
25
28
 
26
29
  export const name = '@goodandready/dsh-moa'
27
30
  export { collectProjectContext, formatProjectContext } from './file-workspace.js'
@@ -132,39 +135,54 @@ export function apply(ctx, config) {
132
135
  // required to be installed).
133
136
  const liveCanvas = createLiveCanvasClient({ getPort: () => getWebServerPort(ctx) })
134
137
 
138
+ // Native Cordis service: ctx.moa
139
+ const moaService = new MoaService(ctx, {
140
+ live,
141
+ runMoAPipeline,
142
+ resolvePresetForPrompt,
143
+ getMoaHistory,
144
+ callLlm,
145
+ liveCanvas,
146
+ get webServerPort() { return getWebServerPort(ctx) },
147
+ get timeMachineEngine() { return service(ctx, 'timeMachineEngine') },
148
+ })
149
+
150
+ if (typeof ctx.provide === 'function') {
151
+ ctx.provide('moa', moaService)
152
+ }
153
+ ctx.moa = moaService
154
+
135
155
  // Route: /dsh-moa/status & one-click updater
136
- const registerWebSurfaces = (targetCtx) => {
137
- if (!targetCtx) return
138
- const ws = service(targetCtx, 'webServer') || service(ctx, 'webServer')
156
+ const registerWebSurfaces = (webCtx) => {
157
+ if (!webCtx) return
158
+ const ws = service(webCtx, 'webServer')
139
159
  if (!ws || typeof ws.register !== 'function') return
140
160
 
141
- const eff = typeof targetCtx.effect === 'function' ? targetCtx.effect : (typeof ctx.effect === 'function' ? ctx.effect : (fn) => fn())
161
+ const eff = typeof webCtx.effect === 'function' ? webCtx.effect : (typeof ctx.effect === 'function' ? ctx.effect : (fn) => fn())
142
162
  eff(() => {
143
- return registerPluginUpdater(targetCtx, {
163
+ return registerPluginUpdater(webCtx, {
144
164
  endpoint: '/api/dsh-moa/update',
145
165
  packageName: '@goodandready/dsh-moa',
146
166
  manifestUrl: new URL('../package.json', import.meta.url),
147
167
  })
148
168
  }, 'dsh-moa: plugin updater route')
149
169
 
150
- registerMoaRoutes(targetCtx, {
170
+ registerMoaRoutes(webCtx, {
151
171
  live,
152
172
  callLlm,
153
173
  liveCanvas,
154
174
  saveConfig,
155
175
  Config,
156
176
  plainConfig,
157
- webServerPort: getWebServerPort(targetCtx) || getWebServerPort(ctx),
158
- timeMachineEngine: service(targetCtx, 'timeMachineEngine') || service(ctx, 'timeMachineEngine') || ctx.reflect?.get?.('timeMachineEngine', false),
177
+ webServerPort: getWebServerPort(webCtx) || getWebServerPort(ctx),
178
+ timeMachineEngine: service(webCtx, 'timeMachineEngine') || service(ctx, 'timeMachineEngine') || ctx.reflect?.get?.('timeMachineEngine', false),
159
179
  })
160
180
  }
161
181
 
162
- if (service(ctx, 'webServer')) {
182
+ if (service(ctx, 'webServer')?.register) {
163
183
  registerWebSurfaces(ctx)
164
184
  } else if (typeof ctx.inject === 'function') {
165
- ctx.inject(['webServer'], (webCtx) => {
166
- registerWebSurfaces(webCtx)
167
- })
185
+ ctx.inject(['webServer'], registerWebSurfaces)
168
186
  }
169
187
 
170
188
  // Session turn interceptor: route turn to real aggregator model & stream MoA synthesis
@@ -286,6 +304,7 @@ export function apply(ctx, config) {
286
304
  liveCanvas,
287
305
  webServerPort: getWebServerPort(ctx),
288
306
  timeMachineEngine: service(ctx, 'timeMachineEngine') || ctx.reflect?.get?.('timeMachineEngine', false),
307
+ ctx,
289
308
  }
290
309
 
291
310
  if (event.signal) {
@@ -339,6 +358,8 @@ export function apply(ctx, config) {
339
358
  }
340
359
 
341
360
  export { runMoAPipeline } from './moa-runner.js'
361
+ export { MoaService } from './moa-service.js'
362
+ export { MOA_EVENTS, emitEvent } from './events.js'
342
363
  export { executeTestGateForCandidates, runCandidateTestGate } from './moa-test-gate.js'
343
364
  export {
344
365
  COMPANION_PLUGINS,
@@ -100,6 +100,7 @@ export async function runReferencesParallel(references, messages, options = {},
100
100
 
101
101
  const runOne = async () => {
102
102
  let candidateTimer = null
103
+ const candidateStart = Date.now()
103
104
  try {
104
105
  const callPromise = callWithTransientRetry(
105
106
  callLlm,
@@ -144,6 +145,8 @@ export async function runReferencesParallel(references, messages, options = {},
144
145
  text,
145
146
  usage: costInfo,
146
147
  costUsd: costInfo.costUsd,
148
+ durationMs: Date.now() - candidateStart,
149
+ model: slot?.model ? (slot.provider ? (slot.provider + ":" + slot.model) : slot.model) : label,
147
150
  ok: true,
148
151
  }
149
152
 
@@ -0,0 +1,155 @@
1
+ /**
2
+ * Native Cordis Service: ctx.moa
3
+ * Allows in-process MoA consensus pipeline execution and telemetry by peer plugins.
4
+ */
5
+
6
+ import { emitEvent, MOA_EVENTS } from './events.js'
7
+
8
+ export class MoaService {
9
+ /**
10
+ * @param {import('@deepseek-ai/cordis').Context} ctx
11
+ * @param {object} [deps={}]
12
+ */
13
+ constructor(ctx, deps = {}) {
14
+ this.ctx = ctx
15
+ this.deps = deps
16
+ }
17
+
18
+ get live() {
19
+ return typeof this.deps.live === 'function' ? this.deps.live() : (this.deps.live || {})
20
+ }
21
+
22
+ /**
23
+ * Get all active configured presets
24
+ * @returns {Array<object>}
25
+ */
26
+ getPresets() {
27
+ const cfg = this.live
28
+ return Array.isArray(cfg?.presets) ? [...cfg.presets] : []
29
+ }
30
+
31
+ /**
32
+ * Get preset by ID or name
33
+ * @param {string} [idOrName]
34
+ * @returns {object|null}
35
+ */
36
+ getPreset(idOrName) {
37
+ const presets = this.getPresets()
38
+ if (!idOrName) return presets[0] || null
39
+ return presets.find((p) => p.id === idOrName || p.name === idOrName) || presets[0] || null
40
+ }
41
+
42
+ /**
43
+ * Get historical MoA runs
44
+ * @param {number} [limit=20]
45
+ * @param {number} [offset=0]
46
+ * @returns {object}
47
+ */
48
+ getHistory(limit = 20, offset = 0) {
49
+ const getHistoryFn = this.deps.getMoaHistory
50
+ if (typeof getHistoryFn === 'function') {
51
+ return getHistoryFn(limit, offset)
52
+ }
53
+ return { runs: [], total: 0 }
54
+ }
55
+
56
+ /**
57
+ * Resolve best preset for a given user prompt
58
+ * @param {string} prompt
59
+ * @returns {Promise<object>}
60
+ */
61
+ async resolvePresetForPrompt(prompt) {
62
+ const resolveFn = this.deps.resolvePresetForPrompt
63
+ if (typeof resolveFn === 'function') {
64
+ return resolveFn({ prompt, live: () => this.live, callLlm: this.deps.callLlm })
65
+ }
66
+ return { preset: this.getPreset() }
67
+ }
68
+
69
+ /**
70
+ * Run MoA consensus pipeline in-process
71
+ * @param {object} options
72
+ * @returns {Promise<object>}
73
+ */
74
+ async run(options = {}) {
75
+ const runFn = this.deps.runMoAPipeline
76
+ if (typeof runFn !== 'function') {
77
+ throw new Error('[dsh-moa] runMoAPipeline is not available in MoaService')
78
+ }
79
+
80
+ const {
81
+ prompt,
82
+ userPrompt,
83
+ messages = [],
84
+ preset: customPreset,
85
+ presetId,
86
+ cwd = process.cwd(),
87
+ signal,
88
+ onProgress,
89
+ onStreamDelta,
90
+ checkpoint,
91
+ force,
92
+ testGateEnabled,
93
+ testCommand,
94
+ } = options
95
+
96
+ const effectivePrompt = prompt || userPrompt || (messages[messages.length - 1]?.content) || ''
97
+ if (!effectivePrompt && messages.length === 0) {
98
+ throw new Error('[dsh-moa] prompt or userPrompt is required to run MoA')
99
+ }
100
+
101
+ let preset = customPreset
102
+ if (!preset) {
103
+ if (presetId) {
104
+ preset = this.getPreset(presetId)
105
+ } else {
106
+ const routed = await this.resolvePresetForPrompt(effectivePrompt)
107
+ preset = routed?.preset || this.getPreset()
108
+ }
109
+ }
110
+
111
+ const cfg = this.live
112
+ return runFn({
113
+ userPrompt: effectivePrompt,
114
+ messages,
115
+ preset,
116
+ callLlm: options.callLlm || this.deps.callLlm,
117
+ cwd,
118
+ prices: options.prices || cfg?.prices || this.deps.prices || {},
119
+ onProgress,
120
+ onStreamDelta,
121
+ signal,
122
+ checkpoint,
123
+ force,
124
+ testGateEnabled,
125
+ testCommand,
126
+ liveCanvas: options.liveCanvas || this.deps.liveCanvas,
127
+ webServerPort: options.webServerPort || this.deps.webServerPort,
128
+ timeMachineEngine: options.timeMachineEngine || this.deps.timeMachineEngine,
129
+ historyFilePath: options.historyFilePath || this.deps.historyFilePath,
130
+ ctx: this.ctx,
131
+ })
132
+ }
133
+
134
+ /**
135
+ * Attach listener to MoA events on the underlying Cordis Context
136
+ * @param {string} event
137
+ * @param {Function} listener
138
+ * @returns {Function|undefined}
139
+ */
140
+ on(event, listener) {
141
+ if (typeof this.ctx?.on === 'function') {
142
+ return this.ctx.on(event, listener)
143
+ }
144
+ return undefined
145
+ }
146
+
147
+ /**
148
+ * Emit an event on the Cordis event bus
149
+ * @param {string} event
150
+ * @param {any} payload
151
+ */
152
+ emit(event, payload) {
153
+ emitEvent(this.ctx, event, payload)
154
+ }
155
+ }
package/lib/moa-stream.js CHANGED
@@ -11,7 +11,7 @@ import { runMoAPipeline } from './moa-runner.js'
11
11
  /**
12
12
  * Streams a full MoA turn into chat with live aggregator tokens and progress feedback.
13
13
  */
14
- export async function* streamMoATurn({ targetPreset, userPrompt, messages, callLlm, cwd, prices, historyFilePath, liveCanvas, webServerPort, timeMachineEngine, force }, options = {}) {
14
+ export async function* streamMoATurn({ targetPreset, userPrompt, messages, callLlm, cwd, prices, historyFilePath, liveCanvas, webServerPort, timeMachineEngine, force, ctx }, options = {}) {
15
15
  const signal = options?.signal
16
16
  const runId = options?.runId || null
17
17
  if (signal?.aborted) return
@@ -58,6 +58,7 @@ export async function* streamMoATurn({ targetPreset, userPrompt, messages, callL
58
58
  force,
59
59
  signal: combinedSignal,
60
60
  runId,
61
+ ctx,
61
62
  })
62
63
  .catch((err) => ({ error: err }))
63
64
  .finally(() => {
@@ -14,8 +14,10 @@ export async function stageProjectContext({
14
14
  startTime,
15
15
  onProgress,
16
16
  }) {
17
- const referenceModels = Array.isArray(preset?.reference_models) && preset.reference_models.length > 0
18
- ? preset.reference_models : [{ provider: '', model: '' }]
17
+ const rawRefs = (Array.isArray(preset?.reference_models) && preset.reference_models.length > 0)
18
+ ? preset.reference_models
19
+ : (Array.isArray(preset?.references) && preset.references.length > 0 ? preset.references : [])
20
+ const referenceModels = rawRefs.length > 0 ? rawRefs : [{ provider: '', model: '' }]
19
21
  const primaryJudge = preset?.aggregator?.provider && preset?.aggregator?.model
20
22
  ? preset.aggregator : { provider: '', model: '' }
21
23
  const fallbackJudges = Array.isArray(preset?.aggregator_fallbacks) ? preset.aggregator_fallbacks : []
@@ -2,6 +2,7 @@ import { logger } from '../logger.js'
2
2
  import { promoteCandidateWorkspace, cleanMoaWorkspaces } from '../workspace/fs.js'
3
3
  import { createPromotedPreview } from '../live-canvas.js'
4
4
  import { recordMoaRunAsync, candidatesForHistory } from '../history.js'
5
+ import { emitEvent, MOA_EVENTS } from '../events.js'
5
6
 
6
7
  export async function stageFastMode({
7
8
  successfulRefs,
@@ -18,6 +19,7 @@ export async function stageFastMode({
18
19
  startTime,
19
20
  collectedCtx,
20
21
  liveCanvas,
22
+ ctx = null,
21
23
  }) {
22
24
  if (signal?.aborted) {
23
25
  await cleanMoaWorkspaces(cwd, { runId })
@@ -51,6 +53,9 @@ export async function stageFastMode({
51
53
  } catch (histErr) {
52
54
  logger.warn('[dsh-moa] Failed to record fast synthesis failure in history:', histErr?.message || histErr)
53
55
  }
56
+ emitEvent(ctx, MOA_EVENTS.RUN_FINISH, {
57
+ runId, durationMs, totalTokens, totalCostUsd, promotedFiles: [], winnerIndex: 0, error: promoteErr?.message || String(promoteErr),
58
+ })
54
59
  return {
55
60
  ok: false,
56
61
  payload: {
@@ -87,6 +92,9 @@ export async function stageFastMode({
87
92
  logger.warn('[dsh-moa] Failed to record fast synthesis run in history:', histErr?.message || histErr)
88
93
  }
89
94
 
95
+ emitEvent(ctx, MOA_EVENTS.RUN_FINISH, {
96
+ runId, durationMs, totalTokens, totalCostUsd, promotedFiles, winnerIndex: single.index,
97
+ })
90
98
  return {
91
99
  ok: true,
92
100
  payload: {
@@ -109,5 +109,5 @@ export async function stagePeerCritique({
109
109
  })
110
110
  }
111
111
 
112
- return { ok: true }
112
+ return { ok: true, critiquesCount: runRound2 ? successfulRefs.length : 0 }
113
113
  }
@@ -7,6 +7,7 @@ import { generateMoABenchmarkReport } from '../moa-report.js'
7
7
  import { parseFileScores } from '../moa-workbench.js'
8
8
  import { recordMoaRunAsync, candidatesForHistory } from '../history.js'
9
9
  import { slotLabel } from '../moa-prompts.js'
10
+ import { emitEvent, MOA_EVENTS } from '../events.js'
10
11
 
11
12
  export async function stagePromotion({
12
13
  synthesizedText,
@@ -31,6 +32,7 @@ export async function stagePromotion({
31
32
  referenceModels,
32
33
  historyFilePath,
33
34
  collectedCtx,
35
+ ctx = null,
34
36
  }) {
35
37
  const winningIndex = (multiJudgeResult?.consensus && multiJudgeResult?.winningCandidateIndex)
36
38
  ? multiJudgeResult.winningCandidateIndex
@@ -89,6 +91,13 @@ export async function stagePromotion({
89
91
  ? parseFileScores(synthesizedText, referenceOutputs)
90
92
  : null
91
93
 
94
+ emitEvent(ctx, MOA_EVENTS.JUDGE_VERDICT, {
95
+ runId,
96
+ winningIndex,
97
+ winningModel: winnerModel,
98
+ scores: fileScores || multiJudgeResult?.scores || null,
99
+ })
100
+
92
101
  try {
93
102
  await recordMoaRunAsync({
94
103
  id: runId, cwd, prompt: userPrompt, preset: preset?.name || 'default', isRefinement,
@@ -101,6 +110,15 @@ export async function stagePromotion({
101
110
  logger.warn('[dsh-moa] Failed to record run in history:', histErr)
102
111
  }
103
112
 
113
+ emitEvent(ctx, MOA_EVENTS.RUN_FINISH, {
114
+ runId,
115
+ durationMs,
116
+ totalTokens: usageSummary.totalTokens,
117
+ totalCostUsd: usageSummary.totalCostUsd,
118
+ promotedFiles,
119
+ winnerIndex: winningIndex,
120
+ })
121
+
104
122
  return {
105
123
  ok: true,
106
124
  payload: {
@@ -11,6 +11,7 @@ import { stagePeerCritique } from './peer-critique.js'
11
11
  import { stageMultiJudge } from './multi-judge.js'
12
12
  import { stageSynthesis } from './synthesis.js'
13
13
  import { stagePromotion } from './promotion.js'
14
+ import { emitEvent, MOA_EVENTS } from '../events.js'
14
15
 
15
16
  export async function runMoAPipeline({
16
17
  userPrompt,
@@ -22,12 +23,21 @@ export async function runMoAPipeline({
22
23
  testGateEnabled = null, testCommand = null, execFn = null,
23
24
  webServerPort = null, timeMachineEngine = null, timeMachineUrl = null,
24
25
  force = isTestEnvironment() ? true : false, checkpoint = true, checkpointFn = null, runId: customRunId = null,
26
+ ctx = null,
25
27
  }) {
26
28
  const runId = (customRunId && String(customRunId).trim()) || crypto.randomUUID()
27
29
  const promoteOpts = { webServerPort, timeMachineEngine, timeMachineUrl, force: Boolean(force), checkpoint, checkpointFn, signal }
28
30
  if (signal?.aborted) return { error: new Error('Turn aborted') }
29
31
  const startTime = Date.now()
30
32
 
33
+ emitEvent(ctx, MOA_EVENTS.RUN_START, {
34
+ runId,
35
+ prompt: userPrompt,
36
+ preset: preset?.name || preset?.id || 'default',
37
+ candidatesCount: Array.isArray(preset?.references) ? preset.references.length : 0,
38
+ timestamp: new Date(startTime).toISOString(),
39
+ })
40
+
31
41
  // Stage 1: Context & Configuration
32
42
  const ctxStage = await stageProjectContext({
33
43
  userPrompt, messages, preset, cwd, prices, maxTokens: preset?.max_tokens, startTime, onProgress,
@@ -54,10 +64,30 @@ export async function runMoAPipeline({
54
64
  })
55
65
  if (!candStage.ok) {
56
66
  if (candStage.abort) return { error: candStage.error }
67
+ emitEvent(ctx, MOA_EVENTS.RUN_FINISH, {
68
+ runId,
69
+ durationMs: Date.now() - startTime,
70
+ totalTokens: candStage.failurePayload?.usage?.totalTokens || 0,
71
+ totalCostUsd: candStage.failurePayload?.usage?.totalCostUsd || 0,
72
+ promotedFiles: [],
73
+ winnerIndex: 0,
74
+ error: candStage.failurePayload?.content,
75
+ })
57
76
  return candStage.failurePayload
58
77
  }
59
78
  const { referenceOutputs, successfulRefs } = candStage
60
79
 
80
+ for (const ref of referenceOutputs) {
81
+ emitEvent(ctx, MOA_EVENTS.CANDIDATE_COMPLETE, {
82
+ runId,
83
+ round: 1,
84
+ model: ref.model || ref.label,
85
+ success: Boolean(ref.ok),
86
+ durationMs: ref.durationMs || 0,
87
+ tokens: ref.usage?.totalTokens || 0,
88
+ })
89
+ }
90
+
61
91
  // Stage 3: Clarifying Questions Branch
62
92
  if (ctxStage.needsQuestions) {
63
93
  return await stageClarifyingQuestions({
@@ -75,7 +105,7 @@ export async function runMoAPipeline({
75
105
  allowCandidateOverride: ctxStage.allowCandidateOverride,
76
106
  promoteOpts, cwd, runId, signal, userPrompt, preset,
77
107
  isRefinement: ctxStage.isRefinement, historyFilePath, startTime,
78
- collectedCtx: ctxStage.collectedCtx, liveCanvas,
108
+ collectedCtx: ctxStage.collectedCtx, liveCanvas, ctx,
79
109
  })
80
110
  if (!fastRes.ok) {
81
111
  if (fastRes.abort) return { error: fastRes.error }
@@ -102,6 +132,12 @@ export async function runMoAPipeline({
102
132
  options: { testGateEnabled, testCommand, execFn },
103
133
  })
104
134
  if (!peerRes.ok) return peerRes.abortPayload
135
+ if (ctxStage.isPeerCritiqueEnabled && peerRes.ok && peerRes.critiquesCount > 0) {
136
+ emitEvent(ctx, MOA_EVENTS.CRITIQUE_COMPLETE, {
137
+ runId,
138
+ critiquesCount: peerRes.critiquesCount,
139
+ })
140
+ }
105
141
 
106
142
  if (signal?.aborted) {
107
143
  await cleanMoaWorkspaces(cwd, { runId })
@@ -153,7 +189,7 @@ export async function runMoAPipeline({
153
189
  promoteOpts, cwd, runId, signal, liveCanvas, startTime, chosenJudge, aggUsage,
154
190
  userPrompt, preset, isRefinement: ctxStage.isRefinement, isFastMode: ctxStage.isFastMode,
155
191
  effectiveReferences: ctxStage.effectiveReferences, referenceModels: ctxStage.referenceModels,
156
- historyFilePath, collectedCtx: ctxStage.collectedCtx,
192
+ historyFilePath, collectedCtx: ctxStage.collectedCtx, ctx,
157
193
  })
158
194
  if (!promRes.ok) {
159
195
  if (promRes.abort) return { error: promRes.error }
@@ -73,6 +73,7 @@ export function registerPipelineRoutes(ctx, { live, callLlm, liveCanvas, webServ
73
73
  timeMachineUrl: payload.timeMachineUrl, force: false,
74
74
  checkpoint: true,
75
75
  runId: payload.runId,
76
+ ctx,
76
77
  })
77
78
 
78
79
  if (result?.skippedFiles > 0) {
package/lib/services.js CHANGED
@@ -16,12 +16,6 @@ export function service(ctx, name) {
16
16
  } catch {
17
17
  /* strict context refuses get() for a service the fiber does not inject */
18
18
  }
19
- try {
20
- const viaReflect = ctx?.reflect?.get?.(name, false)
21
- if (viaReflect) return viaReflect
22
- } catch {
23
- /* ignore reflection errors */
24
- }
25
19
  try {
26
20
  return ctx ? ctx[name] : undefined
27
21
  } catch {
@@ -38,3 +32,4 @@ export function getWebServerPort(ctx) {
38
32
  const ws = service(ctx, 'webServer')
39
33
  return (ws && typeof ws.port === 'number') ? ws.port : null
40
34
  }
35
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-moa",
3
- "version": "0.2.62",
3
+ "version": "0.2.64",
4
4
  "description": "Mixture of Agents (MoA) plugin for DeepSeek Harness with /moa slash command",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",