@goodandready/dsh-moa 0.2.60 → 0.2.62

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.
@@ -0,0 +1,241 @@
1
+ import { writeJson, readBody, assertAuthorizedRequest } from '../access.js'
2
+ import { isSafeWriteRequest } from '../updater.js'
3
+ import { resolvePresetForPrompt } from '../moa-router.js'
4
+
5
+ let _modelsCache = { timestamp: 0, models: [] }
6
+ const MODELS_CACHE_TTL = 60 * 1000
7
+
8
+ export function registerPresetRoutes(ctx, { live, callLlm, saveConfig, Config, plainConfig }) {
9
+ ctx.effect(() => {
10
+ return ctx.webServer.register({
11
+ kind: 'exact',
12
+ path: '/dsh-moa/status',
13
+ handler: (req, res) => {
14
+ if (!assertAuthorizedRequest(req, res, ctx)) return
15
+ if (req.method !== 'GET') return writeJson(res, 405, { ok: false, error: 'GET required' })
16
+ try {
17
+ const cfg = live()
18
+ writeJson(res, 200, {
19
+ ok: true,
20
+ enabled: cfg.enabled !== false,
21
+ defaultPreset: cfg.default_preset || 'default',
22
+ presetsCount: (cfg.presets || []).length,
23
+ })
24
+ } catch (err) { writeJson(res, 500, { ok: false, error: err?.message || String(err) }) }
25
+ },
26
+ })
27
+ }, 'dsh-moa: status route')
28
+
29
+ // Route: /dsh-moa/models
30
+
31
+ ctx.effect(() => {
32
+ return ctx.webServer.register({
33
+ kind: 'exact',
34
+ path: '/dsh-moa/models',
35
+ handler: async (req, res) => {
36
+ if (!assertAuthorizedRequest(req, res, ctx)) return
37
+ if (req.method !== 'GET') return writeJson(res, 405, { ok: false, error: 'GET required' })
38
+ if (_modelsCache.models.length > 0 && Date.now() - _modelsCache.timestamp < MODELS_CACHE_TTL) {
39
+ writeJson(res, 200, { ok: true, models: _modelsCache.models, cached: true })
40
+ return
41
+ }
42
+
43
+ const result = []
44
+ const seen = new Set()
45
+
46
+ const add = (p, m) => {
47
+ if (!p || !m) return
48
+ const key = `${p}:${m}`
49
+ if (!seen.has(key)) {
50
+ seen.add(key)
51
+ result.push({ provider: p, model: m, label: key })
52
+ }
53
+ }
54
+
55
+ if (ctx.llm) {
56
+ try {
57
+ if (typeof ctx.llm.listAvailableModels === 'function') {
58
+ const list = await ctx.llm.listAvailableModels()
59
+ for (const item of (list || [])) {
60
+ if (item?.provider && item?.id) add(item.provider, item.id)
61
+ }
62
+ }
63
+ } catch (e) {
64
+ ctx.logger?.warn?.('[dsh-moa] ctx.llm.listAvailableModels error:', e)
65
+ }
66
+
67
+ try {
68
+ if (typeof ctx.llm.listConfigurableProviders === 'function') {
69
+ const cProvs = ctx.llm.listConfigurableProviders() || []
70
+ const queries = cProvs.map(async (cp) => {
71
+ const cpId = cp.provider || cp.id
72
+ if (!cpId || typeof ctx.llm.listModels !== 'function') return
73
+ try {
74
+ const mList = await Promise.race([
75
+ ctx.llm.listModels(cpId),
76
+ new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 2000)),
77
+ ])
78
+ for (const m of (mList || [])) {
79
+ const mId = typeof m === 'string' ? m : (m?.id || m?.model || m?.name)
80
+ if (mId) add(cpId, mId)
81
+ }
82
+ } catch {
83
+ /* provider query timeout or error handled best-effort */
84
+ }
85
+ })
86
+ await Promise.allSettled(queries)
87
+ }
88
+ } catch (e) {
89
+ ctx.logger?.warn?.('[dsh-moa] ctx.llm.listConfigurableProviders error:', e)
90
+ }
91
+ }
92
+
93
+ // Query settings for configured providers
94
+ try {
95
+ let s = null
96
+ try { s = ctx.get ? ctx.get('settings') : null } catch { s = null }
97
+ const getSetting = (k) => { try { return s?.get?.(k) } catch { return undefined } }
98
+
99
+ const def = getSetting('agent-default-model')
100
+ if (def?.provider && def?.model) add(def.provider, def.model)
101
+
102
+ const piAi = getSetting('llm-pi-ai')
103
+ for (const [pName, pData] of Object.entries(piAi?.providers || {})) {
104
+ for (const m of (pData?.models || [])) {
105
+ const mId = typeof m === 'string' ? m : (m?.id || m?.model)
106
+ if (mId) add(pName, mId)
107
+ }
108
+ }
109
+
110
+ const sync = getSetting('dsh-model-sync')
111
+ for (const [syncProv, syncModels] of Object.entries(sync?.modelCatalogs || {})) {
112
+ for (const m of (syncModels || [])) {
113
+ const mId = typeof m === 'string' ? m : (m?.id || m?.model)
114
+ if (mId) add(syncProv, mId)
115
+ }
116
+ }
117
+ } catch (e) {
118
+ ctx.logger?.warn?.('[dsh-moa] Error reading settings:', e)
119
+ }
120
+
121
+ _modelsCache = { timestamp: Date.now(), models: result }
122
+ writeJson(res, 200, { ok: true, models: result })
123
+ },
124
+ })
125
+ }, 'dsh-moa: models route')
126
+
127
+ // Route: /dsh-moa/presets
128
+
129
+ ctx.effect(() => {
130
+ return ctx.webServer.register({
131
+ kind: 'exact',
132
+ path: '/dsh-moa/presets',
133
+ handler: async (req, res) => {
134
+ if (!assertAuthorizedRequest(req, res, ctx)) return
135
+ const cfg = live()
136
+ if (req.method === 'GET') {
137
+ writeJson(res, 200, {
138
+ ok: true,
139
+ defaultPreset: cfg.default_preset || 'default',
140
+ presets: cfg.presets || [],
141
+ })
142
+ return
143
+ }
144
+
145
+ if (req.method === 'POST' || req.method === 'PUT') {
146
+ if (!isSafeWriteRequest(req)) {
147
+ writeJson(res, 403, { ok: false, error: 'Forbidden: cross-site request rejected' })
148
+ return
149
+ }
150
+ try {
151
+ const raw = await readBody(req)
152
+ const payload = JSON.parse(raw)
153
+ const newConfig = {
154
+ ...cfg,
155
+ enabled: typeof payload.enabled === 'boolean' ? payload.enabled : cfg.enabled !== false,
156
+ default_preset: payload.default_preset || cfg.default_preset || 'default',
157
+ presets: Array.isArray(payload.presets) ? payload.presets : (cfg.presets || []),
158
+ ...(payload.prices !== undefined ? { prices: payload.prices } : {}),
159
+ ...(payload.smart_routing_enabled !== undefined ? { smart_routing_enabled: payload.smart_routing_enabled } : {}),
160
+ ...(payload.smart_routing_model !== undefined ? { smart_routing_model: payload.smart_routing_model } : {}),
161
+ }
162
+
163
+ // Validate the merged payload through the plugin schema before it
164
+ // reaches the settings storage: malformed payloads must be
165
+ // rejected with 400 instead of being persisted.
166
+ let validated
167
+ try {
168
+ validated = Config(structuredClone(newConfig))
169
+ } catch (verr) {
170
+ writeJson(res, 400, { ok: false, error: 'Invalid presets payload: ' + (verr?.message || String(verr)) })
171
+ return
172
+ }
173
+
174
+ // Persist via saveConfig (in-memory and DSH profile settings)
175
+ let persistResult
176
+ if (typeof saveConfig === 'function') {
177
+ try { persistResult = await saveConfig(validated) }
178
+ catch (perr) { return writeJson(res, 500, { ok: false, error: 'Settings persistence failed: ' + (perr?.message || String(perr)) }) }
179
+ }
180
+
181
+ const unwrapped = typeof plainConfig === 'function' ? plainConfig(validated) : validated
182
+ writeJson(res, 200, {
183
+ ok: true,
184
+ persisted: persistResult?.persisted ?? false,
185
+ enabled: unwrapped.enabled !== false,
186
+ defaultPreset: unwrapped.default_preset,
187
+ presets: unwrapped.presets,
188
+ })
189
+ } catch (err) { writeJson(res, 400, { ok: false, error: err?.message || String(err) }) }
190
+ return
191
+ }
192
+
193
+ writeJson(res, 405, { ok: false, error: 'Method Not Allowed' })
194
+ },
195
+ })
196
+ }, 'dsh-moa: presets route')
197
+
198
+ // Route: /dsh-moa/history (#9)
199
+
200
+ ctx.effect(() => {
201
+ return ctx.webServer.register({
202
+ kind: 'exact',
203
+ path: '/dsh-moa/route-preset',
204
+ handler: async (req, res) => {
205
+ if (!assertAuthorizedRequest(req, res, ctx)) return
206
+ if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST required' })
207
+ if (!isSafeWriteRequest(req)) {
208
+ writeJson(res, 403, { ok: false, error: 'Forbidden: cross-site request rejected' })
209
+ return
210
+ }
211
+ try {
212
+ const cfg = live()
213
+ if (cfg.enabled === false) {
214
+ writeJson(res, 400, { ok: false, error: 'MoA is disabled in settings' })
215
+ return
216
+ }
217
+ const raw = await readBody(req)
218
+ const body = JSON.parse(raw || '{}')
219
+ const prompt = body?.prompt || ''
220
+ const activePreset = (cfg.presets || []).find((p) => p.name === (cfg.default_preset || 'default')) || cfg.presets?.[0]
221
+ const routingModel = (activePreset?.smart_routing_model?.model && activePreset.smart_routing_model)
222
+ || (cfg.smart_routing_model?.model && cfg.smart_routing_model) || activePreset?.smart_routing_model || cfg.smart_routing_model
223
+ const result = await resolvePresetForPrompt({
224
+ prompt,
225
+ presets: cfg.presets || [],
226
+ defaultPreset: cfg.default_preset || 'default',
227
+ routingModel,
228
+ callLlm,
229
+ enabled: cfg.enabled !== false,
230
+ })
231
+ writeJson(res, 200, { ok: true, ...result })
232
+ } catch (err) { writeJson(res, 500, { ok: false, error: err?.message || String(err) }) }
233
+ },
234
+ })
235
+ }, 'dsh-moa: route preset endpoint')
236
+
237
+ let _lastRunTimestamp = 0
238
+ const RUN_MIN_INTERVAL_MS = 1000
239
+
240
+ // Route: /dsh-moa/run
241
+ }
@@ -0,0 +1,197 @@
1
+ import path from 'node:path'
2
+ import { getMoaHistory, getMoaRunById, isTestEnvironment } from '../history.js'
3
+ import { promoteCandidateWorkspace, readCandidateFiles, computeLineDiff, isSafeRelativePath, isLoopbackUrl } from '../file-workspace.js'
4
+ import { writeJson, readBody, isAllowedWorkspace, assertAuthorizedRequest } from '../access.js'
5
+ import { isSafeWriteRequest } from '../updater.js'
6
+ import { generateMoABenchmarkReport } from '../moa-report.js'
7
+ import { handleWorkbenchGet, handleWorkbenchAssemble } from '../moa-workbench.js'
8
+
9
+ export function registerWorkbenchRoutes(ctx, { live, callLlm, webServerPort, timeMachineEngine, workspaceRoot }) {
10
+ ctx.effect(() => {
11
+ return ctx.webServer.register({
12
+ kind: 'exact',
13
+ path: '/dsh-moa/promote',
14
+ handler: async (req, res) => {
15
+ if (!assertAuthorizedRequest(req, res, ctx)) return
16
+ if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST required' })
17
+ if (!isSafeWriteRequest(req)) {
18
+ writeJson(res, 403, { ok: false, error: 'Forbidden: cross-site request rejected' })
19
+ return
20
+ }
21
+ try {
22
+ const raw = await readBody(req)
23
+ const body = JSON.parse(raw || '{}')
24
+ const candidateIndex = parseInt(body?.candidateIndex, 10)
25
+ if (isNaN(candidateIndex) || candidateIndex < 1) {
26
+ writeJson(res, 400, { ok: false, error: 'Valid candidateIndex required (>= 1)' })
27
+ return
28
+ }
29
+ const runId = body?.runId
30
+ let recordedRun = null
31
+ if (runId) {
32
+ recordedRun = getMoaRunById(runId)
33
+ if (!recordedRun) {
34
+ writeJson(res, 404, { ok: false, error: `Run '${runId}' not found` })
35
+ return
36
+ }
37
+ } else {
38
+ recordedRun = getMoaHistory(1, 0)?.runs?.[0] || null
39
+ if (!recordedRun) {
40
+ writeJson(res, 400, { ok: false, error: 'No recorded MoA run found' })
41
+ return
42
+ }
43
+ }
44
+ if (body?.cwd && path.resolve(body.cwd) !== path.resolve(recordedRun.cwd)) {
45
+ writeJson(res, 403, { ok: false, error: 'Forbidden: cwd does not match recorded run cwd' })
46
+ return
47
+ }
48
+ const effectiveCwd = recordedRun.cwd
49
+ const promotedFiles = await promoteCandidateWorkspace(effectiveCwd, candidateIndex, {
50
+ runId: recordedRun.id,
51
+ keepMoa: true,
52
+ port: ctx.webServer?.port,
53
+ timeMachineEngine: timeMachineEngine || ctx.reflect?.get?.('timeMachineEngine', false),
54
+ force: Boolean(body?.force),
55
+ })
56
+ writeJson(res, 200, { ok: true, candidateIndex, promotedFiles, checkpoint: promotedFiles?.checkpoint || null })
57
+ } catch (err) { writeJson(res, 500, { ok: false, error: err?.message || String(err) }) }
58
+ },
59
+ })
60
+ }, 'dsh-moa: promote candidate route')
61
+
62
+ // Route: /dsh-moa/diff (Candidate Diff Viewer)
63
+
64
+ ctx.effect(() => {
65
+ return ctx.webServer.register({
66
+ kind: 'exact',
67
+ path: '/dsh-moa/diff',
68
+ handler: async (req, res) => {
69
+ if (!assertAuthorizedRequest(req, res, ctx)) return
70
+ if (req.method !== 'GET') return writeJson(res, 405, { ok: false, error: 'GET required' })
71
+ try {
72
+ const u = new URL(req.url, 'http://localhost')
73
+ const rawRunId = u.searchParams.get('runId')
74
+ const queryCwd = u.searchParams.get('cwd')
75
+ let run = null
76
+ if (rawRunId && rawRunId !== 'latest' && rawRunId !== 'active') {
77
+ run = getMoaRunById(rawRunId)
78
+ if (!run) {
79
+ writeJson(res, 404, { ok: false, error: `Run '${rawRunId}' not found` })
80
+ return
81
+ }
82
+ } else {
83
+ run = getMoaHistory(1, 0)?.runs?.[0] || null
84
+ if (!run) {
85
+ writeJson(res, 404, { ok: false, error: 'No recorded MoA run found' })
86
+ return
87
+ }
88
+ }
89
+ if (queryCwd && path.resolve(queryCwd) !== path.resolve(run.cwd)) {
90
+ writeJson(res, 403, { ok: false, error: 'Forbidden: cwd does not match recorded run cwd' })
91
+ return
92
+ }
93
+ const runId = run.id
94
+ const cwd = run.cwd
95
+ const sanitizeTarget = (val, def) => {
96
+ const clean = String(val || '').trim().replace(/[^a-zA-Z0-9_-]/g, '')
97
+ return clean || def
98
+ }
99
+ const from = sanitizeTarget(u.searchParams.get('from'), '1')
100
+ const to = sanitizeTarget(u.searchParams.get('to'), 'curator-synthesis')
101
+ const rawFile = u.searchParams.get('file') || ''
102
+ const file = (rawFile && isSafeRelativePath(rawFile)) ? rawFile : ''
103
+
104
+ const fromFiles = await readCandidateFiles(cwd, from, { runId })
105
+ const toFiles = await readCandidateFiles(cwd, to, { runId })
106
+
107
+ // Collect all unique file paths
108
+ const allPaths = Array.from(new Set([
109
+ ...fromFiles.map((f) => f.relativePath),
110
+ ...toFiles.map((f) => f.relativePath),
111
+ ])).sort()
112
+
113
+ const targetPath = file || allPaths[0] || ''
114
+ const fromItem = fromFiles.find((f) => f.relativePath === targetPath)
115
+ const toItem = toFiles.find((f) => f.relativePath === targetPath)
116
+
117
+ const diff = targetPath
118
+ ? computeLineDiff(fromItem?.content || '', toItem?.content || '')
119
+ : []
120
+
121
+ writeJson(res, 200, {
122
+ ok: true,
123
+ from,
124
+ to,
125
+ files: allPaths,
126
+ selectedFile: targetPath,
127
+ fromSize: fromItem?.size || 0,
128
+ toSize: toItem?.size || 0,
129
+ diff,
130
+ })
131
+ } catch (err) { writeJson(res, 500, { ok: false, error: err?.message || String(err) }) }
132
+ },
133
+ })
134
+ }, 'dsh-moa: candidate diff route')
135
+
136
+ // Route: /dsh-moa/history/export
137
+
138
+ ctx.effect(() => {
139
+ return ctx.webServer.register({
140
+ kind: 'prefix',
141
+ path: '/dsh-moa/runs',
142
+ handler: (req, res) => {
143
+ if (!assertAuthorizedRequest(req, res, ctx)) return
144
+ const urlPath = req.url.split('?')[0]
145
+ const parts = urlPath.split('/').filter(Boolean)
146
+ const runId = parts[2] || ''
147
+ if (parts[3] === 'workbench') {
148
+ const targetCwd = workspaceRoot || ctx?.reflect?.get?.('baseDir', false) || ctx?.reflect?.get?.('workspaceDir', false) || process.cwd()
149
+ if (req.method === 'GET') {
150
+ return handleWorkbenchGet(req, res, { runId, cwd: targetCwd })
151
+ }
152
+ if (req.method === 'POST') {
153
+ if (!isSafeWriteRequest(req)) {
154
+ return writeJson(res, 403, { ok: false, error: 'Forbidden: cross-site request rejected' })
155
+ }
156
+ return handleWorkbenchAssemble(req, res, { runId, cwd: targetCwd, live, callLlm, webServerPort, timeMachineEngine })
157
+ }
158
+ return writeJson(res, 405, { ok: false, error: 'Method not allowed' })
159
+ }
160
+ if (req.method !== 'GET') return writeJson(res, 405, { ok: false, error: 'GET required' })
161
+ const isReport = parts[3] === 'report'
162
+ const run = runId ? getMoaRunById(runId) : null
163
+ if (!run) {
164
+ writeJson(res, 404, { ok: false, error: 'Run not found' })
165
+ return
166
+ }
167
+ if (isReport) {
168
+ const u = new URL(req.url, 'http://127.0.0.1')
169
+ const format = u.searchParams.get('format') || 'md'
170
+ const report = run.benchmarkReport || generateMoABenchmarkReport({
171
+ runId: run.id,
172
+ timestamp: run.timestamp,
173
+ preset: run.preset,
174
+ prompt: run.prompt,
175
+ durationMs: run.durationMs,
176
+ usage: { totalTokens: run.totalTokens },
177
+ costUsd: run.totalCostUsd,
178
+ candidates: run.candidates,
179
+ winningIndex: run.winnerIndex,
180
+ winningLabel: run.winnerModel,
181
+ })
182
+ if (format === 'json') {
183
+ writeJson(res, 200, { ok: true, report: typeof report === 'object' ? (report.json || report) : report })
184
+ return
185
+ }
186
+ const mdText = typeof report === 'string' ? report : (report.markdown || JSON.stringify(report, null, 2))
187
+ res.writeHead(200, { 'Content-Type': 'text/markdown; charset=utf-8' })
188
+ res.end(mdText)
189
+ return
190
+ }
191
+ writeJson(res, 200, { ok: true, run })
192
+ },
193
+ })
194
+ }, 'dsh-moa: run by id route')
195
+
196
+ // Route: /dsh-moa/route-preset
197
+ }