@goodandready/dsh-moa 0.2.15 → 0.2.16

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
@@ -282,6 +282,13 @@ npm test
282
282
 
283
283
  ---
284
284
 
285
+
286
+ ## 🛠️ Internal Tooling & Development
287
+
288
+ For local verification and package integrity validation:
289
+ - `npm test`: runs the full test suite (95 tests)
290
+ - `./deploy.sh`: local infrastructure validation script (verifies package size, identity parity in package.json/cordis.patch.yml/client.js, and tests). Excluded from the published npm package.
291
+
285
292
  ## 📄 License
286
293
 
287
294
  MIT © [GooDAnDReaDY](https://github.com/GooDAnDReaDY)
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Run a non-critical side effect; never throw.
3
+ * Logs at debug/warn when a logger is provided or via console.debug.
4
+ *
5
+ * @param {string} label - Context identifier for telemetry / diagnostics
6
+ * @param {Function} fn - Operation to execute
7
+ * @param {object} [logger] - Optional logger instance
8
+ */
9
+ export function bestEffort(label, fn, logger) {
10
+ const log = (err) => {
11
+ try {
12
+ if (logger && typeof logger.debug === 'function') {
13
+ logger.debug(`[dsh-moa:bestEffort] ${label}:`, err?.message || err)
14
+ } else if (typeof console !== 'undefined' && typeof console.debug === 'function') {
15
+ console.debug(`[dsh-moa:bestEffort] ${label}:`, err?.message || err)
16
+ }
17
+ } catch {
18
+ // Logger itself must never crash the process
19
+ }
20
+ }
21
+
22
+ try {
23
+ const result = fn()
24
+ if (result && typeof result.then === 'function') {
25
+ return result.then(
26
+ (value) => value,
27
+ (err) => {
28
+ log(err)
29
+ return undefined
30
+ }
31
+ )
32
+ }
33
+ return result
34
+ } catch (err) {
35
+ log(err)
36
+ return undefined
37
+ }
38
+ }
package/lib/client.js CHANGED
@@ -1444,7 +1444,7 @@ window.__ModuleLoader__.load({
1444
1444
  const snapshot = React.useSyncExternalStore
1445
1445
  ? React.useSyncExternalStore(
1446
1446
  React.useMemo(() => (cb) => (scope ? scope.subscribe(cb) : () => {}), [scope]),
1447
- React.useCallback(() => (scope ? scope.getSnapshot() : { status: 'ready' }), [scope]),
1447
+ React.useCallback(() => (scope ? scope.getSnapshot() : { status: 'unavailable' }), [scope]),
1448
1448
  React.useCallback(() => ({ status: 'loading' }), [])
1449
1449
  )
1450
1450
  : null
@@ -1461,8 +1461,11 @@ window.__ModuleLoader__.load({
1461
1461
  const [leaderboardPresetFilter, setLeaderboardPresetFilter] = React.useState('all')
1462
1462
 
1463
1463
  React.useEffect(() => {
1464
- if (!scope || !snapshot) return
1465
- const snapStatus = snapshot.status || 'ready'
1464
+ if (!scope || !snapshot) {
1465
+ setStatus('unavailable')
1466
+ return
1467
+ }
1468
+ const snapStatus = snapshot.status || 'unavailable'
1466
1469
  if (snapStatus === 'ready') {
1467
1470
  const val = snapshot.value || {}
1468
1471
  if (Array.isArray(val.presets)) setPresets(val.presets)
@@ -1730,7 +1733,7 @@ window.__ModuleLoader__.load({
1730
1733
  return () => {
1731
1734
  for (const off of undo) {
1732
1735
  if (typeof off === 'function') {
1733
- try { off() } catch {}
1736
+ try { off() } catch (offErr) { /* unsubscribe best-effort */ }
1734
1737
  }
1735
1738
  }
1736
1739
  }
@@ -1,3 +1,4 @@
1
+ import { bestEffort } from './best-effort.js'
1
2
  import vm from 'node:vm'
2
3
  import fs from 'node:fs/promises'
3
4
  import path from 'node:path'
@@ -114,43 +115,55 @@ export async function collectProjectContext(baseDir, maxCharBudget = 16000) {
114
115
  const collected = []
115
116
  let totalChars = 0
116
117
 
118
+ let skippedFiles = 0
119
+ const skippedList = []
120
+
117
121
  async function scan(currentDir, relPrefix = '') {
118
122
  if (totalChars >= maxCharBudget) return
123
+ let list
119
124
  try {
120
- const list = await fs.readdir(currentDir, { withFileTypes: true })
121
- for (const item of list) {
122
- if (totalChars >= maxCharBudget) break
123
- // Skip all dotfiles, including .env and .env.* variants: their
124
- // contents must never be shipped to third-party model providers.
125
- if (item.name.startsWith('.')) continue
125
+ list = await fs.readdir(currentDir, { withFileTypes: true })
126
+ } catch (readDirErr) {
127
+ skippedFiles++
128
+ skippedList.push(relPrefix || '.')
129
+ return
130
+ }
126
131
 
127
- const rel = relPrefix ? `${relPrefix}/${item.name}` : item.name
128
- const full = path.join(currentDir, item.name)
132
+ for (const item of list) {
133
+ if (totalChars >= maxCharBudget) break
134
+ // Skip all dotfiles, including .env and .env.* variants: their
135
+ // contents must never be shipped to third-party model providers.
136
+ if (item.name.startsWith('.')) continue
129
137
 
130
- if (item.isDirectory()) {
131
- if (!ignoreDirs.has(item.name)) {
132
- await scan(full, rel)
133
- }
134
- } else if (item.isFile()) {
135
- const ext = path.extname(item.name).toLowerCase()
136
- if (codeExts.has(ext)) {
137
- try {
138
- const stat = await fs.stat(full)
139
- if (stat.size > 100000) continue // skip files > 100kb
140
- const content = await fs.readFile(full, 'utf8')
141
- const budgetRemaining = maxCharBudget - totalChars
142
- const truncated = content.length > budgetRemaining ? content.slice(0, budgetRemaining) + '\n... [truncated]' : content
143
- totalChars += truncated.length
144
- collected.push({ relativePath: rel, content: truncated, size: stat.size })
145
- } catch {}
138
+ const rel = relPrefix ? `${relPrefix}/${item.name}` : item.name
139
+ const full = path.join(currentDir, item.name)
140
+
141
+ if (item.isDirectory()) {
142
+ if (!ignoreDirs.has(item.name)) {
143
+ await scan(full, rel)
144
+ }
145
+ } else if (item.isFile()) {
146
+ const ext = path.extname(item.name).toLowerCase()
147
+ if (codeExts.has(ext)) {
148
+ try {
149
+ const stat = await fs.stat(full)
150
+ if (stat.size > 100000) continue // skip files > 100kb
151
+ const content = await fs.readFile(full, 'utf8')
152
+ const budgetRemaining = maxCharBudget - totalChars
153
+ const truncated = content.length > budgetRemaining ? content.slice(0, budgetRemaining) + '\n... [truncated]' : content
154
+ totalChars += truncated.length
155
+ collected.push({ relativePath: rel, content: truncated, size: stat.size })
156
+ } catch (fileErr) {
157
+ skippedFiles++
158
+ skippedList.push(rel)
146
159
  }
147
160
  }
148
161
  }
149
- } catch {}
162
+ }
150
163
  }
151
164
 
152
165
  await scan(baseDir)
153
- return { files: collected, totalChars }
166
+ return { files: collected, totalChars, skippedFiles, skippedList }
154
167
  }
155
168
 
156
169
  /**
@@ -224,7 +237,7 @@ export async function promoteCandidateWorkspace(baseDir, winningIndex, options =
224
237
  const entries = await getFilesRecursively(winningDir)
225
238
  if (entries.length === 0) {
226
239
  if (!options.keepMoa) {
227
- try { await fs.rm(moaRoot, { recursive: true, force: true }) } catch {}
240
+ await bestEffort('clean moaRoot on empty candidate', () => fs.rm(moaRoot, { recursive: true, force: true }))
228
241
  }
229
242
  return []
230
243
  }
@@ -247,7 +260,7 @@ export async function promoteCandidateWorkspace(baseDir, winningIndex, options =
247
260
  return promoted
248
261
  } catch (err) {
249
262
  if (!options.keepMoa) {
250
- try { await fs.rm(moaRoot, { recursive: true, force: true }) } catch {}
263
+ await bestEffort('clean moaRoot on promote error', () => fs.rm(moaRoot, { recursive: true, force: true }))
251
264
  }
252
265
  return []
253
266
  }
@@ -259,9 +272,7 @@ export async function promoteCandidateWorkspace(baseDir, winningIndex, options =
259
272
  export async function cleanMoaWorkspaces(baseDir) {
260
273
  if (!baseDir) return
261
274
  const moaRoot = path.join(baseDir, '.moa')
262
- try {
263
- await fs.rm(moaRoot, { recursive: true, force: true })
264
- } catch {}
275
+ await bestEffort('cleanMoaWorkspaces', () => fs.rm(moaRoot, { recursive: true, force: true }))
265
276
  }
266
277
 
267
278
  async function getFilesRecursively(dir) {
@@ -269,13 +280,14 @@ async function getFilesRecursively(dir) {
269
280
  try {
270
281
  const stat = await fs.stat(dir)
271
282
  if (!stat.isDirectory()) return []
272
- } catch {
283
+ } catch (statErr) {
273
284
  return []
274
285
  }
275
286
 
276
287
  async function scan(current, relPrefix = '') {
288
+ let list
277
289
  try {
278
- const list = await fs.readdir(current, { withFileTypes: true })
290
+ list = await fs.readdir(current, { withFileTypes: true })
279
291
  for (const item of list) {
280
292
  const rel = relPrefix ? `${relPrefix}/${item.name}` : item.name
281
293
  const full = path.join(current, item.name)
@@ -285,7 +297,9 @@ async function getFilesRecursively(dir) {
285
297
  results.push(rel)
286
298
  }
287
299
  }
288
- } catch {}
300
+ } catch (readErr) {
301
+ return
302
+ }
289
303
  }
290
304
  await scan(dir)
291
305
  return results
package/lib/history.js CHANGED
@@ -15,7 +15,7 @@ const DEFAULT_HISTORY_FILE = path.join(DEFAULT_HISTORY_DIR, 'moa-history.jsonl')
15
15
  /** Max history file size before rotation (10 MB) */
16
16
  export const MAX_HISTORY_BYTES = 10 * 1024 * 1024
17
17
 
18
- export function getHistoryFilePath(customDir) {
18
+ function getHistoryFilePath(customDir) {
19
19
  if (customDir) {
20
20
  return path.join(customDir, '.moa-history.jsonl')
21
21
  }
@@ -33,7 +33,7 @@ export function rotateHistoryFileIfNeeded(filePath = DEFAULT_HISTORY_FILE, maxBy
33
33
  if (stat.size >= maxBytes) {
34
34
  const backupPath = `${filePath}.1`
35
35
  if (fs.existsSync(backupPath)) {
36
- try { fs.unlinkSync(backupPath) } catch {}
36
+ try { fs.unlinkSync(backupPath) } catch (unlinkErr) { /* backup unlink best-effort */ }
37
37
  }
38
38
  fs.renameSync(filePath, backupPath)
39
39
  return true
@@ -241,7 +241,7 @@ export function getMoaRunById(runId, filePath = DEFAULT_HISTORY_FILE) {
241
241
  try {
242
242
  const item = JSON.parse(lines[i])
243
243
  if (item && item.id === runId) return item
244
- } catch {}
244
+ } catch (parseErr) { /* corrupt line skipped */ }
245
245
  }
246
246
  return null
247
247
  } catch {
@@ -270,7 +270,7 @@ export function exportMoaHistory(filePath = DEFAULT_HISTORY_FILE, options = {})
270
270
  if (!item) continue
271
271
  if (presetFilter && presetFilter !== 'all' && item.preset !== presetFilter) continue
272
272
  records.push(item)
273
- } catch {}
273
+ } catch (parseErr) { /* corrupt line skipped */ }
274
274
  }
275
275
 
276
276
  if (format === 'csv') {
package/lib/index.js CHANGED
@@ -19,17 +19,12 @@ import {
19
19
  streamMoATurn,
20
20
  } from './moa-runner.js'
21
21
 
22
- import {
23
- getMoaHistory,
24
- getMoaLeaderboard,
25
- getMoaRunById,
26
- exportMoaHistory,
27
- } from './history.js'
28
- import { promoteCandidateWorkspace } from './file-workspace.js'
22
+ import { registerMoaRoutes } from './routes.js'
29
23
 
30
24
  import { createLiveCanvasClient } from './live-canvas.js'
25
+ import { registerPluginUpdater, isSafeWriteRequest } from './updater.js'
31
26
 
32
- export const name = 'dsh-moa'
27
+ export const name = '@goodandready/dsh-moa'
33
28
  export const inject = ['webServer', 'llm', 'settings', 'sessions', 'tools']
34
29
 
35
30
  export const NS = 'dsh-moa'
@@ -117,28 +112,6 @@ export const Config = z.object({
117
112
  ]),
118
113
  })
119
114
 
120
- function writeJson(res, status, body) {
121
- res.writeHead(status, {
122
- 'Content-Type': 'application/json; charset=utf-8',
123
- 'Cache-Control': 'no-store',
124
- })
125
- res.end(JSON.stringify(body))
126
- }
127
-
128
- function readBody(req) {
129
- return new Promise((resolve, reject) => {
130
- let acc = ''
131
- req.on('data', (chunk) => {
132
- acc += chunk
133
- if (acc.length > 5 * 1024 * 1024) {
134
- req.destroy()
135
- reject(new Error('Payload Too Large'))
136
- }
137
- })
138
- req.on('end', () => resolve(acc))
139
- req.on('error', reject)
140
- })
141
- }
142
115
 
143
116
  export function apply(ctx, config) {
144
117
  refreshCatalogInBackground()
@@ -238,361 +211,23 @@ export function apply(ctx, config) {
238
211
  const liveCanvas = createLiveCanvasClient({ getPort: () => ctx.webServer?.port })
239
212
 
240
213
  // Route: /dsh-moa/status
214
+ // Register one-click updater endpoint
241
215
  ctx.effect(() => {
242
- return ctx.webServer.register({
243
- kind: 'exact',
244
- path: '/dsh-moa/status',
245
- handler: (_req, res) => {
246
- const cfg = live()
247
- writeJson(res, 200, {
248
- ok: true,
249
- enabled: cfg.enabled !== false,
250
- defaultPreset: cfg.default_preset || 'default',
251
- presetsCount: (cfg.presets || []).length,
252
- })
253
- },
216
+ return registerPluginUpdater(ctx, {
217
+ endpoint: '/api/dsh-moa/update',
218
+ packageName: '@goodandready/dsh-moa',
219
+ manifestUrl: new URL('../package.json', import.meta.url),
254
220
  })
255
- }, 'dsh-moa: status route')
256
-
257
- // Route: /dsh-moa/models
258
- ctx.effect(() => {
259
- return ctx.webServer.register({
260
- kind: 'exact',
261
- path: '/dsh-moa/models',
262
- handler: async (_req, res) => {
263
- const result = []
264
- const seen = new Set()
265
-
266
- const add = (p, m) => {
267
- if (!p || !m) return
268
- const key = `${p}:${m}`
269
- if (!seen.has(key)) {
270
- seen.add(key)
271
- result.push({ provider: p, model: m, label: key })
272
- }
273
- }
274
-
275
- if (ctx.llm) {
276
- try {
277
- if (typeof ctx.llm.listAvailableModels === 'function') {
278
- const list = await ctx.llm.listAvailableModels()
279
- for (const item of (list || [])) {
280
- if (item?.provider && item?.id) add(item.provider, item.id)
281
- }
282
- }
283
- } catch (e) {
284
- console.warn('[dsh-moa] ctx.llm.listAvailableModels error:', e)
285
- }
286
-
287
- try {
288
- if (typeof ctx.llm.listConfigurableProviders === 'function') {
289
- const cProvs = ctx.llm.listConfigurableProviders() || []
290
- for (const cp of cProvs) {
291
- const cpId = cp.provider || cp.id
292
- if (cpId && typeof ctx.llm.listModels === 'function') {
293
- try {
294
- const mList = await ctx.llm.listModels(cpId)
295
- for (const m of (mList || [])) {
296
- const mId = typeof m === 'string' ? m : (m.id || m.model || m.name)
297
- if (mId) add(cpId, mId)
298
- }
299
- } catch {}
300
- }
301
- }
302
- }
303
- } catch (e) {
304
- console.warn('[dsh-moa] ctx.llm.listConfigurableProviders error:', e)
305
- }
306
- }
307
-
308
- // Query settings for configured providers
309
- try {
310
- const s = ctx.get ? ctx.get('settings') : ctx.settings
311
- const getSetting = (k) => {
312
- try { return s?.get?.(k) } catch { return undefined }
313
- }
314
-
315
- const def = getSetting('agent-default-model')
316
- if (def?.provider && def?.model) {
317
- add(def.provider, def.model)
318
- }
319
-
320
- const piAi = getSetting('llm-pi-ai')
321
- for (const [pName, pData] of Object.entries(piAi?.providers || {})) {
322
- for (const m of (pData?.models || [])) {
323
- const mId = typeof m === 'string' ? m : (m?.id || m?.model)
324
- if (mId) add(pName, mId)
325
- }
326
- }
327
-
328
- const sync = getSetting('dsh-model-sync')
329
- for (const [syncProv, syncModels] of Object.entries(sync?.modelCatalogs || {})) {
330
- for (const m of (syncModels || [])) {
331
- const mId = typeof m === 'string' ? m : (m?.id || m?.model)
332
- if (mId) add(syncProv, mId)
333
- }
334
- }
335
- } catch (e) {
336
- console.warn('[dsh-moa] Error reading settings:', e)
337
- }
338
-
339
- writeJson(res, 200, { ok: true, models: result })
340
- },
341
- })
342
- }, 'dsh-moa: models route')
343
-
344
- // Route: /dsh-moa/presets
345
- ctx.effect(() => {
346
- return ctx.webServer.register({
347
- kind: 'exact',
348
- path: '/dsh-moa/presets',
349
- handler: async (req, res) => {
350
- const cfg = live()
351
- if (req.method === 'GET') {
352
- writeJson(res, 200, {
353
- ok: true,
354
- defaultPreset: cfg.default_preset || 'default',
355
- presets: cfg.presets || [],
356
- })
357
- return
358
- }
359
-
360
- if (req.method === 'POST' || req.method === 'PUT') {
361
- try {
362
- const raw = await readBody(req)
363
- const payload = JSON.parse(raw)
364
- const newConfig = {
365
- enabled: typeof payload.enabled === 'boolean' ? payload.enabled : cfg.enabled !== false,
366
- default_preset: payload.default_preset || cfg.default_preset || 'default',
367
- presets: Array.isArray(payload.presets) ? payload.presets : (cfg.presets || []),
368
- }
369
-
370
- // Validate the merged payload through the plugin schema before it
371
- // reaches the settings storage: malformed payloads must be
372
- // rejected with 400 instead of being persisted.
373
- let validated
374
- try {
375
- validated = Config(structuredClone(newConfig))
376
- } catch (verr) {
377
- writeJson(res, 400, { ok: false, error: 'Invalid presets payload: ' + (verr?.message || String(verr)) })
378
- return
379
- }
380
-
381
- // Persist via Cordis Settings API
382
- if (settingsScope && typeof settingsScope.replace === 'function') {
383
- await settingsScope.replace(validated)
384
- } else {
385
- // In-memory fallback if settings service is unavailable
386
- getConfig = () => validated
387
- }
388
-
389
- writeJson(res, 200, {
390
- ok: true,
391
- enabled: validated.enabled !== false,
392
- defaultPreset: validated.default_preset,
393
- presets: validated.presets,
394
- })
395
- } catch (err) {
396
- writeJson(res, 400, { ok: false, error: err?.message || String(err) })
397
- }
398
- return
399
- }
400
-
401
- writeJson(res, 405, { ok: false, error: 'Method Not Allowed' })
402
- },
403
- })
404
- }, 'dsh-moa: presets route')
405
-
406
- // Route: /dsh-moa/history (#9)
407
- ctx.effect(() => {
408
- return ctx.webServer.register({
409
- kind: 'exact',
410
- path: '/dsh-moa/history',
411
- handler: async (req, res) => {
412
- if (req.method !== 'GET') {
413
- writeJson(res, 405, { ok: false, error: 'GET required' })
414
- return
415
- }
416
- try {
417
- const u = new URL(req.url, 'http://localhost')
418
- const limit = parseInt(u.searchParams.get('limit') || '20', 10)
419
- const offset = parseInt(u.searchParams.get('offset') || '0', 10)
420
- const data = getMoaHistory(limit, offset)
421
- writeJson(res, 200, { ok: true, ...data })
422
- } catch (err) {
423
- writeJson(res, 500, { ok: false, error: err?.message || String(err) })
424
- }
425
- },
426
- })
427
- }, 'dsh-moa: history route')
428
-
429
- // Route: /dsh-moa/leaderboard
430
- ctx.effect(() => {
431
- return ctx.webServer.register({
432
- kind: 'exact',
433
- path: '/dsh-moa/leaderboard',
434
- handler: async (req, res) => {
435
- if (req.method !== 'GET') {
436
- writeJson(res, 405, { ok: false, error: 'GET required' })
437
- return
438
- }
439
- try {
440
- const url = new URL(req.url || '', 'http://127.0.0.1')
441
- const preset = url.searchParams.get('preset') || null
442
- const format = url.searchParams.get('format') || 'json'
443
-
444
- if (format === 'csv') {
445
- const csv = exportMoaHistory(undefined, { format: 'csv', preset })
446
- res.writeHead(200, {
447
- 'Content-Type': 'text/csv; charset=utf-8',
448
- 'Content-Disposition': 'attachment; filename="moa-leaderboard.csv"',
449
- })
450
- res.end(csv)
451
- return
452
- }
453
-
454
- const data = getMoaLeaderboard(undefined, { preset })
455
- writeJson(res, 200, { ok: true, ...data })
456
- } catch (err) {
457
- writeJson(res, 500, { ok: false, error: err?.message || String(err) })
458
- }
459
- },
460
- })
461
- }, 'dsh-moa: leaderboard route')
462
-
463
- // Route: /dsh-moa/promote (Candidate Override Action)
464
- ctx.effect(() => {
465
- return ctx.webServer.register({
466
- kind: 'exact',
467
- path: '/dsh-moa/promote',
468
- handler: async (req, res) => {
469
- if (req.method !== 'POST') {
470
- writeJson(res, 405, { ok: false, error: 'POST required' })
471
- return
472
- }
473
- try {
474
- const raw = await readBody(req)
475
- const body = JSON.parse(raw || '{}')
476
- const candidateIndex = parseInt(body?.candidateIndex, 10)
477
- if (isNaN(candidateIndex) || candidateIndex < 1) {
478
- writeJson(res, 400, { ok: false, error: 'Valid candidateIndex required (>= 1)' })
479
- return
480
- }
481
- const cwd = body?.cwd || process.cwd()
482
- const promotedFiles = await promoteCandidateWorkspace(cwd, candidateIndex, { keepMoa: true })
483
- writeJson(res, 200, { ok: true, candidateIndex, promotedFiles })
484
- } catch (err) {
485
- writeJson(res, 500, { ok: false, error: err?.message || String(err) })
486
- }
487
- },
488
- })
489
- }, 'dsh-moa: promote candidate route')
490
-
491
- // Route: /dsh-moa/history/export
492
- ctx.effect(() => {
493
- return ctx.webServer.register({
494
- kind: 'exact',
495
- path: '/dsh-moa/history/export',
496
- handler: async (req, res) => {
497
- if (req.method !== 'GET') {
498
- writeJson(res, 405, { ok: false, error: 'GET required' })
499
- return
500
- }
501
- try {
502
- const url = new URL(req.url || '', 'http://127.0.0.1')
503
- const preset = url.searchParams.get('preset') || null
504
- const format = url.searchParams.get('format') || 'json'
505
- const data = exportMoaHistory(undefined, { format, preset })
506
-
507
- if (format === 'csv') {
508
- res.writeHead(200, {
509
- 'Content-Type': 'text/csv; charset=utf-8',
510
- 'Content-Disposition': 'attachment; filename="moa-history.csv"',
511
- })
512
- res.end(data)
513
- return
514
- }
515
-
516
- res.writeHead(200, {
517
- 'Content-Type': 'application/json; charset=utf-8',
518
- 'Content-Disposition': 'attachment; filename="moa-history.json"',
519
- })
520
- res.end(data)
521
- } catch (err) {
522
- writeJson(res, 500, { ok: false, error: err?.message || String(err) })
523
- }
524
- },
525
- })
526
- }, 'dsh-moa: history export route')
527
-
528
- // Route: /dsh-moa/runs/<id> (run replay by id)
529
- ctx.effect(() => {
530
- return ctx.webServer.register({
531
- kind: 'prefix',
532
- path: '/dsh-moa/runs/',
533
- handler: (req, res) => {
534
- if (req.method !== 'GET') {
535
- writeJson(res, 405, { ok: false, error: 'GET required' })
536
- return
537
- }
538
- const urlPath = req.url.split('?')[0]
539
- const parts = urlPath.split('/').filter(Boolean)
540
- const runId = parts[2] || ''
541
- const run = runId ? getMoaRunById(runId) : null
542
- if (!run) {
543
- writeJson(res, 404, { ok: false, error: 'Run not found' })
544
- return
545
- }
546
- writeJson(res, 200, { ok: true, run })
547
- },
548
- })
549
- }, 'dsh-moa: run by id route')
550
-
551
- // Route: /dsh-moa/run
552
- ctx.effect(() => {
553
- return ctx.webServer.register({
554
- kind: 'exact',
555
- path: '/dsh-moa/run',
556
- handler: async (req, res) => {
557
- if (req.method !== 'POST') {
558
- writeJson(res, 405, { ok: false, error: 'POST required' })
559
- return
560
- }
561
-
562
- try {
563
- const raw = await readBody(req)
564
- const payload = JSON.parse(raw)
565
- const prompt = payload.prompt || ''
566
- if (!prompt.trim()) {
567
- writeJson(res, 400, { ok: false, error: 'Empty prompt' })
568
- return
569
- }
221
+ }, 'dsh-moa: plugin updater route')
222
+
223
+ // Route dispatch: req.method verified fail-closed in lib/routes.js
224
+ registerMoaRoutes(ctx, {
225
+ live,
226
+ callLlm,
227
+ liveCanvas,
228
+ getSettingsScope: () => settingsScope,
229
+ })
570
230
 
571
- const cfg = live()
572
- if (cfg.enabled === false) {
573
- writeJson(res, 400, { ok: false, error: 'MoA is disabled in settings' })
574
- return
575
- }
576
- const presetName = payload.preset || cfg.default_preset || 'default'
577
- const preset = (cfg.presets || []).find((p) => p.name === presetName) || cfg.presets?.[0]
578
-
579
- const result = await runMoAPipeline({
580
- userPrompt: prompt,
581
- messages: payload.messages || [],
582
- preset,
583
- callLlm,
584
- cwd: payload.cwd || process.cwd(),
585
- prices: cfg.prices || {},
586
- liveCanvas,
587
- })
588
-
589
- writeJson(res, 200, { ok: true, ...result })
590
- } catch (err) {
591
- writeJson(res, 500, { ok: false, error: err?.message || String(err) })
592
- }
593
- },
594
- })
595
- }, 'dsh-moa: run pipeline route')
596
231
  // Session turn interceptor: route turn to real aggregator model & stream MoA synthesis
597
232
  const moaPendingTurns = new WeakMap()
598
233
  const moaSessionTurns = new Map()