@goodandready/dsh-moa 0.2.29 → 0.2.30
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 +12 -12
- package/README.ru.md +12 -12
- package/lib/file-workspace.js +23 -2
- package/lib/history.js +41 -11
- package/lib/index.js +17 -15
- package/lib/moa-router.js +2 -2
- package/lib/moa-runner.js +21 -7
- package/lib/moa-stream.js +4 -1
- package/lib/pricing.js +25 -7
- package/lib/routes.js +6 -1
- package/lib/updater.js +96 -5
- package/package.json +3 -4
package/README.md
CHANGED
|
@@ -151,8 +151,8 @@ When `multi_judge_enabled: true`, candidate solutions are independently evaluate
|
|
|
151
151
|
### 11. Composite Hybrid Synthesis (AST / Block Merge)
|
|
152
152
|
When `composite_merge_enabled: true`, the aggregator assembles a modular hybrid: combining the strongest core logic from one model, robust error handling from another, and complete types/tests from a third.
|
|
153
153
|
|
|
154
|
-
### 12. Smart Dynamic Preset Router &
|
|
155
|
-
When invoking `/moa` without explicit preset flags, the router classifies prompt intent using keyword heuristics or a zero-shot
|
|
154
|
+
### 12. Smart Dynamic Preset Router & LLM Classifier
|
|
155
|
+
When invoking `/moa` without explicit preset flags, the router classifies prompt intent using keyword heuristics or a zero-shot router model (`smart_routing_model`) to automatically select the optimal preset.
|
|
156
156
|
|
|
157
157
|
### 13. Real-Time Live Cost Counter & Streaming Ticker
|
|
158
158
|
As each candidate completes, live token counts and USD costs are streamed directly into the chat based on catalog pricing and vendor rates.
|
|
@@ -195,16 +195,16 @@ v0.2.13 introduces 10 ready-to-use presets engineered for real-world software wo
|
|
|
195
195
|
|
|
196
196
|
| Preset Name | Purpose | Default Aggregator | Peer Critique | Blind Eval |
|
|
197
197
|
| :--- | :--- | :--- | :---: | :---: |
|
|
198
|
-
| `default` | Balanced multi-model generation |
|
|
199
|
-
| `code-review` | Thorough peer review & vulnerability detection |
|
|
200
|
-
| `fast-audit` | Ultra-fast single-model audit (Fast Mode) |
|
|
201
|
-
| `deep-architect` | Distributed systems & complex architectures |
|
|
202
|
-
| `bug-hunter` | Root cause discovery & adversarial edge cases |
|
|
203
|
-
| `refactor-cleanup` | Dead-code pruning & standard-library simplicity |
|
|
204
|
-
| `frontend-ui` | High-fidelity responsive web interfaces |
|
|
205
|
-
| `security-audit` | Zero-trust threat analysis & sanitization |
|
|
206
|
-
| `math-logic` | Deterministic algorithmic proofs & math logic |
|
|
207
|
-
| `creative-brainstorm`| Divergent lateral thinking & ideation |
|
|
198
|
+
| `default` | Balanced multi-model generation | `<provider>:<model>` | Optional | Off |
|
|
199
|
+
| `code-review` | Thorough peer review & vulnerability detection | `<provider>:<model>` | On | On |
|
|
200
|
+
| `fast-audit` | Ultra-fast single-model audit (Fast Mode) | `<provider>:<model>` | Off | Off |
|
|
201
|
+
| `deep-architect` | Distributed systems & complex architectures | `<provider>:<model>` | On | Off |
|
|
202
|
+
| `bug-hunter` | Root cause discovery & adversarial edge cases | `<provider>:<model>` | On | Off |
|
|
203
|
+
| `refactor-cleanup` | Dead-code pruning & standard-library simplicity | `<provider>:<model>` | Off | Off |
|
|
204
|
+
| `frontend-ui` | High-fidelity responsive web interfaces | `<provider>:<model>` | Off | Off |
|
|
205
|
+
| `security-audit` | Zero-trust threat analysis & sanitization | `<provider>:<model>` | On | On |
|
|
206
|
+
| `math-logic` | Deterministic algorithmic proofs & math logic | `<provider>:<model>` | On | Off |
|
|
207
|
+
| `creative-brainstorm`| Divergent lateral thinking & ideation | `<provider>:<model>` | Off | Off |
|
|
208
208
|
|
|
209
209
|
### Candidate Personas (`role_persona`)
|
|
210
210
|
Assign archetypal engineering mentalities to individual candidate slots to ensure genuine perspective divergence:
|
package/README.ru.md
CHANGED
|
@@ -151,8 +151,8 @@ graph TD
|
|
|
151
151
|
### 11. Композитный гибридный синтез (Composite AST/Block Merge)
|
|
152
152
|
Опция `composite_merge_enabled` активирует алгоритм объединения модулей: судья не просто выбирает одного кандидата, а синтезирует итоговое решение из лучших компонентов всех участников (чистейшая алгоритмическая база от одного, надежные обработчики ошибок от второго, исчерпывающие типы и тесты от третьего).
|
|
153
153
|
|
|
154
|
-
### 12. Умный динамический роутинг пресетов и модель
|
|
155
|
-
При запуске `/moa` без явного указания флага встроенный классификатор определяет намерение (интерфейсы, безопасность, рефакторинг, баги, архитектура) по эвристическим правилам или с помощью легковесной модели
|
|
154
|
+
### 12. Умный динамический роутинг пресетов и модель роутера
|
|
155
|
+
При запуске `/moa` без явного указания флага встроенный классификатор определяет намерение (интерфейсы, безопасность, рефакторинг, баги, архитектура) по эвристическим правилам или с помощью легковесной модели роутера (`smart_routing_model`) и автоматически подбирает оптимальный пресет.
|
|
156
156
|
|
|
157
157
|
### 13. Живой счетчик стоимости и потоковая телеметрия
|
|
158
158
|
По мере завершения каждого кандидата плагин в реальном времени транслирует в чат накопленный расход токенов и расчетную стоимость в USD на базе встроенного каталога тарифов OpenRouter и прямых цен провайдеров.
|
|
@@ -195,16 +195,16 @@ dsh plugin --profile web add @goodandready/dsh-moa
|
|
|
195
195
|
|
|
196
196
|
| Имя пресета | Назначение | Судья по умолчанию | Рецензия (Round 2) | Слепая оценка |
|
|
197
197
|
| :--- | :--- | :--- | :---: | :---: |
|
|
198
|
-
| `default` | Сбалансированная генерация |
|
|
199
|
-
| `code-review` | Глубокое взаимное ревью кода |
|
|
200
|
-
| `fast-audit` | Сверхбыстрый экспресс-аудит (Fast Mode) |
|
|
201
|
-
| `deep-architect` | Проектирование распределенных систем |
|
|
202
|
-
| `bug-hunter` | Поиск скрытых багов и граничных случаев |
|
|
203
|
-
| `refactor-cleanup` | Чистка легаси и удаление оверинжиниринга |
|
|
204
|
-
| `frontend-ui` | Качественные адаптивные веб-интерфейсы |
|
|
205
|
-
| `security-audit` | Анализ уязвимостей и санитайзинг |
|
|
206
|
-
| `math-logic` | Алгоритмы, математическая логика и доказательства |
|
|
207
|
-
| `creative-brainstorm`| Нестандартные идеи и дивергентный поиск |
|
|
198
|
+
| `default` | Сбалансированная генерация | `<provider>:<model>` | Опционально | Выкл |
|
|
199
|
+
| `code-review` | Глубокое взаимное ревью кода | `<provider>:<model>` | Вкл | Вкл |
|
|
200
|
+
| `fast-audit` | Сверхбыстрый экспресс-аудит (Fast Mode) | `<provider>:<model>` | Выкл | Выкл |
|
|
201
|
+
| `deep-architect` | Проектирование распределенных систем | `<provider>:<model>` | Вкл | Выкл |
|
|
202
|
+
| `bug-hunter` | Поиск скрытых багов и граничных случаев | `<provider>:<model>` | Вкл | Выкл |
|
|
203
|
+
| `refactor-cleanup` | Чистка легаси и удаление оверинжиниринга | `<provider>:<model>` | Выкл | Выкл |
|
|
204
|
+
| `frontend-ui` | Качественные адаптивные веб-интерфейсы | `<provider>:<model>` | Выкл | Выкл |
|
|
205
|
+
| `security-audit` | Анализ уязвимостей и санитайзинг | `<provider>:<model>` | Вкл | Вкл |
|
|
206
|
+
| `math-logic` | Алгоритмы, математическая логика и доказательства | `<provider>:<model>` | Вкл | Выкл |
|
|
207
|
+
| `creative-brainstorm`| Нестандартные идеи и дивергентный поиск | `<provider>:<model>` | Выкл | Выкл |
|
|
208
208
|
|
|
209
209
|
### Инженерные персоны кандидатов (`role_persona`)
|
|
210
210
|
Задавайте кандидатам четкие роли для максимального разнообразия подходов:
|
package/lib/file-workspace.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { bestEffort } from './best-effort.js'
|
|
2
|
+
import { logger } from './logger.js'
|
|
2
3
|
import vm from 'node:vm'
|
|
3
4
|
import fs from 'node:fs/promises'
|
|
4
5
|
import path from 'node:path'
|
|
@@ -237,13 +238,28 @@ export async function createPrePromotionCheckpoint(baseDir, candidateTarget, opt
|
|
|
237
238
|
}
|
|
238
239
|
|
|
239
240
|
const label = options.checkpointLabel || `moa-pre-promotion: candidate-${candidateTarget}`
|
|
240
|
-
|
|
241
|
+
|
|
242
|
+
// 1. Direct Cordis timeMachineEngine service call if available
|
|
243
|
+
if (options.timeMachineEngine && typeof options.timeMachineEngine.createSnapshot === 'function') {
|
|
244
|
+
const snap = await bestEffort('dsh-time-machine direct service createSnapshot', async () => {
|
|
245
|
+
return await options.timeMachineEngine.createSnapshot(label, { cwd: baseDir })
|
|
246
|
+
})
|
|
247
|
+
if (snap) return snap
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// 2. HTTP endpoint call if port or timeMachineUrl is provided
|
|
251
|
+
const port = options.port || options.webServerPort || process.env.DSH_PORT || process.env.PORT
|
|
252
|
+
if (!port && !options.timeMachineUrl) {
|
|
253
|
+
logger.warn('[dsh-moa] Cannot create pre-promotion checkpoint: webServerPort is unknown and no timeMachineUrl provided')
|
|
254
|
+
return null
|
|
255
|
+
}
|
|
256
|
+
|
|
241
257
|
const endpoint = options.timeMachineUrl || `http://127.0.0.1:${port}/dsh-time-machine/create`
|
|
242
258
|
|
|
243
259
|
const snap = await bestEffort('dsh-time-machine pre-promotion checkpoint', async () => {
|
|
244
260
|
const res = await fetch(endpoint, {
|
|
245
261
|
method: 'POST',
|
|
246
|
-
headers: { 'Content-Type': 'application/json'
|
|
262
|
+
headers: { 'Content-Type': 'application/json' },
|
|
247
263
|
body: JSON.stringify({ label, cwd: baseDir }),
|
|
248
264
|
signal: AbortSignal.timeout(options.timeoutMs ?? 1500),
|
|
249
265
|
})
|
|
@@ -275,6 +291,11 @@ export async function promoteCandidateWorkspace(baseDir, winningIndex, options =
|
|
|
275
291
|
}
|
|
276
292
|
|
|
277
293
|
const checkpoint = await createPrePromotionCheckpoint(baseDir, winningIndex, options)
|
|
294
|
+
if (options.checkpoint !== false && !checkpoint && !options.force) {
|
|
295
|
+
const errMsg = `[dsh-moa] Pre-promotion checkpoint failed for candidate-${winningIndex} in ${baseDir}. File overwrite blocked to prevent uncheckpointed changes (pass force: true to override).`
|
|
296
|
+
logger.warn(errMsg)
|
|
297
|
+
throw new Error(errMsg)
|
|
298
|
+
}
|
|
278
299
|
|
|
279
300
|
const promoted = []
|
|
280
301
|
for (const rel of entries) {
|
package/lib/history.js
CHANGED
|
@@ -10,8 +10,38 @@ import path from 'node:path'
|
|
|
10
10
|
import os from 'node:os'
|
|
11
11
|
import crypto from 'node:crypto'
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
export function isTestEnvironment() {
|
|
14
|
+
return (
|
|
15
|
+
process.env.NODE_ENV === 'test' ||
|
|
16
|
+
process.env.DSH_TEST === '1' ||
|
|
17
|
+
Boolean(process.env.NODE_TEST_CONTEXT) ||
|
|
18
|
+
Boolean(process.execArgv?.some((arg) => arg.includes('test'))) ||
|
|
19
|
+
Boolean(process.argv?.[1]?.includes('test'))
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function getDefaultHistoryDir() {
|
|
24
|
+
if (process.env.DSH_HISTORY_DIR) {
|
|
25
|
+
return process.env.DSH_HISTORY_DIR
|
|
26
|
+
}
|
|
27
|
+
if (process.env.DSH_HOME) {
|
|
28
|
+
return path.join(process.env.DSH_HOME, '.dsh')
|
|
29
|
+
}
|
|
30
|
+
if (isTestEnvironment()) {
|
|
31
|
+
return path.join(os.tmpdir(), '.dsh-test')
|
|
32
|
+
}
|
|
33
|
+
return path.join(os.homedir(), '.dsh')
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function getDefaultHistoryFile() {
|
|
37
|
+
if (process.env.DSH_HISTORY_FILE) {
|
|
38
|
+
return process.env.DSH_HISTORY_FILE
|
|
39
|
+
}
|
|
40
|
+
return path.join(getDefaultHistoryDir(), 'moa-history.jsonl')
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const DEFAULT_HISTORY_DIR = getDefaultHistoryDir()
|
|
44
|
+
export const DEFAULT_HISTORY_FILE = getDefaultHistoryFile()
|
|
15
45
|
|
|
16
46
|
/** Max history file size before rotation (10 MB) */
|
|
17
47
|
export const MAX_HISTORY_BYTES = 10 * 1024 * 1024
|
|
@@ -23,18 +53,18 @@ export function invalidateHistoryCache() {
|
|
|
23
53
|
_historyCache = null
|
|
24
54
|
}
|
|
25
55
|
|
|
26
|
-
function getHistoryFilePath(customDir) {
|
|
56
|
+
export function getHistoryFilePath(customDir) {
|
|
27
57
|
if (customDir) {
|
|
28
58
|
return path.join(customDir, '.moa-history.jsonl')
|
|
29
59
|
}
|
|
30
|
-
return
|
|
60
|
+
return getDefaultHistoryFile()
|
|
31
61
|
}
|
|
32
62
|
|
|
33
63
|
/**
|
|
34
64
|
* Checks if the history file exceeds the size threshold and rotates it.
|
|
35
65
|
* Moves current file to `${filePath}.1`, removing any older `.1` backup.
|
|
36
66
|
*/
|
|
37
|
-
export function rotateHistoryFileIfNeeded(filePath =
|
|
67
|
+
export function rotateHistoryFileIfNeeded(filePath = getDefaultHistoryFile(), maxBytes = MAX_HISTORY_BYTES) {
|
|
38
68
|
try {
|
|
39
69
|
if (!fs.existsSync(filePath)) return false
|
|
40
70
|
const stat = fs.statSync(filePath)
|
|
@@ -96,7 +126,7 @@ function normalizeRunRecord(record) {
|
|
|
96
126
|
* Appends a completed MoA run record to the history file synchronously (preserves sync API).
|
|
97
127
|
* Performs automatic size-based rotation.
|
|
98
128
|
*/
|
|
99
|
-
export function recordMoaRun(record, filePath =
|
|
129
|
+
export function recordMoaRun(record, filePath = getDefaultHistoryFile()) {
|
|
100
130
|
try {
|
|
101
131
|
const dir = path.dirname(filePath)
|
|
102
132
|
if (!fs.existsSync(dir)) {
|
|
@@ -119,7 +149,7 @@ export function recordMoaRun(record, filePath = DEFAULT_HISTORY_FILE) {
|
|
|
119
149
|
/**
|
|
120
150
|
* Asynchronously appends a completed MoA run record without blocking the event loop.
|
|
121
151
|
*/
|
|
122
|
-
export async function recordMoaRunAsync(record, filePath =
|
|
152
|
+
export async function recordMoaRunAsync(record, filePath = getDefaultHistoryFile()) {
|
|
123
153
|
try {
|
|
124
154
|
const dir = path.dirname(filePath)
|
|
125
155
|
await fs.promises.mkdir(dir, { recursive: true }).catch(() => {})
|
|
@@ -183,7 +213,7 @@ export function readTailLinesSync(filePath, countNeeded = 50) {
|
|
|
183
213
|
/**
|
|
184
214
|
* Reads history runs with pagination (latest first).
|
|
185
215
|
*/
|
|
186
|
-
export function getMoaHistory(limit = 20, offset = 0, filePath =
|
|
216
|
+
export function getMoaHistory(limit = 20, offset = 0, filePath = getDefaultHistoryFile()) {
|
|
187
217
|
try {
|
|
188
218
|
if (!fs.existsSync(filePath)) {
|
|
189
219
|
return { total: 0, runs: [] }
|
|
@@ -242,7 +272,7 @@ export function getMoaHistory(limit = 20, offset = 0, filePath = DEFAULT_HISTORY
|
|
|
242
272
|
* Calculates win-rate leaderboard and model performance statistics across history.
|
|
243
273
|
* Employs in-memory caching keyed by file mtime and size for O(1) repeated queries.
|
|
244
274
|
*/
|
|
245
|
-
export function getMoaLeaderboard(filePath =
|
|
275
|
+
export function getMoaLeaderboard(filePath = getDefaultHistoryFile(), options = {}) {
|
|
246
276
|
try {
|
|
247
277
|
if (!fs.existsSync(filePath)) {
|
|
248
278
|
return { totalRuns: 0, models: [] }
|
|
@@ -344,7 +374,7 @@ export function getMoaLeaderboard(filePath = DEFAULT_HISTORY_FILE, options = {})
|
|
|
344
374
|
/**
|
|
345
375
|
* Retrieves a single history run by ID.
|
|
346
376
|
*/
|
|
347
|
-
export function getMoaRunById(runId, filePath =
|
|
377
|
+
export function getMoaRunById(runId, filePath = getDefaultHistoryFile()) {
|
|
348
378
|
try {
|
|
349
379
|
if (!fs.existsSync(filePath)) return null
|
|
350
380
|
const raw = fs.readFileSync(filePath, 'utf8')
|
|
@@ -364,7 +394,7 @@ export function getMoaRunById(runId, filePath = DEFAULT_HISTORY_FILE) {
|
|
|
364
394
|
/**
|
|
365
395
|
* Exports history data in JSON or CSV format with optional preset filtering.
|
|
366
396
|
*/
|
|
367
|
-
export function exportMoaHistory(filePath =
|
|
397
|
+
export function exportMoaHistory(filePath = getDefaultHistoryFile(), options = {}) {
|
|
368
398
|
try {
|
|
369
399
|
if (!fs.existsSync(filePath)) {
|
|
370
400
|
return options?.format === 'csv' ? 'id,timestamp,preset,winnerModel,totalTokens,totalCostUsd,durationMs\n' : '[]'
|
package/lib/index.js
CHANGED
|
@@ -51,8 +51,8 @@ export const PriceRow = z.object({
|
|
|
51
51
|
})
|
|
52
52
|
|
|
53
53
|
export const ModelSlotSchema = z.object({
|
|
54
|
-
provider: z.string().default('
|
|
55
|
-
model: z.string().default('
|
|
54
|
+
provider: z.string().default(''),
|
|
55
|
+
model: z.string().default(''),
|
|
56
56
|
role_persona: z.string().default(''),
|
|
57
57
|
temperature: z.number().default(-1),
|
|
58
58
|
})
|
|
@@ -66,7 +66,7 @@ export const PresetSchema = z.object({
|
|
|
66
66
|
quorum_enabled: z.boolean().default(false),
|
|
67
67
|
grace_period_sec: z.number().default(10),
|
|
68
68
|
reference_models: z.array(ModelSlotSchema).default([]),
|
|
69
|
-
aggregator: ModelSlotSchema.default({ provider: '
|
|
69
|
+
aggregator: ModelSlotSchema.default({ provider: '', model: '' }),
|
|
70
70
|
aggregator_fallbacks: z.array(ModelSlotSchema).default([]),
|
|
71
71
|
reference_temperature: z.number().default(0.6),
|
|
72
72
|
aggregator_temperature: z.number().default(0.4),
|
|
@@ -85,7 +85,7 @@ export const PresetSchema = z.object({
|
|
|
85
85
|
judge_voting_strategy: z.string().default('majority'),
|
|
86
86
|
composite_merge_enabled: z.boolean().default(false),
|
|
87
87
|
smart_routing_enabled: z.boolean().default(false),
|
|
88
|
-
smart_routing_model: ModelSlotSchema.default({ provider: '
|
|
88
|
+
smart_routing_model: ModelSlotSchema.default({ provider: '', model: '' }),
|
|
89
89
|
budget_guard_enabled: z.boolean().default(false),
|
|
90
90
|
max_budget_usd: z.number().default(0),
|
|
91
91
|
budget_action: z.string().default('trim'),
|
|
@@ -100,7 +100,7 @@ export const Config = z.object({
|
|
|
100
100
|
enabled: z.boolean().default(true).volatile(),
|
|
101
101
|
default_preset: z.string().default('default').volatile(),
|
|
102
102
|
smart_routing_enabled: z.boolean().default(false).volatile(),
|
|
103
|
-
smart_routing_model: ModelSlotSchema.default({ provider: '
|
|
103
|
+
smart_routing_model: ModelSlotSchema.default({ provider: '', model: '' }).volatile(),
|
|
104
104
|
prices: z.dict(PriceRow).default({}).volatile(),
|
|
105
105
|
presets: z.array(PresetSchema).default([
|
|
106
106
|
{
|
|
@@ -108,10 +108,10 @@ export const Config = z.object({
|
|
|
108
108
|
enabled: true,
|
|
109
109
|
ask_clarifying_questions: true,
|
|
110
110
|
reference_models: [
|
|
111
|
-
{ provider: '
|
|
112
|
-
{ provider: '
|
|
111
|
+
{ provider: '', model: '', role_persona: 'proposer' },
|
|
112
|
+
{ provider: '', model: '', role_persona: 'challenger' },
|
|
113
113
|
],
|
|
114
|
-
aggregator: { provider: '
|
|
114
|
+
aggregator: { provider: '', model: '' },
|
|
115
115
|
reference_temperature: 0.6,
|
|
116
116
|
aggregator_temperature: 0.4,
|
|
117
117
|
max_tokens: 4096,
|
|
@@ -122,9 +122,9 @@ export const Config = z.object({
|
|
|
122
122
|
enabled: true,
|
|
123
123
|
ask_clarifying_questions: false,
|
|
124
124
|
reference_models: [
|
|
125
|
-
{ provider: '
|
|
125
|
+
{ provider: '', model: '', role_persona: 'fast' },
|
|
126
126
|
],
|
|
127
|
-
aggregator: { provider: '
|
|
127
|
+
aggregator: { provider: '', model: '' },
|
|
128
128
|
reference_temperature: 0.6,
|
|
129
129
|
aggregator_temperature: 0.2,
|
|
130
130
|
max_tokens: 4096,
|
|
@@ -135,10 +135,10 @@ export const Config = z.object({
|
|
|
135
135
|
enabled: true,
|
|
136
136
|
ask_clarifying_questions: true,
|
|
137
137
|
reference_models: [
|
|
138
|
-
{ provider: '
|
|
139
|
-
{ provider: '
|
|
138
|
+
{ provider: '', model: '', role_persona: 'architect' },
|
|
139
|
+
{ provider: '', model: '', role_persona: 'critic' },
|
|
140
140
|
],
|
|
141
|
-
aggregator: { provider: '
|
|
141
|
+
aggregator: { provider: '', model: '' },
|
|
142
142
|
reference_temperature: 0.6,
|
|
143
143
|
aggregator_temperature: 0.2,
|
|
144
144
|
max_tokens: 8192,
|
|
@@ -377,8 +377,8 @@ export function apply(ctx, config) {
|
|
|
377
377
|
}
|
|
378
378
|
}
|
|
379
379
|
const targetPreset = (cfg.presets || []).find((p) => p.name === parsed.presetName) || cfg.presets?.[0]
|
|
380
|
-
const aggProvider = targetPreset?.aggregator?.provider || '
|
|
381
|
-
const aggModel = targetPreset?.aggregator?.model || '
|
|
380
|
+
const aggProvider = targetPreset?.aggregator?.provider || ''
|
|
381
|
+
const aggModel = targetPreset?.aggregator?.model || ''
|
|
382
382
|
const sessCwd = event.cwd || event.session?.cwd || (event.session?.path ? path.dirname(event.session.path) : undefined) || process.cwd()
|
|
383
383
|
|
|
384
384
|
const turnContext = {
|
|
@@ -390,6 +390,8 @@ export function apply(ctx, config) {
|
|
|
390
390
|
callLlm,
|
|
391
391
|
prices: cfg.prices || {},
|
|
392
392
|
liveCanvas,
|
|
393
|
+
webServerPort: ctx.webServer?.port,
|
|
394
|
+
timeMachineEngine: ctx.get?.('timeMachineEngine') || ctx.timeMachineEngine,
|
|
393
395
|
}
|
|
394
396
|
|
|
395
397
|
if (event.signal) {
|
package/lib/moa-router.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Smart Preset Router for Mixture of Agents (MoA).
|
|
3
|
-
* Supports heuristic keyword classification and optional
|
|
3
|
+
* Supports heuristic keyword classification and optional LLM zero-shot routing.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
const INTENT_RULES = [
|
|
@@ -48,7 +48,7 @@ export async function resolvePresetForPrompt({
|
|
|
48
48
|
return { presetName: safeDefault, reason: 'default', isAutoRouted: false }
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
// 1. If LLM routing model
|
|
51
|
+
// 1. If LLM routing model is configured and callable, run fast zero-shot classifier
|
|
52
52
|
if (routingModel?.provider && routingModel?.model && typeof callLlm === 'function') {
|
|
53
53
|
try {
|
|
54
54
|
const systemPrompt = `You are the MoA Preset Router. Given user prompt, classify which preset is best suited.
|
package/lib/moa-runner.js
CHANGED
|
@@ -4,7 +4,7 @@ import crypto from 'node:crypto'
|
|
|
4
4
|
import { bestEffort } from './best-effort.js'
|
|
5
5
|
import { extractFileBlocks, collectProjectContext, formatProjectContext, isRefinementTask, writeCandidateWorkspace, promoteCandidateWorkspace, cleanMoaWorkspaces, verifyFileSyntax } from './file-workspace.js'
|
|
6
6
|
import { estimateTokenCost, summarizeMoAUsage } from './pricing.js'
|
|
7
|
-
import { recordMoaRunAsync, candidatesForHistory } from './history.js'
|
|
7
|
+
import { recordMoaRunAsync, candidatesForHistory, isTestEnvironment } from './history.js'
|
|
8
8
|
import { createPromotedPreview } from './live-canvas.js'
|
|
9
9
|
import { slotLabel, cleanAdvisoryMessages, isBroadPromptRequiringQuestions, buildQuestionSynthesisPrompt, buildCuratorSynthesisPrompt, buildSynthesisPrompt, buildPeerCritiquePrompt, ROLE_PERSONA_PROMPTS, SYSTEM_ROLE_PROPOSER, ANTIPATTERNS_RUBRIC } from './moa-prompts.js'
|
|
10
10
|
import { parseWinnerIndex, parseRecommendedAssembler, parseMoACommand, stripOrSummarizeCode, formatMoAResponse } from './moa-parser.js'
|
|
@@ -41,15 +41,29 @@ export async function runMoAPipeline({
|
|
|
41
41
|
testGateEnabled = null,
|
|
42
42
|
testCommand = null,
|
|
43
43
|
execFn = null,
|
|
44
|
+
webServerPort = null,
|
|
45
|
+
timeMachineEngine = null,
|
|
46
|
+
timeMachineUrl = null,
|
|
47
|
+
force = isTestEnvironment() ? true : false,
|
|
48
|
+
checkpoint = true,
|
|
49
|
+
checkpointFn = null,
|
|
44
50
|
}) {
|
|
51
|
+
const promoteOpts = {
|
|
52
|
+
webServerPort,
|
|
53
|
+
timeMachineEngine,
|
|
54
|
+
timeMachineUrl,
|
|
55
|
+
force: Boolean(force),
|
|
56
|
+
checkpoint,
|
|
57
|
+
checkpointFn,
|
|
58
|
+
}
|
|
45
59
|
if (signal?.aborted) return { error: new Error('Turn aborted') }
|
|
46
60
|
const startTime = Date.now()
|
|
47
61
|
|
|
48
62
|
// 1. Resolve configurations
|
|
49
63
|
const referenceModels = Array.isArray(preset?.reference_models) && preset.reference_models.length > 0
|
|
50
|
-
? preset.reference_models : [{ provider: '
|
|
64
|
+
? preset.reference_models : [{ provider: '', model: '' }]
|
|
51
65
|
const primaryJudge = preset?.aggregator?.provider && preset?.aggregator?.model
|
|
52
|
-
? preset.aggregator : { provider: '
|
|
66
|
+
? preset.aggregator : { provider: '', model: '' }
|
|
53
67
|
|
|
54
68
|
const fallbackJudges = Array.isArray(preset?.aggregator_fallbacks) ? preset.aggregator_fallbacks : []
|
|
55
69
|
const judgesChain = [primaryJudge, ...fallbackJudges]
|
|
@@ -283,7 +297,7 @@ export async function runMoAPipeline({
|
|
|
283
297
|
const single = successfulRefs[0]
|
|
284
298
|
let promotedFiles = []
|
|
285
299
|
if (single.files?.length > 0) {
|
|
286
|
-
promotedFiles = await promoteCandidateWorkspace(cwd, single.index)
|
|
300
|
+
promotedFiles = await promoteCandidateWorkspace(cwd, single.index, promoteOpts)
|
|
287
301
|
} else {
|
|
288
302
|
await cleanMoaWorkspaces(cwd)
|
|
289
303
|
}
|
|
@@ -492,12 +506,12 @@ export async function runMoAPipeline({
|
|
|
492
506
|
const keepWorkspaces = allowCandidateOverride
|
|
493
507
|
if (synthesizedFiles.length > 0) {
|
|
494
508
|
await writeCandidateWorkspace(cwd, 'curator-synthesis', synthesizedFiles)
|
|
495
|
-
promotedFiles = await promoteCandidateWorkspace(cwd, 'curator-synthesis', { keepMoa: keepWorkspaces })
|
|
509
|
+
promotedFiles = await promoteCandidateWorkspace(cwd, 'curator-synthesis', { ...promoteOpts, keepMoa: keepWorkspaces })
|
|
496
510
|
} else if (referenceOutputs[winningIndex - 1]?.files?.length > 0) {
|
|
497
|
-
promotedFiles = await promoteCandidateWorkspace(cwd, winningIndex, { keepMoa: keepWorkspaces })
|
|
511
|
+
promotedFiles = await promoteCandidateWorkspace(cwd, winningIndex, { ...promoteOpts, keepMoa: keepWorkspaces })
|
|
498
512
|
} else if (successfulRefs[0]?.files?.length > 0) {
|
|
499
513
|
const fallbackIdx = successfulRefs[0].index
|
|
500
|
-
promotedFiles = await promoteCandidateWorkspace(cwd, fallbackIdx, { keepMoa: keepWorkspaces })
|
|
514
|
+
promotedFiles = await promoteCandidateWorkspace(cwd, fallbackIdx, { ...promoteOpts, keepMoa: keepWorkspaces })
|
|
501
515
|
} else if (!keepWorkspaces) {
|
|
502
516
|
await cleanMoaWorkspaces(cwd)
|
|
503
517
|
}
|
package/lib/moa-stream.js
CHANGED
|
@@ -9,7 +9,7 @@ import { runMoAPipeline } from './moa-runner.js'
|
|
|
9
9
|
/**
|
|
10
10
|
* Streams a full MoA turn into chat with live aggregator tokens and progress feedback.
|
|
11
11
|
*/
|
|
12
|
-
export async function* streamMoATurn({ targetPreset, userPrompt, messages, callLlm, cwd, prices, historyFilePath, liveCanvas }, options = {}) {
|
|
12
|
+
export async function* streamMoATurn({ targetPreset, userPrompt, messages, callLlm, cwd, prices, historyFilePath, liveCanvas, webServerPort, timeMachineEngine, force }, options = {}) {
|
|
13
13
|
const signal = options?.signal
|
|
14
14
|
if (signal?.aborted) return
|
|
15
15
|
|
|
@@ -40,6 +40,9 @@ export async function* streamMoATurn({ targetPreset, userPrompt, messages, callL
|
|
|
40
40
|
prices,
|
|
41
41
|
historyFilePath,
|
|
42
42
|
liveCanvas,
|
|
43
|
+
webServerPort,
|
|
44
|
+
timeMachineEngine,
|
|
45
|
+
force,
|
|
43
46
|
signal,
|
|
44
47
|
})
|
|
45
48
|
.catch((err) => ({ error: err }))
|
package/lib/pricing.js
CHANGED
|
@@ -11,7 +11,21 @@ import { homedir } from 'node:os'
|
|
|
11
11
|
import { dirname, join } from 'node:path'
|
|
12
12
|
|
|
13
13
|
export const CATALOG_URL = 'https://openrouter.ai/api/v1/models'
|
|
14
|
-
|
|
14
|
+
|
|
15
|
+
export function getDefaultCacheFile() {
|
|
16
|
+
if (process.env.DSH_CATALOG_FILE) {
|
|
17
|
+
return process.env.DSH_CATALOG_FILE
|
|
18
|
+
}
|
|
19
|
+
if (process.env.DSH_STORAGE_DIR) {
|
|
20
|
+
return join(process.env.DSH_STORAGE_DIR, 'dsh-moa-catalog.json')
|
|
21
|
+
}
|
|
22
|
+
const baseDir = process.env.DSH_HOME
|
|
23
|
+
? join(process.env.DSH_HOME, '.dsh')
|
|
24
|
+
: join(homedir(), '.dsh')
|
|
25
|
+
return join(baseDir, 'storages', 'dsh-moa-catalog.json')
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const CACHE_FILE = getDefaultCacheFile()
|
|
15
29
|
export const REFRESH_INTERVAL_MS = 24 * 60 * 60 * 1000 // 24 hours
|
|
16
30
|
|
|
17
31
|
/**
|
|
@@ -43,7 +57,7 @@ export function resetMemoryCatalog() {
|
|
|
43
57
|
/**
|
|
44
58
|
* Load cached catalog from disk into memory.
|
|
45
59
|
*/
|
|
46
|
-
export function loadCachedCatalog(cachePath =
|
|
60
|
+
export function loadCachedCatalog(cachePath = getDefaultCacheFile()) {
|
|
47
61
|
if (memoryCatalog && memoryCachePath === cachePath) return memoryCatalog
|
|
48
62
|
try {
|
|
49
63
|
if (existsSync(cachePath)) {
|
|
@@ -69,7 +83,7 @@ export function loadCachedCatalog(cachePath = CACHE_FILE) {
|
|
|
69
83
|
*/
|
|
70
84
|
export async function fetchCatalog({
|
|
71
85
|
url = CATALOG_URL,
|
|
72
|
-
cachePath =
|
|
86
|
+
cachePath = getDefaultCacheFile(),
|
|
73
87
|
signal = null,
|
|
74
88
|
} = {}) {
|
|
75
89
|
try {
|
|
@@ -118,7 +132,7 @@ export async function fetchCatalog({
|
|
|
118
132
|
/**
|
|
119
133
|
* Background refresh catalog if stale (> 24 hours).
|
|
120
134
|
*/
|
|
121
|
-
export function refreshCatalogInBackground(cachePath =
|
|
135
|
+
export function refreshCatalogInBackground(cachePath = getDefaultCacheFile()) {
|
|
122
136
|
const catalog = loadCachedCatalog(cachePath)
|
|
123
137
|
const isStale = !lastFetchedAt || Date.now() - lastFetchedAt > REFRESH_INTERVAL_MS
|
|
124
138
|
if (isStale) {
|
|
@@ -134,16 +148,20 @@ export function refreshCatalogInBackground(cachePath = CACHE_FILE) {
|
|
|
134
148
|
/**
|
|
135
149
|
* Resolve price rates (USD per 1M tokens) for a given provider/model slot.
|
|
136
150
|
*/
|
|
137
|
-
export function resolveModelRates(slot = {}, customPrices = {}, cachePath =
|
|
151
|
+
export function resolveModelRates(slot = {}, customPrices = {}, cachePath = getDefaultCacheFile()) {
|
|
138
152
|
const provider = (slot?.provider || '').trim().toLowerCase()
|
|
139
153
|
const model = (slot?.model || '').trim().toLowerCase()
|
|
140
154
|
const fullKey = provider ? `${provider}/${model}` : model
|
|
155
|
+
const colonKey = provider ? `${provider}:${model}` : model
|
|
141
156
|
|
|
142
157
|
// 1. Check custom user overrides in config
|
|
143
158
|
if (customPrices && typeof customPrices === 'object') {
|
|
144
159
|
if (customPrices[fullKey]) return normalizeRates(customPrices[fullKey])
|
|
160
|
+
if (customPrices[colonKey]) return normalizeRates(customPrices[colonKey])
|
|
145
161
|
if (customPrices[model]) return normalizeRates(customPrices[model])
|
|
146
|
-
if (provider && customPrices[`${provider}/*`]
|
|
162
|
+
if (provider && (customPrices[`${provider}/*`] || customPrices[`${provider}:*`])) {
|
|
163
|
+
return normalizeRates(customPrices[`${provider}/*`] || customPrices[`${provider}:*`])
|
|
164
|
+
}
|
|
147
165
|
if (customPrices['*']) return normalizeRates(customPrices['*'])
|
|
148
166
|
}
|
|
149
167
|
|
|
@@ -271,7 +289,7 @@ function normalizeRates(rate) {
|
|
|
271
289
|
/**
|
|
272
290
|
* Estimate USD cost and token totals for a single model call.
|
|
273
291
|
*/
|
|
274
|
-
export function estimateTokenCost(slot = {}, usage = {}, customPrices = {}, cachePath =
|
|
292
|
+
export function estimateTokenCost(slot = {}, usage = {}, customPrices = {}, cachePath = getDefaultCacheFile()) {
|
|
275
293
|
const promptTokens = usage.prompt_tokens ?? usage.inputTokens ?? usage.input ?? usage.promptTokens ?? 0
|
|
276
294
|
const completionTokens = usage.completion_tokens ?? usage.outputTokens ?? usage.output ?? usage.completionTokens ?? 0
|
|
277
295
|
const cacheHitTokens = usage.prompt_tokens_details?.cached_tokens ?? usage.cacheHitTokens ?? 0
|
package/lib/routes.js
CHANGED
|
@@ -319,7 +319,12 @@ ctx.effect(() => {
|
|
|
319
319
|
return
|
|
320
320
|
}
|
|
321
321
|
const cwd = body?.cwd || process.cwd()
|
|
322
|
-
const promotedFiles = await promoteCandidateWorkspace(cwd, candidateIndex, {
|
|
322
|
+
const promotedFiles = await promoteCandidateWorkspace(cwd, candidateIndex, {
|
|
323
|
+
keepMoa: true,
|
|
324
|
+
port: ctx.webServer?.port,
|
|
325
|
+
timeMachineEngine: ctx.get?.('timeMachineEngine') || ctx.timeMachineEngine,
|
|
326
|
+
force: Boolean(body?.force),
|
|
327
|
+
})
|
|
323
328
|
writeJson(res, 200, { ok: true, candidateIndex, promotedFiles, checkpoint: promotedFiles?.checkpoint || null })
|
|
324
329
|
} catch (err) {
|
|
325
330
|
writeJson(res, 500, { ok: false, error: err?.message || String(err) })
|
package/lib/updater.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Implements canonical DSH plugin-updater specification.
|
|
3
3
|
|
|
4
4
|
import { spawn } from 'node:child_process';
|
|
5
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
5
|
+
import { existsSync, readFileSync, statSync, rmSync } from 'node:fs';
|
|
6
6
|
import { readFile } from 'node:fs/promises';
|
|
7
7
|
import { homedir } from 'node:os';
|
|
8
8
|
import { basename, dirname, isAbsolute, resolve } from 'node:path';
|
|
@@ -183,8 +183,79 @@ export async function getUpdateStatus(options, target = runtime()) {
|
|
|
183
183
|
};
|
|
184
184
|
}
|
|
185
185
|
|
|
186
|
-
|
|
186
|
+
export function readLockPid(lockPath) {
|
|
187
|
+
try {
|
|
188
|
+
if (!existsSync(lockPath)) return undefined;
|
|
189
|
+
const stat = statSync(lockPath);
|
|
190
|
+
if (stat.isDirectory()) return undefined;
|
|
191
|
+
const content = readFileSync(lockPath, 'utf8').trim();
|
|
192
|
+
if (!content) return undefined;
|
|
193
|
+
try {
|
|
194
|
+
const parsed = JSON.parse(content);
|
|
195
|
+
if (parsed && typeof parsed.pid === 'number') return parsed.pid;
|
|
196
|
+
} catch (parseErr) { /* ignore json parse error */ }
|
|
197
|
+
const num = parseInt(content, 10);
|
|
198
|
+
if (!isNaN(num) && num > 0) return num;
|
|
199
|
+
const match = /\b(\d{1,8})\b/.exec(content);
|
|
200
|
+
if (match) return Number(match[1]);
|
|
201
|
+
return undefined;
|
|
202
|
+
} catch {
|
|
203
|
+
return undefined;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function isPidAlive(pid) {
|
|
208
|
+
if (typeof pid !== 'number' || isNaN(pid) || pid <= 0) return false;
|
|
209
|
+
try {
|
|
210
|
+
process.kill(pid, 0);
|
|
211
|
+
return true;
|
|
212
|
+
} catch (err) {
|
|
213
|
+
return err?.code === 'EPERM';
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export function checkAndCleanLock(profileDir, childPid) {
|
|
218
|
+
const lockPath = resolve(profileDir, 'package.json.lock');
|
|
219
|
+
if (!existsSync(lockPath)) return { locked: false };
|
|
220
|
+
const pid = readLockPid(lockPath);
|
|
221
|
+
if (pid !== undefined) {
|
|
222
|
+
if (isPidAlive(pid)) {
|
|
223
|
+
if (childPid !== undefined && pid === childPid) {
|
|
224
|
+
try {
|
|
225
|
+
rmSync(lockPath, { recursive: true, force: true });
|
|
226
|
+
return { locked: false, cleaned: true };
|
|
227
|
+
} catch (rmErr) { return { locked: true, pid }; }
|
|
228
|
+
}
|
|
229
|
+
return { locked: true, pid };
|
|
230
|
+
}
|
|
231
|
+
// PID is dead, remove stale lock
|
|
232
|
+
try {
|
|
233
|
+
rmSync(lockPath, { recursive: true, force: true });
|
|
234
|
+
return { locked: false, cleaned: true };
|
|
235
|
+
} catch (rmErr) { return { locked: true, pid }; }
|
|
236
|
+
}
|
|
237
|
+
// Unknown or empty lockfile format
|
|
238
|
+
if (childPid !== undefined) {
|
|
239
|
+
try {
|
|
240
|
+
rmSync(lockPath, { recursive: true, force: true });
|
|
241
|
+
return { locked: false, cleaned: true };
|
|
242
|
+
} catch (parseErr) { /* ignore json parse error */ }
|
|
243
|
+
}
|
|
244
|
+
return { locked: true };
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export async function installExact(target, packageSpec, options = {}) {
|
|
187
248
|
if (target.cliEntry === undefined) throw new Error('Automatic update is unavailable in this runtime.');
|
|
249
|
+
|
|
250
|
+
const lockStatus = checkAndCleanLock(target.profileDir);
|
|
251
|
+
if (lockStatus.locked) {
|
|
252
|
+
const error = new Error('Another plugin installation is currently in progress.');
|
|
253
|
+
error.status = 409;
|
|
254
|
+
error.statusCode = 409;
|
|
255
|
+
error.code = 'ELOCKED';
|
|
256
|
+
throw error;
|
|
257
|
+
}
|
|
258
|
+
|
|
188
259
|
await new Promise((resolvePromise, reject) => {
|
|
189
260
|
// Note: Respect strict ecosystem quarantine policy without age bypass
|
|
190
261
|
const args = [
|
|
@@ -201,13 +272,24 @@ async function installExact(target, packageSpec, options) {
|
|
|
201
272
|
let detail = '';
|
|
202
273
|
child.stdout?.on('data', chunk => { detail = (detail + String(chunk)).slice(-4_000); });
|
|
203
274
|
child.stderr?.on('data', chunk => { detail = (detail + String(chunk)).slice(-4_000); });
|
|
275
|
+
let timedOut = false;
|
|
276
|
+
const timeoutMs = options.timeoutMs ?? UPDATE_TIMEOUT_MS;
|
|
204
277
|
const timer = setTimeout(() => {
|
|
205
|
-
|
|
278
|
+
timedOut = true;
|
|
279
|
+
try { child.kill('SIGTERM'); } catch (killErr) { /* best-effort */ }
|
|
280
|
+
setTimeout(() => {
|
|
281
|
+
try { child.kill('SIGKILL'); } catch (killErr) { /* best-effort */ }
|
|
282
|
+
checkAndCleanLock(target.profileDir, child.pid);
|
|
283
|
+
}, 500).unref?.();
|
|
206
284
|
reject(new Error('Update timed out; use the normal DSH update flow.'));
|
|
207
|
-
},
|
|
285
|
+
}, timeoutMs);
|
|
208
286
|
child.once('error', error => { clearTimeout(timer); reject(error); });
|
|
209
|
-
child.once('exit', code => {
|
|
287
|
+
child.once('exit', (code, signal) => {
|
|
210
288
|
clearTimeout(timer);
|
|
289
|
+
if (timedOut) {
|
|
290
|
+
checkAndCleanLock(target.profileDir, child.pid);
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
211
293
|
if (code === 0) resolvePromise();
|
|
212
294
|
else reject(new Error(detail.trim() || `Update exited with code ${String(code)}.`));
|
|
213
295
|
});
|
|
@@ -253,6 +335,11 @@ export function registerPluginUpdater(ctx, options) {
|
|
|
253
335
|
json(response, 409, { error: 'This plugin is already updating.' });
|
|
254
336
|
return;
|
|
255
337
|
}
|
|
338
|
+
const lockStatus = checkAndCleanLock(target.profileDir);
|
|
339
|
+
if (lockStatus.locked) {
|
|
340
|
+
json(response, 409, { error: 'Another plugin installation is currently in progress.' });
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
256
343
|
installing = true;
|
|
257
344
|
try {
|
|
258
345
|
const before = await getUpdateStatus(options, target);
|
|
@@ -274,6 +361,10 @@ export function registerPluginUpdater(ctx, options) {
|
|
|
274
361
|
installing = false;
|
|
275
362
|
}
|
|
276
363
|
} catch (error) {
|
|
364
|
+
if (error?.status === 409 || error?.statusCode === 409) {
|
|
365
|
+
json(response, 409, { error: error.message || 'Another plugin installation is currently in progress.' });
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
277
368
|
ctx.logger?.warn?.(`plugin updater failed: ${String(error)}`);
|
|
278
369
|
json(response, 503, { error: 'Plugin update failed; see server logs.' });
|
|
279
370
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-moa",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.30",
|
|
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",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"LICENSE"
|
|
20
20
|
],
|
|
21
21
|
"scripts": {
|
|
22
|
-
"test": "node --test test/*.test.mjs",
|
|
22
|
+
"test": "node --import ./test/_setup.mjs --test test/*.test.mjs",
|
|
23
23
|
"build:client": "node scripts/build-client.mjs"
|
|
24
24
|
},
|
|
25
25
|
"keywords": [
|
|
@@ -27,8 +27,7 @@
|
|
|
27
27
|
"dsh-plugin",
|
|
28
28
|
"moa",
|
|
29
29
|
"mixture-of-agents",
|
|
30
|
-
"slash-command"
|
|
31
|
-
"hermes-agent"
|
|
30
|
+
"slash-command"
|
|
32
31
|
],
|
|
33
32
|
"repository": {
|
|
34
33
|
"type": "git",
|