@goodandready/dsh-moa 0.2.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/LICENSE +21 -0
- package/README.md +120 -0
- package/README.ru.md +83 -0
- package/cordis.patch.yml +4 -0
- package/lib/client.js +1090 -0
- package/lib/file-workspace.js +186 -0
- package/lib/index.js +494 -0
- package/lib/moa-runner.js +672 -0
- package/package.json +62 -0
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import fs from 'node:fs/promises'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Parses markdown code blocks that specify file paths.
|
|
6
|
+
* Supports patterns like:
|
|
7
|
+
* ```html file="index.html"
|
|
8
|
+
* ```javascript filepath=script.js
|
|
9
|
+
* // File: index.html
|
|
10
|
+
* <!-- File: styles.css -->
|
|
11
|
+
* ### File: index.html
|
|
12
|
+
* ```html
|
|
13
|
+
*/
|
|
14
|
+
export function extractFileBlocks(text) {
|
|
15
|
+
if (!text || typeof text !== 'string') return []
|
|
16
|
+
|
|
17
|
+
const files = []
|
|
18
|
+
const seenPaths = new Set()
|
|
19
|
+
|
|
20
|
+
// Pattern 1: fenced blocks with file="..." or path="..." in info string
|
|
21
|
+
// Example: ```html file="index.html" or ```js path=src/main.js
|
|
22
|
+
const fenceRegex = /```[\w-]*\s+(?:file|path|filepath)=["']?([^\s"'\n\r]+)["']?[\r\n]+([\s\S]*?)```/gi
|
|
23
|
+
let match
|
|
24
|
+
while ((match = fenceRegex.exec(text)) !== null) {
|
|
25
|
+
const rawPath = match[1].trim()
|
|
26
|
+
const content = match[2]
|
|
27
|
+
if (rawPath && !seenPaths.has(rawPath)) {
|
|
28
|
+
seenPaths.add(rawPath)
|
|
29
|
+
files.push({ relativePath: sanitizePath(rawPath), content })
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Pattern 2: Explicit headers before code blocks, e.g.
|
|
34
|
+
// **File: `index.html`** or ### File: index.html or `File: index.html` followed by ```
|
|
35
|
+
const headerRegex = /(?:###?\s*File:\s*|File:\s*|#\s*File:\s*)[`"]?([a-zA-Z0-9_.\-\/\\]+)[`"]?\s*[\r\n]+```[\w-]*[\r\n]+([\s\S]*?)```/gi
|
|
36
|
+
while ((match = headerRegex.exec(text)) !== null) {
|
|
37
|
+
const rawPath = match[1].trim()
|
|
38
|
+
const content = match[2]
|
|
39
|
+
if (rawPath && !seenPaths.has(rawPath)) {
|
|
40
|
+
seenPaths.add(rawPath)
|
|
41
|
+
files.push({ relativePath: sanitizePath(rawPath), content })
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Pattern 3: Comment inside the code block on line 1: // File: foo.js or /* File: foo.css */ or <!-- File: index.html -->
|
|
46
|
+
const commentRegex = /```[\w-]*[\r\n]+(?:\/\/|\/\*|<!--|#)\s*(?:File|filename):\s*([a-zA-Z0-9_.\-\/\\]+)(?:\s*\*\/|\s*-->)?[\r\n]+([\s\S]*?)```/gi
|
|
47
|
+
while ((match = commentRegex.exec(text)) !== null) {
|
|
48
|
+
const rawPath = match[1].trim()
|
|
49
|
+
const content = match[2]
|
|
50
|
+
if (rawPath && !seenPaths.has(rawPath)) {
|
|
51
|
+
seenPaths.add(rawPath)
|
|
52
|
+
files.push({ relativePath: sanitizePath(rawPath), content })
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Fallback: If no annotated blocks found, check if text has exactly one prominent standalone code block with common extensions
|
|
57
|
+
if (files.length === 0) {
|
|
58
|
+
const singleBlockRegex = /```(html|css|javascript|js|json|python|py|sh|bash)[\r\n]+([\s\S]*?)```/gi
|
|
59
|
+
const singleMatches = []
|
|
60
|
+
while ((match = singleBlockRegex.exec(text)) !== null) {
|
|
61
|
+
singleMatches.push({ lang: match[1].toLowerCase(), content: match[2] })
|
|
62
|
+
}
|
|
63
|
+
if (singleMatches.length === 1) {
|
|
64
|
+
const extMap = {
|
|
65
|
+
html: 'index.html',
|
|
66
|
+
javascript: 'index.js',
|
|
67
|
+
js: 'index.js',
|
|
68
|
+
css: 'style.css',
|
|
69
|
+
python: 'main.py',
|
|
70
|
+
py: 'main.py',
|
|
71
|
+
json: 'data.json',
|
|
72
|
+
bash: 'run.sh',
|
|
73
|
+
sh: 'run.sh',
|
|
74
|
+
}
|
|
75
|
+
const defaultName = extMap[singleMatches[0].lang] || 'output.txt'
|
|
76
|
+
files.push({ relativePath: defaultName, content: singleMatches[0].content })
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return files
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function sanitizePath(filePath) {
|
|
84
|
+
const normalized = path.normalize(filePath).replace(/^(\.\.[\/\\])+/, '')
|
|
85
|
+
return normalized.replace(/^[\\\/]+/, '')
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Writes candidate files into an isolated candidate workspace:
|
|
90
|
+
* <baseDir>/.moa/candidate-<index>/
|
|
91
|
+
*/
|
|
92
|
+
export async function writeCandidateWorkspace(baseDir, candidateIndex, files = []) {
|
|
93
|
+
if (!baseDir || !Array.isArray(files) || files.length === 0) return []
|
|
94
|
+
|
|
95
|
+
const candidateDir = path.join(baseDir, '.moa', `candidate-${candidateIndex}`)
|
|
96
|
+
await fs.mkdir(candidateDir, { recursive: true })
|
|
97
|
+
|
|
98
|
+
const written = []
|
|
99
|
+
for (const file of files) {
|
|
100
|
+
const fullPath = path.join(candidateDir, file.relativePath)
|
|
101
|
+
await fs.mkdir(path.dirname(fullPath), { recursive: true })
|
|
102
|
+
await fs.writeFile(fullPath, file.content, 'utf8')
|
|
103
|
+
written.push({
|
|
104
|
+
relativePath: file.relativePath,
|
|
105
|
+
fullPath,
|
|
106
|
+
size: Buffer.byteLength(file.content, 'utf8'),
|
|
107
|
+
})
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return written
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Promotes winning candidate files to baseDir root,
|
|
115
|
+
* then cleans up the temporary .moa folder.
|
|
116
|
+
*/
|
|
117
|
+
export async function promoteCandidateWorkspace(baseDir, winningIndex) {
|
|
118
|
+
if (!baseDir || winningIndex === null || winningIndex === undefined) return []
|
|
119
|
+
|
|
120
|
+
const winningDir = path.join(baseDir, '.moa', `candidate-${winningIndex}`)
|
|
121
|
+
const moaRoot = path.join(baseDir, '.moa')
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
const entries = await getFilesRecursively(winningDir)
|
|
125
|
+
if (entries.length === 0) {
|
|
126
|
+
try { await fs.rm(moaRoot, { recursive: true, force: true }) } catch {}
|
|
127
|
+
return []
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const promoted = []
|
|
131
|
+
for (const rel of entries) {
|
|
132
|
+
const srcPath = path.join(winningDir, rel)
|
|
133
|
+
const destPath = path.join(baseDir, rel)
|
|
134
|
+
|
|
135
|
+
await fs.mkdir(path.dirname(destPath), { recursive: true })
|
|
136
|
+
await fs.copyFile(srcPath, destPath)
|
|
137
|
+
promoted.push(rel)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Clean up temporary .moa folder
|
|
141
|
+
await fs.rm(moaRoot, { recursive: true, force: true })
|
|
142
|
+
|
|
143
|
+
return promoted
|
|
144
|
+
} catch (err) {
|
|
145
|
+
try { await fs.rm(moaRoot, { recursive: true, force: true }) } catch {}
|
|
146
|
+
return []
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Cleans up any left-over .moa candidate folder.
|
|
152
|
+
*/
|
|
153
|
+
export async function cleanMoaWorkspaces(baseDir) {
|
|
154
|
+
if (!baseDir) return
|
|
155
|
+
const moaRoot = path.join(baseDir, '.moa')
|
|
156
|
+
try {
|
|
157
|
+
await fs.rm(moaRoot, { recursive: true, force: true })
|
|
158
|
+
} catch {}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async function getFilesRecursively(dir) {
|
|
162
|
+
const results = []
|
|
163
|
+
try {
|
|
164
|
+
const stat = await fs.stat(dir)
|
|
165
|
+
if (!stat.isDirectory()) return []
|
|
166
|
+
} catch {
|
|
167
|
+
return []
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async function scan(current, relPrefix = '') {
|
|
171
|
+
try {
|
|
172
|
+
const list = await fs.readdir(current, { withFileTypes: true })
|
|
173
|
+
for (const item of list) {
|
|
174
|
+
const rel = relPrefix ? `${relPrefix}/${item.name}` : item.name
|
|
175
|
+
const full = path.join(current, item.name)
|
|
176
|
+
if (item.isDirectory()) {
|
|
177
|
+
await scan(full, rel)
|
|
178
|
+
} else if (item.isFile()) {
|
|
179
|
+
results.push(rel)
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
} catch {}
|
|
183
|
+
}
|
|
184
|
+
await scan(dir)
|
|
185
|
+
return results
|
|
186
|
+
}
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
import os from 'node:os'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import z from '@deepseek-ai/schemastery'
|
|
4
|
+
import { runMoAPipeline, parseMoACommand, formatMoAResponse, MoaRunnerAdapter } from './moa-runner.js'
|
|
5
|
+
|
|
6
|
+
export const name = '@goodandready/dsh-moa'
|
|
7
|
+
export const inject = ['settings', 'webServer', 'llm', 'credentials', 'sessions', 'agents']
|
|
8
|
+
|
|
9
|
+
export { parseMoACommand }
|
|
10
|
+
|
|
11
|
+
export const ModelSlotSchema = z.object({
|
|
12
|
+
provider: z.string().default(''),
|
|
13
|
+
model: z.string().default(''),
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
export const PresetSchema = z.object({
|
|
17
|
+
name: z.string().default('default'),
|
|
18
|
+
enabled: z.boolean().default(true),
|
|
19
|
+
reference_models: z.array(ModelSlotSchema).default([
|
|
20
|
+
{ provider: 'opencode-go', model: 'deepseek-v4-flash' },
|
|
21
|
+
{ provider: 'codex', model: 'gpt-5.6-sol' },
|
|
22
|
+
]),
|
|
23
|
+
aggregator: ModelSlotSchema.default({
|
|
24
|
+
provider: 'antigravity',
|
|
25
|
+
model: 'gemini-3-flash',
|
|
26
|
+
}),
|
|
27
|
+
reference_temperature: z.number().step(0.05).min(0).max(2).default(0.6),
|
|
28
|
+
aggregator_temperature: z.number().step(0.05).min(0).max(2).default(0.4),
|
|
29
|
+
max_tokens: z.number().min(256).max(65536).default(4096),
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
export const Config = z.object({
|
|
33
|
+
enabled: z.boolean().default(true),
|
|
34
|
+
default_preset: z.string().default('default'),
|
|
35
|
+
presets: z.array(PresetSchema).default([
|
|
36
|
+
{
|
|
37
|
+
name: 'default',
|
|
38
|
+
enabled: true,
|
|
39
|
+
reference_models: [
|
|
40
|
+
{ provider: 'opencode-go', model: 'deepseek-v4-flash' },
|
|
41
|
+
{ provider: 'codex', model: 'gpt-5.6-sol' },
|
|
42
|
+
],
|
|
43
|
+
aggregator: {
|
|
44
|
+
provider: 'antigravity',
|
|
45
|
+
model: 'gemini-3-flash',
|
|
46
|
+
},
|
|
47
|
+
reference_temperature: 0.6,
|
|
48
|
+
aggregator_temperature: 0.4,
|
|
49
|
+
max_tokens: 4096,
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
name: 'deep-reasoning',
|
|
53
|
+
enabled: true,
|
|
54
|
+
reference_models: [
|
|
55
|
+
{ provider: 'grok', model: 'grok-4.20-0309-reasoning' },
|
|
56
|
+
{ provider: 'codex', model: 'gpt-5.6-terra' },
|
|
57
|
+
],
|
|
58
|
+
aggregator: {
|
|
59
|
+
provider: 'antigravity',
|
|
60
|
+
model: 'gemini-3-flash',
|
|
61
|
+
},
|
|
62
|
+
reference_temperature: 0.7,
|
|
63
|
+
aggregator_temperature: 0.2,
|
|
64
|
+
max_tokens: 8192,
|
|
65
|
+
},
|
|
66
|
+
]),
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
const NS = 'dsh-moa'
|
|
70
|
+
|
|
71
|
+
function writeJson(res, code, data) {
|
|
72
|
+
try {
|
|
73
|
+
res.writeHead(code, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' })
|
|
74
|
+
res.end(JSON.stringify(data))
|
|
75
|
+
} catch {
|
|
76
|
+
/* socket closed */
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function readBody(req) {
|
|
81
|
+
return new Promise((resolve, reject) => {
|
|
82
|
+
const chunks = []
|
|
83
|
+
req.on('data', (c) => chunks.push(c))
|
|
84
|
+
req.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')))
|
|
85
|
+
req.on('error', reject)
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
export function apply(ctx, config) {
|
|
91
|
+
let settingsApi = null
|
|
92
|
+
let currentConfig = structuredClone(config)
|
|
93
|
+
|
|
94
|
+
// Try reading persisted config from ~/.dsh/settings.yaml on load
|
|
95
|
+
try {
|
|
96
|
+
const fs = require('node:fs')
|
|
97
|
+
const yaml = require('yaml')
|
|
98
|
+
const p = path.join(os.homedir(), '.dsh', 'settings.yaml')
|
|
99
|
+
if (fs.existsSync(p)) {
|
|
100
|
+
const parsed = yaml.parse(fs.readFileSync(p, 'utf8'))
|
|
101
|
+
if (parsed && parsed[NS]) {
|
|
102
|
+
currentConfig = Config(parsed[NS])
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
} catch {}
|
|
106
|
+
|
|
107
|
+
ctx.inject(['settings'], (sctx) => {
|
|
108
|
+
const scope = sctx.settings.register(NS, Config, { base: currentConfig })
|
|
109
|
+
settingsApi = scope
|
|
110
|
+
const val = scope.get()
|
|
111
|
+
if (val && Object.keys(val).length > 0) {
|
|
112
|
+
currentConfig = val
|
|
113
|
+
}
|
|
114
|
+
sctx.effect(() => () => {
|
|
115
|
+
settingsApi = null
|
|
116
|
+
})
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
const getConfig = () => {
|
|
120
|
+
if (settingsApi) {
|
|
121
|
+
const live = settingsApi.get()
|
|
122
|
+
if (live && live.presets && live.presets.length) return live
|
|
123
|
+
}
|
|
124
|
+
return currentConfig || config
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Canonical collector for ctx.llm.stream chunk streams
|
|
128
|
+
const collectStreamText = async (iterable) => {
|
|
129
|
+
let out = ''
|
|
130
|
+
let sawDelta = false
|
|
131
|
+
for await (const chunk of iterable) {
|
|
132
|
+
if (chunk && chunk.type === 'text-delta' && typeof chunk.text === 'string') {
|
|
133
|
+
out += chunk.text
|
|
134
|
+
sawDelta = true
|
|
135
|
+
} else if (
|
|
136
|
+
!sawDelta && chunk && chunk.type === 'block-end' &&
|
|
137
|
+
chunk.block && chunk.block.type === 'text' && typeof chunk.block.text === 'string'
|
|
138
|
+
) {
|
|
139
|
+
out += chunk.block.text
|
|
140
|
+
} else if (chunk && chunk.type === 'finish' && chunk.reason && chunk.reason.kind === 'error') {
|
|
141
|
+
throw new Error(chunk.reason.failure?.message || 'LLM stream failed')
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return out.trim()
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Adapter to ctx.llm.stream
|
|
148
|
+
const callLlm = async (params) => {
|
|
149
|
+
if (!ctx.llm || typeof ctx.llm.stream !== 'function') {
|
|
150
|
+
throw new Error('ctx.llm.stream service is not available')
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const { provider, model, messages, temperature, maxTokens, signal } = params
|
|
154
|
+
|
|
155
|
+
// Convert string/simple role messages to format expected by ctx.llm.stream
|
|
156
|
+
// Each message must have role, content (array of blocks or string), and source
|
|
157
|
+
const formattedMessages = (messages || []).map((m) => {
|
|
158
|
+
let content = m.content
|
|
159
|
+
if (typeof content === 'string') {
|
|
160
|
+
content = [{ type: 'text', text: content }]
|
|
161
|
+
} else if (Array.isArray(content)) {
|
|
162
|
+
content = content.map((b) => (typeof b === 'string' ? { type: 'text', text: b } : b))
|
|
163
|
+
} else {
|
|
164
|
+
content = [{ type: 'text', text: String(content || '') }]
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const role = m.role || 'user'
|
|
168
|
+
const source = m.source || (role === 'assistant' ? { kind: 'model', provider, model } : { kind: 'user' })
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
role,
|
|
172
|
+
content,
|
|
173
|
+
source,
|
|
174
|
+
}
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
// Provider-specific parameter cleansing (Codex / reasoning models reject temperature, max_output_tokens, etc.)
|
|
178
|
+
const isCodexOrReasoning = provider === 'codex' || String(model).includes('sol') || String(model).includes('reasoner')
|
|
179
|
+
const streamOpts = {
|
|
180
|
+
provider,
|
|
181
|
+
model,
|
|
182
|
+
messages: formattedMessages,
|
|
183
|
+
...(signal ? { signal } : {}),
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (!isCodexOrReasoning) {
|
|
187
|
+
if (temperature !== undefined) streamOpts.temperature = temperature
|
|
188
|
+
if (maxTokens !== undefined) streamOpts.maxTokens = maxTokens
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const chunks = ctx.llm.stream(streamOpts)
|
|
192
|
+
|
|
193
|
+
return await collectStreamText(chunks)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// HTTP endpoints
|
|
197
|
+
ctx.effect(() => {
|
|
198
|
+
return ctx.webServer.register({
|
|
199
|
+
kind: 'exact',
|
|
200
|
+
path: '/dsh-moa/status',
|
|
201
|
+
handler: (_req, res) => {
|
|
202
|
+
const cfg = getConfig()
|
|
203
|
+
writeJson(res, 200, {
|
|
204
|
+
ok: true,
|
|
205
|
+
enabled: cfg.enabled !== false,
|
|
206
|
+
defaultPreset: cfg.default_preset || 'default',
|
|
207
|
+
hasCtxLlm: Boolean(ctx.llm),
|
|
208
|
+
llmKeys: ctx.llm ? Object.keys(ctx.llm) : [],
|
|
209
|
+
llmProto: ctx.llm ? Object.getOwnPropertyNames(Object.getPrototypeOf(ctx.llm)) : [],
|
|
210
|
+
presetsCount: (cfg.presets || []).length,
|
|
211
|
+
})
|
|
212
|
+
},
|
|
213
|
+
})
|
|
214
|
+
}, 'dsh-moa: status route')
|
|
215
|
+
|
|
216
|
+
ctx.effect(() => {
|
|
217
|
+
return ctx.webServer.register({
|
|
218
|
+
kind: 'exact',
|
|
219
|
+
path: '/dsh-moa/models',
|
|
220
|
+
handler: async (_req, res) => {
|
|
221
|
+
const result = []
|
|
222
|
+
const seen = new Set()
|
|
223
|
+
|
|
224
|
+
const add = (prov, mod, label) => {
|
|
225
|
+
if (!prov || !mod) return
|
|
226
|
+
const p = String(prov).trim()
|
|
227
|
+
const m = String(mod).trim()
|
|
228
|
+
if (!p || !m) return
|
|
229
|
+
const key = `${p}:${m}`
|
|
230
|
+
if (seen.has(key)) return
|
|
231
|
+
seen.add(key)
|
|
232
|
+
result.push({ provider: p, model: m, label: label || key })
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// ONLY query REAL models from runtime adapters & settings. NO hardcoded fallbacks!
|
|
236
|
+
if (ctx.llm) {
|
|
237
|
+
try {
|
|
238
|
+
if (typeof ctx.llm.listProviders === 'function') {
|
|
239
|
+
const provList = ctx.llm.listProviders() || []
|
|
240
|
+
for (const p of provList) {
|
|
241
|
+
const pId = typeof p === 'string' ? p : (p.id || p.provider || p.name)
|
|
242
|
+
if (!pId) continue
|
|
243
|
+
if (typeof ctx.llm.listModels === 'function') {
|
|
244
|
+
try {
|
|
245
|
+
const mList = await ctx.llm.listModels(pId)
|
|
246
|
+
for (const m of (mList || [])) {
|
|
247
|
+
const mId = typeof m === 'string' ? m : (m.id || m.model || m.name)
|
|
248
|
+
const mLabel = m.displayName || m.name || mId
|
|
249
|
+
if (mId) add(pId, mId, `${pId}: ${mLabel}`)
|
|
250
|
+
}
|
|
251
|
+
} catch {}
|
|
252
|
+
}
|
|
253
|
+
for (const m of (p.models || [])) {
|
|
254
|
+
const mId = typeof m === 'string' ? m : (m.id || m.model || m.name)
|
|
255
|
+
if (mId) add(pId, mId)
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
} catch (e) {
|
|
260
|
+
console.warn('[dsh-moa] ctx.llm.listProviders error:', e)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
try {
|
|
264
|
+
if (typeof ctx.llm.listConfigurableProviders === 'function') {
|
|
265
|
+
const cProvs = ctx.llm.listConfigurableProviders() || []
|
|
266
|
+
for (const cp of cProvs) {
|
|
267
|
+
const cpId = cp.provider || cp.id
|
|
268
|
+
if (cpId && typeof ctx.llm.listModels === 'function') {
|
|
269
|
+
try {
|
|
270
|
+
const mList = await ctx.llm.listModels(cpId)
|
|
271
|
+
for (const m of (mList || [])) {
|
|
272
|
+
const mId = typeof m === 'string' ? m : (m.id || m.model || m.name)
|
|
273
|
+
if (mId) add(cpId, mId)
|
|
274
|
+
}
|
|
275
|
+
} catch {}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
} catch (e) {
|
|
280
|
+
console.warn('[dsh-moa] ctx.llm.listConfigurableProviders error:', e)
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Query settings for configured providers
|
|
285
|
+
try {
|
|
286
|
+
const s = ctx.get ? ctx.get('settings') : ctx.settings
|
|
287
|
+
const getSetting = (k) => {
|
|
288
|
+
try { return s?.get?.(k) } catch { return undefined }
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const def = getSetting('agent-default-model')
|
|
292
|
+
if (def?.provider && def?.model) {
|
|
293
|
+
add(def.provider, def.model)
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const piAi = getSetting('llm-pi-ai')
|
|
297
|
+
for (const [pName, pData] of Object.entries(piAi?.providers || {})) {
|
|
298
|
+
for (const m of (pData?.models || [])) {
|
|
299
|
+
const mId = typeof m === 'string' ? m : (m?.id || m?.model)
|
|
300
|
+
if (mId) add(pName, mId)
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const sync = getSetting('dsh-model-sync')
|
|
305
|
+
for (const [syncProv, syncModels] of Object.entries(sync?.modelCatalogs || {})) {
|
|
306
|
+
for (const m of (syncModels || [])) {
|
|
307
|
+
const mId = typeof m === 'string' ? m : (m?.id || m?.model)
|
|
308
|
+
if (mId) add(syncProv, mId)
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
} catch (e) {
|
|
312
|
+
console.warn('[dsh-moa] Error reading settings:', e)
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
writeJson(res, 200, { ok: true, models: result })
|
|
316
|
+
},
|
|
317
|
+
})
|
|
318
|
+
}, 'dsh-moa: models route')
|
|
319
|
+
|
|
320
|
+
ctx.effect(() => {
|
|
321
|
+
return ctx.webServer.register({
|
|
322
|
+
kind: 'exact',
|
|
323
|
+
path: '/dsh-moa/presets',
|
|
324
|
+
handler: async (req, res) => {
|
|
325
|
+
const cfg = getConfig()
|
|
326
|
+
if (req.method === 'GET') {
|
|
327
|
+
writeJson(res, 200, {
|
|
328
|
+
ok: true,
|
|
329
|
+
defaultPreset: cfg.default_preset || 'default',
|
|
330
|
+
presets: cfg.presets || [],
|
|
331
|
+
})
|
|
332
|
+
return
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (req.method === 'POST' || req.method === 'PUT') {
|
|
336
|
+
try {
|
|
337
|
+
const raw = await readBody(req)
|
|
338
|
+
const payload = JSON.parse(raw)
|
|
339
|
+
const newConfig = {
|
|
340
|
+
enabled: cfg.enabled !== false,
|
|
341
|
+
default_preset: payload.default_preset || cfg.default_preset || 'default',
|
|
342
|
+
presets: Array.isArray(payload.presets) ? payload.presets : (cfg.presets || []),
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// 1. Persist via Cordis Settings API
|
|
346
|
+
if (settingsApi && typeof settingsApi.replace === 'function') {
|
|
347
|
+
await settingsApi.replace(newConfig)
|
|
348
|
+
}
|
|
349
|
+
currentConfig = newConfig
|
|
350
|
+
|
|
351
|
+
// 2. Direct persistence to ~/.dsh/settings.yaml
|
|
352
|
+
try {
|
|
353
|
+
const fs = await import('node:fs')
|
|
354
|
+
const yaml = await import('yaml')
|
|
355
|
+
const p = path.join(os.homedir(), '.dsh', 'settings.yaml')
|
|
356
|
+
if (fs.existsSync(p)) {
|
|
357
|
+
const curYaml = yaml.parse(fs.readFileSync(p, 'utf8')) || {}
|
|
358
|
+
curYaml[NS] = newConfig
|
|
359
|
+
fs.writeFileSync(p, yaml.stringify(curYaml), 'utf8')
|
|
360
|
+
}
|
|
361
|
+
} catch (fsErr) {
|
|
362
|
+
console.warn('[dsh-moa] direct file write error:', fsErr)
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
writeJson(res, 200, {
|
|
366
|
+
ok: true,
|
|
367
|
+
defaultPreset: newConfig.default_preset,
|
|
368
|
+
presets: newConfig.presets,
|
|
369
|
+
})
|
|
370
|
+
} catch (err) {
|
|
371
|
+
writeJson(res, 400, { ok: false, error: err?.message || String(err) })
|
|
372
|
+
}
|
|
373
|
+
return
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
writeJson(res, 405, { ok: false, error: 'Method Not Allowed' })
|
|
377
|
+
},
|
|
378
|
+
})
|
|
379
|
+
}, 'dsh-moa: presets route')
|
|
380
|
+
|
|
381
|
+
ctx.effect(() => {
|
|
382
|
+
return ctx.webServer.register({
|
|
383
|
+
kind: 'exact',
|
|
384
|
+
path: '/dsh-moa/run',
|
|
385
|
+
handler: async (req, res) => {
|
|
386
|
+
if (req.method !== 'POST') {
|
|
387
|
+
writeJson(res, 405, { ok: false, error: 'POST required' })
|
|
388
|
+
return
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
try {
|
|
392
|
+
const raw = await readBody(req)
|
|
393
|
+
const payload = JSON.parse(raw)
|
|
394
|
+
const prompt = payload.prompt || ''
|
|
395
|
+
if (!prompt.trim()) {
|
|
396
|
+
writeJson(res, 400, { ok: false, error: 'Empty prompt' })
|
|
397
|
+
return
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const cfg = getConfig()
|
|
401
|
+
const presetName = payload.preset || cfg.default_preset || 'default'
|
|
402
|
+
const preset = (cfg.presets || []).find((p) => p.name === presetName) || cfg.presets?.[0]
|
|
403
|
+
|
|
404
|
+
const result = await runMoAPipeline({
|
|
405
|
+
userPrompt: prompt,
|
|
406
|
+
messages: payload.messages || [],
|
|
407
|
+
preset,
|
|
408
|
+
callLlm,
|
|
409
|
+
cwd: payload.cwd || process.cwd(),
|
|
410
|
+
})
|
|
411
|
+
|
|
412
|
+
writeJson(res, 200, { ok: true, ...result })
|
|
413
|
+
} catch (err) {
|
|
414
|
+
writeJson(res, 500, { ok: false, error: err?.message || String(err) })
|
|
415
|
+
}
|
|
416
|
+
},
|
|
417
|
+
})
|
|
418
|
+
}, 'dsh-moa: run pipeline route')
|
|
419
|
+
|
|
420
|
+
// Register virtual LLM adapter for live-streaming MoA turns
|
|
421
|
+
if (ctx.llm && typeof ctx.llm.registerAdapter === 'function') {
|
|
422
|
+
ctx.effect(() => {
|
|
423
|
+
const adapter = new MoaRunnerAdapter((options) => {
|
|
424
|
+
const msgs = options.messages || []
|
|
425
|
+
const lastUser = [...msgs].reverse().find((m) => m && m.role === 'user')
|
|
426
|
+
const userText = typeof lastUser?.content === 'string'
|
|
427
|
+
? lastUser.content
|
|
428
|
+
: (Array.isArray(lastUser?.content)
|
|
429
|
+
? lastUser.content.filter((p) => p.type === 'text').map((p) => p.text).join('\n')
|
|
430
|
+
: '')
|
|
431
|
+
|
|
432
|
+
const cfg = getConfig()
|
|
433
|
+
const parsed = parseMoACommand(userText, cfg.presets || []) || { prompt: userText, presetName: 'default' }
|
|
434
|
+
const targetPreset = (cfg.presets || []).find((p) => p.name === parsed.presetName) || cfg.presets?.[0]
|
|
435
|
+
|
|
436
|
+
const extraCtx = options.signal ? moaPendingContexts.get(options.signal) : null
|
|
437
|
+
const cwd = options.cwd || extraCtx?.cwd || process.cwd()
|
|
438
|
+
|
|
439
|
+
return {
|
|
440
|
+
targetPreset,
|
|
441
|
+
userPrompt: parsed.prompt || userText,
|
|
442
|
+
messages: msgs.slice(0, -1),
|
|
443
|
+
callLlm,
|
|
444
|
+
cwd,
|
|
445
|
+
}
|
|
446
|
+
})
|
|
447
|
+
|
|
448
|
+
const dispose = ctx.llm.registerAdapter(['moa-runner'], adapter)
|
|
449
|
+
return () => {
|
|
450
|
+
if (typeof dispose === 'function') dispose()
|
|
451
|
+
}
|
|
452
|
+
}, 'dsh-moa: llm adapter')
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Session turn interceptor: flag MoA turns and route to moa-runner adapter
|
|
456
|
+
const moaPendingSignals = new WeakSet()
|
|
457
|
+
const moaPendingContexts = new WeakMap()
|
|
458
|
+
|
|
459
|
+
ctx.on('agent/pre-step', async (event, next) => {
|
|
460
|
+
const messages = event.messages || []
|
|
461
|
+
const lastUserMsg = [...messages].reverse().find((m) => m && m.role === 'user')
|
|
462
|
+
const userText = typeof lastUserMsg?.content === 'string'
|
|
463
|
+
? lastUserMsg.content.trim()
|
|
464
|
+
: (Array.isArray(lastUserMsg?.content)
|
|
465
|
+
? lastUserMsg.content.filter((p) => p.type === 'text').map((p) => p.text).join('\n').trim()
|
|
466
|
+
: '')
|
|
467
|
+
|
|
468
|
+
if (userText.startsWith('/moa')) {
|
|
469
|
+
const cfg = getConfig()
|
|
470
|
+
if (cfg.enabled !== false) {
|
|
471
|
+
if (event.signal) {
|
|
472
|
+
moaPendingSignals.add(event.signal)
|
|
473
|
+
const sessCwd = event.cwd || event.session?.cwd || (event.session?.path ? path.dirname(event.session.path) : undefined)
|
|
474
|
+
moaPendingContexts.set(event.signal, { cwd: sessCwd })
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
return next()
|
|
480
|
+
})
|
|
481
|
+
|
|
482
|
+
ctx.on('agent/request', async (payload, next) => {
|
|
483
|
+
const resolved = await next()
|
|
484
|
+
if (payload.signal && moaPendingSignals.has(payload.signal)) {
|
|
485
|
+
moaPendingSignals.delete(payload.signal)
|
|
486
|
+
return {
|
|
487
|
+
...resolved,
|
|
488
|
+
provider: 'moa-runner',
|
|
489
|
+
model: 'ensemble',
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
return resolved
|
|
493
|
+
})
|
|
494
|
+
}
|