@goodandready/dsh-moa 0.2.2 → 0.2.4

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
@@ -37,6 +37,10 @@ Single-model AI generation often suffers from blind spots, single-perspective bi
37
37
  4. **Instant Live Canvas Previewing**: When web applications or UI components are generated, `dsh-moa` integrates seamlessly with `@goodandready/dsh-live-canvas`, automatically spawning sandboxes for 1-click browser previewing.
38
38
  5. **Token-Saving Chat Summarization**: Replaces massive code dumps in chat bubbles with compact file listings and clean architectural summaries.
39
39
  6. **One-Shot Session Model Restoration**: Executes cleanly as a one-shot turn modifier, automatically reverting back to the user's primary session model immediately after completion.
40
+ 7. **Inherent Cost Tracking & Token Estimation**: Embedded pricing calculator for frontier models estimating token usage and run costs directly in the summary badge without external dependencies.
41
+ 8. **Refinement Mode (Incremental Edits)**: Automatically detects existing codebase context to generate precise delta modifications instead of destructive full-file rewrites.
42
+ 9. **Fast Mode & Custom Judge Criteria**: Ultra-fast single-model preset for quick tasks and customizable evaluation guidelines for the judge.
43
+ 10. **Run History & Win-Rate Leaderboard**: Persistent logging with built-in REST endpoints (`/dsh-moa/history` and `/dsh-moa/leaderboard`).
40
44
 
41
45
  ---
42
46
 
@@ -175,6 +179,16 @@ dsh-moa:
175
179
 
176
180
  ---
177
181
 
182
+ ## 📊 REST API & Endpoints
183
+
184
+ | Endpoint | Method | Description |
185
+ |:---|:---|:---|
186
+ | `/dsh-moa/presets` | `GET` | Returns list of configured MoA presets |
187
+ | `/dsh-moa/history?limit=20&offset=0` | `GET` | Returns recent MoA runs with candidates, winner, cost, and tokens |
188
+ | `/dsh-moa/leaderboard` | `GET` | Computes model win-rate leaderboard and average execution costs |
189
+
190
+ ---
191
+
178
192
  ## 🧪 Testing
179
193
 
180
194
  Run the automated test suite:
package/docs/README.ru.md CHANGED
@@ -37,6 +37,10 @@
37
37
  4. **Мгновенный предпросмотр через Live Canvas**: При создании веб-приложений и UI-компонентов `dsh-moa` бесшовно интегрируется с `@goodandready/dsh-live-canvas`, автоматически инициализируя контейнер для предпросмотра в браузере в 1 клик.
38
38
  5. **Экономия токенов в чате**: Вместо вывода огромных листингов кода в чат формируется компактный отчет с перечнем созданных файлов и архитектурным резюме.
39
39
  6. **Одноразовый модификатор сессии**: Команда выполняется в рамках одного такта и автоматически возвращает исходную модель сессии пользователя сразу после завершения.
40
+ 7. **Встроенный подсчет стоимости и токенов**: Собственный калькулятор стоимости моделей оценивает расход токенов и цену запуска напрямую в бейдже без внешних плагинов.
41
+ 8. **Режим доработки (Refinement Mode)**: Автоматически считывает контекст существующего проекта и генерирует точечные дельта-правки без перетирания всей кодовой базы.
42
+ 9. **Быстрый режим (Fast Mode) и критерии судьи**: Режим для одиночных быстрых задач без судьи и гибкая настройка фокуса оценки (безопасность, производительность, минимализм).
43
+ 10. **История запусков и лидерборд моделей**: Персистентное логирование запусков и REST-эндпоинты (`/dsh-moa/history` и `/dsh-moa/leaderboard`).
40
44
 
41
45
  ---
42
46
 
@@ -175,6 +179,16 @@ dsh-moa:
175
179
 
176
180
  ---
177
181
 
182
+ ## 📊 REST API эндпоинты
183
+
184
+ | Эндпоинт | Метод | Описание |
185
+ |:---|:---|:---|
186
+ | `/dsh-moa/presets` | `GET` | Возвращает список настроенных пресетов MoA |
187
+ | `/dsh-moa/history?limit=20&offset=0` | `GET` | История запусков с кандидатами, победителем, токенами и ценой |
188
+ | `/dsh-moa/leaderboard` | `GET` | Лидерборд побед моделей и средняя стоимость генерации |
189
+
190
+ ---
191
+
178
192
  ## 🧪 Тестирование
179
193
 
180
194
  Запуск автоматического набора тестов:
package/lib/client.js CHANGED
@@ -6,6 +6,7 @@ window.__ModuleLoader__.load({
6
6
  let React = require('react')
7
7
 
8
8
  const NS = 'dsh-moa'
9
+ let localeRegistered = false
9
10
 
10
11
  let ChevronIcon = null
11
12
  try {
@@ -621,6 +622,30 @@ window.__ModuleLoader__.load({
621
622
  })
622
623
  ),
623
624
  React.createElement('div', { className: 'moa-hint' }, t('aggTempHint') || 'Рекомендуется 0.2 – 0.4 для точной проверки и синтеза без галлюцинаций.')
625
+ ),
626
+ React.createElement(
627
+ 'div',
628
+ { className: 'moa-field', style: { marginTop: 4, borderTop: '1px solid var(--dsw-alias-border-l2)', paddingTop: 8 } },
629
+ React.createElement('label', { className: 'moa-label', style: { fontSize: 12 } }, t('judgeCriteriaLabel') || 'Критерии оценки судьи (опционально):'),
630
+ React.createElement('textarea', {
631
+ className: 'moa-textarea',
632
+ rows: 2,
633
+ style: {
634
+ width: '100%',
635
+ boxSizing: 'border-box',
636
+ padding: '8px 10px',
637
+ borderRadius: 8,
638
+ border: '1px solid var(--dsw-alias-border-l2)',
639
+ background: 'var(--dsw-alias-bg-layer-3)',
640
+ color: 'var(--dsw-alias-label-primary)',
641
+ fontSize: 13,
642
+ resize: 'vertical',
643
+ outline: 'none',
644
+ },
645
+ placeholder: t('judgeCriteriaPlaceholder') || 'например: Приоритет — производительность, чистый код, минимум зависимостей...',
646
+ value: judgeCriteria,
647
+ onChange: (e) => setJudgeCriteria(e.target.value),
648
+ })
624
649
  )
625
650
  ),
626
651
  /* Proposers / Advisors Box with Temperature Slider */
@@ -911,9 +936,14 @@ window.__ModuleLoader__.load({
911
936
  )
912
937
  }
913
938
 
914
- exports.inject = ['slots', 'locale', 'sessions', 'inputTriggers']
939
+ exports.inject = ['slots', 'locale', 'inputTriggers']
915
940
  exports.apply = function apply(ctx) {
916
- ctx.locale.register(NS, {
941
+ const registerLocale = () => {
942
+ if (localeRegistered) return
943
+ try {
944
+ if (ctx.locale && typeof ctx.locale.register === 'function') {
945
+ localeRegistered = true
946
+ return ctx.locale.register(NS, {
917
947
  en: {
918
948
  title: 'Mixture of Agents (MoA)',
919
949
  description: 'Run tasks with multiple parallel proposer models and a synthesizing judge model',
@@ -954,6 +984,8 @@ window.__ModuleLoader__.load({
954
984
  noModelsMatch: 'No models found matching query.',
955
985
  useCustom: 'Use custom',
956
986
  selectModel: 'Select model...',
987
+ judgeCriteriaLabel: 'Custom Judge Evaluation Criteria (optional):',
988
+ judgeCriteriaPlaceholder: 'e.g. Priority: performance, zero external dependencies, robust edge-case handling...',
957
989
  },
958
990
  ru: {
959
991
  title: 'Mixture of Agents (MoA)',
@@ -995,10 +1027,30 @@ window.__ModuleLoader__.load({
995
1027
  noModelsMatch: 'Модели не найдены.',
996
1028
  useCustom: 'Использовать кастомное',
997
1029
  selectModel: 'Выберите модель...',
1030
+ judgeCriteriaLabel: 'Критерии оценки судьи (опционально):',
1031
+ judgeCriteriaPlaceholder: 'например: Приоритет — производительность, минимум зависимостей, обработка граничных случаев...',
998
1032
  },
999
1033
  })
1034
+ }
1035
+ } catch (err) {
1036
+ console.warn('[dsh-moa] locale registration error:', err)
1037
+ }
1038
+ }
1039
+
1040
+ if (typeof ctx.effect === 'function') {
1041
+ ctx.effect(() => registerLocale(), 'dsh-moa: dictionaries')
1042
+ } else {
1043
+ registerLocale()
1044
+ }
1000
1045
 
1001
- const t = ctx.locale.bind(NS)
1046
+ const t = (key) => {
1047
+ try {
1048
+ if (ctx.locale && typeof ctx.locale.bind === 'function') {
1049
+ return ctx.locale.bind(NS)(key) || key
1050
+ }
1051
+ } catch {}
1052
+ return key
1053
+ }
1002
1054
 
1003
1055
  if (ctx.slots) {
1004
1056
  const registerDirectSection = () => {
@@ -85,6 +85,89 @@ function sanitizePath(filePath) {
85
85
  return normalized.replace(/^[\\\/]+/, '')
86
86
  }
87
87
 
88
+ /**
89
+ * Collects readable text files from current workspace to feed as context for candidate models (#6).
90
+ * Ignores node_modules, .git, .moa, binary files, large bundles.
91
+ */
92
+ export async function collectProjectContext(baseDir, maxCharBudget = 16000) {
93
+ if (!baseDir) return { files: [], totalChars: 0 }
94
+
95
+ const ignoreDirs = new Set(['node_modules', '.git', '.moa', '.worktrees', 'dist', 'build', '.dsh'])
96
+ const codeExts = new Set([
97
+ '.html', '.htm', '.css', '.js', '.mjs', '.cjs', '.jsx', '.ts', '.tsx',
98
+ '.vue', '.svelte', '.json', '.py', '.md', '.txt', '.sh', '.yaml', '.yml',
99
+ ])
100
+
101
+ const collected = []
102
+ let totalChars = 0
103
+
104
+ async function scan(currentDir, relPrefix = '') {
105
+ if (totalChars >= maxCharBudget) return
106
+ try {
107
+ const list = await fs.readdir(currentDir, { withFileTypes: true })
108
+ for (const item of list) {
109
+ if (totalChars >= maxCharBudget) break
110
+ if (item.name.startsWith('.') && item.name !== '.env') continue
111
+
112
+ const rel = relPrefix ? `${relPrefix}/${item.name}` : item.name
113
+ const full = path.join(currentDir, item.name)
114
+
115
+ if (item.isDirectory()) {
116
+ if (!ignoreDirs.has(item.name)) {
117
+ await scan(full, rel)
118
+ }
119
+ } else if (item.isFile()) {
120
+ const ext = path.extname(item.name).toLowerCase()
121
+ if (codeExts.has(ext)) {
122
+ try {
123
+ const stat = await fs.stat(full)
124
+ if (stat.size > 100000) continue // skip files > 100kb
125
+ const content = await fs.readFile(full, 'utf8')
126
+ const budgetRemaining = maxCharBudget - totalChars
127
+ const truncated = content.length > budgetRemaining ? content.slice(0, budgetRemaining) + '\n... [truncated]' : content
128
+ totalChars += truncated.length
129
+ collected.push({ relativePath: rel, content: truncated, size: stat.size })
130
+ } catch {}
131
+ }
132
+ }
133
+ }
134
+ } catch {}
135
+ }
136
+
137
+ await scan(baseDir)
138
+ return { files: collected, totalChars }
139
+ }
140
+
141
+ /**
142
+ * Checks if a task prompt is an iterative refinement / modification
143
+ * of an existing project rather than a greenfield project creation (#4).
144
+ */
145
+ export function isRefinementTask(userPrompt = '', projectFiles = []) {
146
+ if (!Array.isArray(projectFiles) || projectFiles.length === 0) return false
147
+ const p = userPrompt.toLowerCase().trim()
148
+ if (!p) return false
149
+
150
+ // Greenfield creation phrases - ALWAYS fresh task
151
+ const freshPhrases = [
152
+ 'с нуля', 'from scratch', 'новый проект', 'новое приложение', 'new project', 'new app',
153
+ 'создай проект', 'создай приложение', 'создай игру', 'создай сервис',
154
+ 'сделай проект', 'сделай приложение', 'сделай игру',
155
+ 'create a new', 'build a new', 'generate a new',
156
+ ]
157
+ if (freshPhrases.some((phrase) => p.includes(phrase))) {
158
+ return false
159
+ }
160
+
161
+ // Modification keywords
162
+ const modKeywords = [
163
+ 'добавь', 'измени', 'поменяй', 'исправь', 'обнови', 'переделай', 'доработай', 'удали',
164
+ 'add', 'change', 'update', 'fix', 'modify', 'refactor', 'improve', 'enhance', 'adjust',
165
+ 'style', 'css', 'темн', 'светл', 'color', 'цвет', 'кнопк', 'theme', 'bug', 'баг',
166
+ ]
167
+
168
+ return modKeywords.some((kw) => p.includes(kw))
169
+ }
170
+
88
171
  /**
89
172
  * Writes candidate files into an isolated candidate workspace:
90
173
  * <baseDir>/.moa/candidate-<index>/
@@ -114,7 +197,7 @@ export async function writeCandidateWorkspace(baseDir, candidateIndex, files = [
114
197
  * Promotes winning candidate files to baseDir root,
115
198
  * then cleans up the temporary .moa folder.
116
199
  */
117
- export async function promoteCandidateWorkspace(baseDir, winningIndex) {
200
+ export async function promoteCandidateWorkspace(baseDir, winningIndex, options = { keepMoa: false }) {
118
201
  if (!baseDir || winningIndex === null || winningIndex === undefined) return []
119
202
 
120
203
  const winningDir = path.join(baseDir, '.moa', `candidate-${winningIndex}`)
@@ -123,7 +206,9 @@ export async function promoteCandidateWorkspace(baseDir, winningIndex) {
123
206
  try {
124
207
  const entries = await getFilesRecursively(winningDir)
125
208
  if (entries.length === 0) {
126
- try { await fs.rm(moaRoot, { recursive: true, force: true }) } catch {}
209
+ if (!options.keepMoa) {
210
+ try { await fs.rm(moaRoot, { recursive: true, force: true }) } catch {}
211
+ }
127
212
  return []
128
213
  }
129
214
 
@@ -137,12 +222,16 @@ export async function promoteCandidateWorkspace(baseDir, winningIndex) {
137
222
  promoted.push(rel)
138
223
  }
139
224
 
140
- // Clean up temporary .moa folder
141
- await fs.rm(moaRoot, { recursive: true, force: true })
225
+ if (!options.keepMoa) {
226
+ // Clean up temporary .moa folder
227
+ await fs.rm(moaRoot, { recursive: true, force: true })
228
+ }
142
229
 
143
230
  return promoted
144
231
  } catch (err) {
145
- try { await fs.rm(moaRoot, { recursive: true, force: true }) } catch {}
232
+ if (!options.keepMoa) {
233
+ try { await fs.rm(moaRoot, { recursive: true, force: true }) } catch {}
234
+ }
146
235
  return []
147
236
  }
148
237
  }
package/lib/history.js ADDED
@@ -0,0 +1,191 @@
1
+ /**
2
+ * MoA Run History & Analytics Module
3
+ * Records every MoA pipeline execution, tracks model win rates and costs,
4
+ * and provides history search and run replay capabilities.
5
+ */
6
+
7
+ import fs from 'node:fs'
8
+ import path from 'node:path'
9
+ import os from 'node:os'
10
+ import crypto from 'node:crypto'
11
+
12
+ const DEFAULT_HISTORY_DIR = path.join(os.homedir(), '.dsh')
13
+ const DEFAULT_HISTORY_FILE = path.join(DEFAULT_HISTORY_DIR, 'moa-history.jsonl')
14
+
15
+ export function getHistoryFilePath(customDir) {
16
+ if (customDir) {
17
+ return path.join(customDir, '.moa-history.jsonl')
18
+ }
19
+ return DEFAULT_HISTORY_FILE
20
+ }
21
+
22
+ /**
23
+ * Appends a completed MoA run record to the history file.
24
+ */
25
+ export function recordMoaRun(record, filePath = DEFAULT_HISTORY_FILE) {
26
+ try {
27
+ const dir = path.dirname(filePath)
28
+ if (!fs.existsSync(dir)) {
29
+ fs.mkdirSync(dir, { recursive: true })
30
+ }
31
+
32
+ const entry = {
33
+ id: record.id || crypto.randomUUID(),
34
+ timestamp: record.timestamp || new Date().toISOString(),
35
+ prompt: record.prompt || '',
36
+ preset: record.preset || 'default',
37
+ isRefinement: Boolean(record.isRefinement),
38
+ candidates: Array.isArray(record.candidates)
39
+ ? record.candidates.map((c) => ({
40
+ provider: c.provider || '',
41
+ model: c.model || '',
42
+ filesCount: Array.isArray(c.files) ? c.files.length : (c.filesCount || 0),
43
+ usage: c.usage || { inputTokens: 0, outputTokens: 0 },
44
+ costUsd: Number(c.costUsd || 0),
45
+ }))
46
+ : [],
47
+ aggregator: record.aggregator
48
+ ? {
49
+ provider: record.aggregator.provider || '',
50
+ model: record.aggregator.model || '',
51
+ usage: record.aggregator.usage || { inputTokens: 0, outputTokens: 0 },
52
+ costUsd: Number(record.aggregator.costUsd || 0),
53
+ }
54
+ : null,
55
+ winnerIndex: typeof record.winnerIndex === 'number' ? record.winnerIndex : -1,
56
+ winnerModel: record.winnerModel || '',
57
+ promotedFiles: Array.isArray(record.promotedFiles) ? record.promotedFiles : [],
58
+ totalTokens: Number(record.totalTokens || 0),
59
+ totalCostUsd: Number(record.totalCostUsd || 0),
60
+ durationMs: Number(record.durationMs || 0),
61
+ }
62
+
63
+ const line = JSON.stringify(entry) + '\n'
64
+ fs.appendFileSync(filePath, line, 'utf8')
65
+ return entry
66
+ } catch (err) {
67
+ console.warn('[dsh-moa] Failed to record run to history:', err)
68
+ return null
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Reads history runs with pagination (latest first).
74
+ */
75
+ export function getMoaHistory(limit = 20, offset = 0, filePath = DEFAULT_HISTORY_FILE) {
76
+ try {
77
+ if (!fs.existsSync(filePath)) {
78
+ return { total: 0, runs: [] }
79
+ }
80
+
81
+ const raw = fs.readFileSync(filePath, 'utf8')
82
+ const lines = raw.trim().split('\n').filter(Boolean)
83
+ const total = lines.length
84
+
85
+ const sliced = lines
86
+ .slice(Math.max(0, total - offset - limit), total - offset)
87
+ .reverse()
88
+ .map((line) => {
89
+ try {
90
+ return JSON.parse(line)
91
+ } catch {
92
+ return null
93
+ }
94
+ })
95
+ .filter(Boolean)
96
+
97
+ return { total, runs: sliced }
98
+ } catch (err) {
99
+ console.warn('[dsh-moa] Failed to read history:', err)
100
+ return { total: 0, runs: [] }
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Calculates win-rate leaderboard and model performance statistics across history.
106
+ */
107
+ export function getMoaLeaderboard(filePath = DEFAULT_HISTORY_FILE) {
108
+ try {
109
+ if (!fs.existsSync(filePath)) {
110
+ return { totalRuns: 0, models: [] }
111
+ }
112
+
113
+ const raw = fs.readFileSync(filePath, 'utf8')
114
+ const lines = raw.trim().split('\n').filter(Boolean)
115
+ const totalRuns = lines.length
116
+
117
+ const modelStats = new Map()
118
+
119
+ for (const line of lines) {
120
+ try {
121
+ const item = JSON.parse(line)
122
+ if (!item) continue
123
+
124
+ const winner = item.winnerModel ? String(item.winnerModel).trim() : null
125
+
126
+ if (Array.isArray(item.candidates)) {
127
+ for (const cand of item.candidates) {
128
+ const key = cand.provider && cand.model ? `${cand.provider}:${cand.model}` : (cand.model || 'unknown')
129
+ if (!modelStats.has(key)) {
130
+ modelStats.set(key, {
131
+ modelKey: key,
132
+ provider: cand.provider || '',
133
+ model: cand.model || key,
134
+ runs: 0,
135
+ wins: 0,
136
+ totalTokens: 0,
137
+ totalCostUsd: 0,
138
+ })
139
+ }
140
+ const st = modelStats.get(key)
141
+ st.runs += 1
142
+ const inTok = cand.usage?.inputTokens || 0
143
+ const outTok = cand.usage?.outputTokens || 0
144
+ st.totalTokens += inTok + outTok
145
+ st.totalCostUsd += Number(cand.costUsd || 0)
146
+
147
+ if (winner && (winner === key || winner === cand.model || winner.includes(cand.model))) {
148
+ st.wins += 1
149
+ }
150
+ }
151
+ }
152
+ } catch {
153
+ // ignore malformed line
154
+ }
155
+ }
156
+
157
+ const models = Array.from(modelStats.values())
158
+ .map((st) => ({
159
+ ...st,
160
+ winRate: st.runs > 0 ? Number(((st.wins / st.runs) * 100).toFixed(1)) : 0,
161
+ avgTokens: st.runs > 0 ? Math.round(st.totalTokens / st.runs) : 0,
162
+ avgCostUsd: st.runs > 0 ? Number((st.totalCostUsd / st.runs).toFixed(4)) : 0,
163
+ }))
164
+ .sort((a, b) => b.wins - a.wins || b.winRate - a.winRate)
165
+
166
+ return { totalRuns, models }
167
+ } catch (err) {
168
+ console.warn('[dsh-moa] Failed to calculate leaderboard:', err)
169
+ return { totalRuns: 0, models: [] }
170
+ }
171
+ }
172
+
173
+ /**
174
+ * Retrieves a single history run by ID.
175
+ */
176
+ export function getMoaRunById(runId, filePath = DEFAULT_HISTORY_FILE) {
177
+ try {
178
+ if (!fs.existsSync(filePath)) return null
179
+ const raw = fs.readFileSync(filePath, 'utf8')
180
+ const lines = raw.trim().split('\n').filter(Boolean)
181
+ for (let i = lines.length - 1; i >= 0; i--) {
182
+ try {
183
+ const item = JSON.parse(lines[i])
184
+ if (item && item.id === runId) return item
185
+ } catch {}
186
+ }
187
+ return null
188
+ } catch {
189
+ return null
190
+ }
191
+ }