@goodandready/dsh-moa 0.2.16 → 0.2.17

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/lib/client.js CHANGED
@@ -113,6 +113,17 @@ window.__ModuleLoader__.load({
113
113
  'badge.disabled': 'MoA Disabled',
114
114
  'config.enabled_label': 'Enable MoA (slash command and turn routing)',
115
115
  'slash.desc': 'Run turn with Mixture of Agents ensemble synthesis (/moa [preset] <prompt>)',
116
+ 'updater.title': 'Plugin Updates',
117
+ 'updater.desc': 'Check npm registry for new versions and update with one click.',
118
+ 'updater.btnCheck': 'Check Updates',
119
+ 'updater.btnUpdate': 'Update to v{version}',
120
+ 'updater.checking': 'Checking registry...',
121
+ 'updater.updating': 'Updating plugin...',
122
+ 'updater.current': 'Current version: v{version}',
123
+ 'updater.available': 'Update available: v{version}',
124
+ 'updater.upToDate': 'Up to date',
125
+ 'updater.checkFailed': 'Registry check failed',
126
+ 'updater.success': 'Successfully updated to v{version}. Restart DSH to apply.',
116
127
  }
117
128
 
118
129
  const zh = {
@@ -205,6 +216,17 @@ window.__ModuleLoader__.load({
205
216
  'badge.disabled': 'MoA 已禁用',
206
217
  'config.enabled_label': '启用 MoA(斜杠命令与轮次拦截路由)',
207
218
  'slash.desc': '使用智能体混合 (MoA) 协同综合运行此轮次 (/moa [预设] <提示词>)',
219
+ 'updater.title': '插件在线更新',
220
+ 'updater.desc': '检查 npm 镜像源上的最新版本并支持一键无感升级。',
221
+ 'updater.btnCheck': '检查更新',
222
+ 'updater.btnUpdate': '升级至 v{version}',
223
+ 'updater.checking': '正在检查版本...',
224
+ 'updater.updating': '正在执行更新...',
225
+ 'updater.current': '当前版本:v{version}',
226
+ 'updater.available': '发现新版本:v{version}',
227
+ 'updater.upToDate': '已是最新版本',
228
+ 'updater.checkFailed': '检查版本失败',
229
+ 'updater.success': '成功更新至 v{version},请重启 DSH 使其生效。',
208
230
  }
209
231
 
210
232
 
@@ -572,6 +594,9 @@ window.__ModuleLoader__.load({
572
594
  handleSave,
573
595
  saveStatus,
574
596
  reload,
597
+ updateState,
598
+ checkUpdates,
599
+ runUpdate,
575
600
  }) {
576
601
  const [selectedPresetName, setSelectedPresetName] = React.useState(defaultPreset || (presets[0] && presets[0].name) || 'default')
577
602
  const [isCreating, setIsCreating] = React.useState(false)
@@ -1381,6 +1406,84 @@ window.__ModuleLoader__.load({
1381
1406
  )
1382
1407
  ),
1383
1408
 
1409
+ /* ── PLUGIN UPDATES ── */
1410
+ React.createElement(
1411
+ 'div',
1412
+ { className: 'moa-section-card' },
1413
+ React.createElement(
1414
+ 'div',
1415
+ { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 6 } },
1416
+ React.createElement('div', { className: 'moa-sec-title' }, '✨ ' + (t('updater.title') || 'Plugin Updates')),
1417
+ React.createElement(
1418
+ 'div',
1419
+ { style: { display: 'flex', gap: 8 } },
1420
+ React.createElement(
1421
+ 'button',
1422
+ {
1423
+ type: 'button',
1424
+ className: 'moa-btn moa-btn-sub',
1425
+ disabled: updateState?.checking || updateState?.updating,
1426
+ onClick: checkUpdates,
1427
+ },
1428
+ updateState?.checking ? (t('updater.checking') || 'Checking...') : (t('updater.btnCheck') || 'Check Updates')
1429
+ ),
1430
+ updateState?.updateAvailable
1431
+ ? React.createElement(
1432
+ 'button',
1433
+ {
1434
+ type: 'button',
1435
+ className: 'moa-btn moa-btn-sub moa-btn-primary',
1436
+ disabled: updateState?.updating,
1437
+ onClick: runUpdate,
1438
+ },
1439
+ updateState?.updating
1440
+ ? (t('updater.updating') || 'Updating...')
1441
+ : (t('updater.btnUpdate') || 'Update to v{version}').replace('{version}', updateState.latestVersion || '')
1442
+ )
1443
+ : null
1444
+ )
1445
+ ),
1446
+ React.createElement(
1447
+ 'div',
1448
+ { className: 'moa-sec-desc', style: { marginBottom: 8 } },
1449
+ t('updater.desc') || 'Check npm registry for new versions and update with one click.'
1450
+ ),
1451
+ React.createElement(
1452
+ 'div',
1453
+ { style: { display: 'flex', alignItems: 'center', gap: 10, fontSize: 12 } },
1454
+ React.createElement(
1455
+ 'span',
1456
+ { style: { color: 'var(--dsw-alias-label-secondary)' } },
1457
+ (t('updater.current') || 'Current version: v{version}').replace('{version}', updateState?.currentVersion || '0.2.16')
1458
+ ),
1459
+ updateState?.checking
1460
+ ? React.createElement('span', { style: { color: 'var(--dsw-alias-state-brand-primary, var(--dsw-alias-label-primary))' } }, t('updater.checking'))
1461
+ : updateState?.updateAvailable
1462
+ ? React.createElement(
1463
+ 'span',
1464
+ { style: { color: 'var(--dsw-alias-state-error-primary)', fontWeight: 600 } },
1465
+ (t('updater.available') || 'Update available: v{version}').replace('{version}', updateState?.latestVersion || '')
1466
+ )
1467
+ : updateState?.currentVersion && !updateState?.error
1468
+ ? React.createElement('span', { style: { color: 'var(--dsw-alias-state-success-primary)' } }, '✓ ' + (t('updater.upToDate') || 'Up to date'))
1469
+ : null
1470
+ ),
1471
+ updateState?.notice
1472
+ ? React.createElement(
1473
+ 'div',
1474
+ { className: 'moa-alert-ok', style: { marginTop: 8 } },
1475
+ updateState.notice
1476
+ )
1477
+ : null,
1478
+ updateState?.error
1479
+ ? React.createElement(
1480
+ 'div',
1481
+ { className: 'moa-alert-err', style: { marginTop: 8 } },
1482
+ updateState.error
1483
+ )
1484
+ : null
1485
+ ),
1486
+
1384
1487
  /* ── ENABLED TOGGLE ── */
1385
1488
  React.createElement(
1386
1489
  'label',
@@ -1454,6 +1557,65 @@ window.__ModuleLoader__.load({
1454
1557
  const [defaultPreset, setDefaultPreset] = React.useState('default')
1455
1558
  const [availableModels, setAvailableModels] = React.useState([])
1456
1559
  const [saveStatus, setSaveStatus] = React.useState('')
1560
+ const [updateState, setUpdateState] = React.useState({
1561
+ checking: false,
1562
+ updating: false,
1563
+ currentVersion: '0.2.16',
1564
+ latestVersion: undefined,
1565
+ updateAvailable: false,
1566
+ notice: null,
1567
+ error: null,
1568
+ })
1569
+
1570
+ const checkUpdates = async () => {
1571
+ setUpdateState((prev) => ({ ...prev, checking: true, error: null, notice: null }))
1572
+ try {
1573
+ const res = await fetch('/api/dsh-moa/update', { cache: 'no-store' })
1574
+ const data = await res.json()
1575
+ if (data && data.currentVersion) {
1576
+ setUpdateState((prev) => ({
1577
+ ...prev,
1578
+ checking: false,
1579
+ currentVersion: data.currentVersion,
1580
+ latestVersion: data.latestVersion,
1581
+ updateAvailable: Boolean(data.updateAvailable),
1582
+ error: data.latestCheckFailed ? (t('updater.checkFailed') || 'Registry check failed') : null,
1583
+ }))
1584
+ } else {
1585
+ setUpdateState((prev) => ({ ...prev, checking: false, error: data.error || 'Check failed' }))
1586
+ }
1587
+ } catch (err) {
1588
+ setUpdateState((prev) => ({ ...prev, checking: false, error: String(err?.message || err) }))
1589
+ }
1590
+ }
1591
+
1592
+ const runUpdate = async () => {
1593
+ setUpdateState((prev) => ({ ...prev, updating: true, error: null, notice: null }))
1594
+ try {
1595
+ const res = await fetch('/api/dsh-moa/update', {
1596
+ method: 'POST',
1597
+ headers: { 'Content-Type': 'application/json', 'x-dsh-plugin-update': '1' },
1598
+ })
1599
+ const data = await res.json()
1600
+ if (data && data.updated) {
1601
+ setUpdateState((prev) => ({
1602
+ ...prev,
1603
+ updating: false,
1604
+ updateAvailable: false,
1605
+ currentVersion: data.updatedVersion || data.latestVersion,
1606
+ notice: (t('updater.success') || 'Successfully updated to v{version}. Restart DSH to apply.').replace('{version}', data.updatedVersion || data.latestVersion),
1607
+ }))
1608
+ } else {
1609
+ setUpdateState((prev) => ({
1610
+ ...prev,
1611
+ updating: false,
1612
+ error: data.error || data.message || 'Update failed',
1613
+ }))
1614
+ }
1615
+ } catch (err) {
1616
+ setUpdateState((prev) => ({ ...prev, updating: false, error: String(err?.message || err) }))
1617
+ }
1618
+ }
1457
1619
  const [enabled, setEnabled] = React.useState(true)
1458
1620
  const [hostStatus, setHostStatus] = React.useState('connecting')
1459
1621
  const [stats, setStats] = React.useState({ totalRuns: null, avgCostUsd: null })
@@ -1619,6 +1781,9 @@ window.__ModuleLoader__.load({
1619
1781
  handleSave,
1620
1782
  saveStatus,
1621
1783
  reload,
1784
+ updateState,
1785
+ checkUpdates,
1786
+ runUpdate,
1622
1787
  }
1623
1788
  }
1624
1789
 
@@ -95,16 +95,25 @@ function sanitizePath(filePath) {
95
95
  * Formats collected project files into structured Markdown code blocks
96
96
  * suitable for inclusion in candidate system prompts.
97
97
  */
98
- export function formatProjectContext(projectFiles = []) {
98
+ export function formatProjectContext(projectFiles = [], options = {}) {
99
99
  if (!Array.isArray(projectFiles) || projectFiles.length === 0) return ''
100
+ const { skippedFiles = 0, skippedList = [] } = options
100
101
  const fence = '```'
101
- return projectFiles
102
+ let formatted = projectFiles
102
103
  .map((f) => `### File: ${f.relativePath}\n${fence}\n${f.content}\n${fence}`)
103
104
  .join('\n\n')
105
+
106
+ if (skippedFiles > 0) {
107
+ const preview = skippedList.slice(0, 5).join(', ')
108
+ const more = skippedList.length > 5 ? ` and ${skippedList.length - 5} more` : ''
109
+ const warning = `> ⚠️ **Workspace Scan Notice:** ${skippedFiles} workspace file(s) were skipped or truncated due to size/budget limits or read errors (${preview}${more}). The workspace context below is partial.`
110
+ formatted = `${warning}\n\n${formatted}`
111
+ }
112
+ return formatted
104
113
  }
105
114
 
106
115
  export async function collectProjectContext(baseDir, maxCharBudget = 16000) {
107
- if (!baseDir) return { files: [], totalChars: 0 }
116
+ if (!baseDir) return { files: [], totalChars: 0, skippedFiles: 0, skippedList: [] }
108
117
 
109
118
  const ignoreDirs = new Set(['node_modules', '.git', '.moa', '.worktrees', 'dist', 'build', '.dsh', '.next', '.nuxt', '.cache', 'coverage', '.turbo', '.idea', '.vscode'])
110
119
  const codeExts = new Set([
@@ -130,7 +139,6 @@ export async function collectProjectContext(baseDir, maxCharBudget = 16000) {
130
139
  }
131
140
 
132
141
  for (const item of list) {
133
- if (totalChars >= maxCharBudget) break
134
142
  // Skip all dotfiles, including .env and .env.* variants: their
135
143
  // contents must never be shipped to third-party model providers.
136
144
  if (item.name.startsWith('.')) continue
@@ -138,6 +146,14 @@ export async function collectProjectContext(baseDir, maxCharBudget = 16000) {
138
146
  const rel = relPrefix ? `${relPrefix}/${item.name}` : item.name
139
147
  const full = path.join(currentDir, item.name)
140
148
 
149
+ if (totalChars >= maxCharBudget) {
150
+ if (item.isFile() && codeExts.has(path.extname(item.name).toLowerCase())) {
151
+ skippedFiles++
152
+ skippedList.push(`${rel} (budget limit)`)
153
+ }
154
+ continue
155
+ }
156
+
141
157
  if (item.isDirectory()) {
142
158
  if (!ignoreDirs.has(item.name)) {
143
159
  await scan(full, rel)
@@ -147,7 +163,11 @@ export async function collectProjectContext(baseDir, maxCharBudget = 16000) {
147
163
  if (codeExts.has(ext)) {
148
164
  try {
149
165
  const stat = await fs.stat(full)
150
- if (stat.size > 100000) continue // skip files > 100kb
166
+ if (stat.size > 100000) {
167
+ skippedFiles++
168
+ skippedList.push(`${rel} (>100KB)`)
169
+ continue
170
+ }
151
171
  const content = await fs.readFile(full, 'utf8')
152
172
  const budgetRemaining = maxCharBudget - totalChars
153
173
  const truncated = content.length > budgetRemaining ? content.slice(0, budgetRemaining) + '\n... [truncated]' : content
package/lib/index.js CHANGED
@@ -25,6 +25,7 @@ import { createLiveCanvasClient } from './live-canvas.js'
25
25
  import { registerPluginUpdater, isSafeWriteRequest } from './updater.js'
26
26
 
27
27
  export const name = '@goodandready/dsh-moa'
28
+ export { collectProjectContext, formatProjectContext } from './file-workspace.js'
28
29
  export const inject = ['webServer', 'llm', 'settings', 'sessions', 'tools']
29
30
 
30
31
  export const NS = 'dsh-moa'
package/lib/moa-parser.js CHANGED
@@ -138,6 +138,12 @@ export function formatMoAResponse({ moaResult, presetName }) {
138
138
  parts.push('> 🔄 **Mode**: Iterative project refinement (Refinement)')
139
139
  }
140
140
 
141
+ if (moaResult?.skippedFiles > 0) {
142
+ const preview = moaResult.skippedList?.slice(0, 3)?.join(', ') || ''
143
+ const more = (moaResult.skippedList?.length || 0) > 3 ? ` and ${(moaResult.skippedList?.length || 0) - 3} more` : ''
144
+ parts.push(`> ⚠️ **Workspace Notice**: ${moaResult.skippedFiles} file(s) skipped during workspace scan (${preview}${more}). Context representation may be partial.`)
145
+ }
146
+
141
147
  const hasPromoted = moaResult?.promotedFiles && moaResult.promotedFiles.length > 0
142
148
  if (hasPromoted) {
143
149
  parts.push(`> 📦 **Files created in the project**: \`${moaResult.promotedFiles.join('`, `')}\``)
package/lib/moa-runner.js CHANGED
@@ -18,8 +18,6 @@ export { slotLabel, cleanAdvisoryMessages, isBroadPromptRequiringQuestions, buil
18
18
  export { parseWinnerIndex, parseRecommendedAssembler, parseMoACommand, stripOrSummarizeCode, formatMoAResponse } from './moa-parser.js'
19
19
  export { estimateTokenCost, summarizeMoAUsage } from './pricing.js'
20
20
 
21
- const DEFAULT_PROMPT = 'Please propose an optimal, well-structured, production-ready solution with full code and explanations.'
22
-
23
21
  /**
24
22
  * Invokes LLM call with transient retry for recoverable network/rate-limit errors.
25
23
  */
@@ -78,11 +76,19 @@ export async function runMoAPipeline({
78
76
  const collectedCtx = await collectProjectContext(cwd, 16000)
79
77
  const isRefinement = Boolean(collectedCtx?.files?.length > 0 && isRefinementTask(userPrompt, collectedCtx.files))
80
78
  let projectContext = ''
79
+ if (collectedCtx?.skippedFiles > 0 && typeof onProgress === 'function') {
80
+ const preview = collectedCtx.skippedList.slice(0, 3).join(', ')
81
+ const more = collectedCtx.skippedList.length > 3 ? ` (+${collectedCtx.skippedList.length - 3})` : ''
82
+ onProgress(`⚠️ *Workspace scan note: ${collectedCtx.skippedFiles} file(s) skipped (${preview}${more})*\n`)
83
+ }
81
84
  if (isRefinement) {
82
85
  if (typeof onProgress === 'function') {
83
86
  onProgress('🔍 *Reading project files for refinement context...*\n')
84
87
  }
85
- projectContext = formatProjectContext(collectedCtx.files)
88
+ projectContext = formatProjectContext(collectedCtx.files, {
89
+ skippedFiles: collectedCtx.skippedFiles,
90
+ skippedList: collectedCtx.skippedList,
91
+ })
86
92
  }
87
93
 
88
94
  // 3. Build prompts & evaluate broad questionnaire needs
@@ -140,6 +146,8 @@ export async function runMoAPipeline({
140
146
  candidates: referenceOutputs.map((r) => ({ label: r.label, usage: r.usage, costUsd: r.costUsd })),
141
147
  aggregator: { inputTokens: 0, outputTokens: 0, totalTokens: 0, costUsd: 0 },
142
148
  },
149
+ skippedFiles: collectedCtx?.skippedFiles || 0,
150
+ skippedList: collectedCtx?.skippedList || [],
143
151
  durationMs: Date.now() - startTime,
144
152
  }
145
153
  }
@@ -227,6 +235,8 @@ export async function runMoAPipeline({
227
235
  candidates: referenceOutputs.map((r) => ({ label: r.label, usage: r.usage, costUsd: r.costUsd })),
228
236
  aggregator: qUsage,
229
237
  },
238
+ skippedFiles: collectedCtx?.skippedFiles || 0,
239
+ skippedList: collectedCtx?.skippedList || [],
230
240
  durationMs,
231
241
  }
232
242
  }
@@ -283,6 +293,8 @@ export async function runMoAPipeline({
283
293
  candidates: referenceOutputs.map((r) => ({ label: r.label, usage: r.usage, costUsd: r.costUsd })),
284
294
  aggregator: { inputTokens: 0, outputTokens: 0, totalTokens: 0, costUsd: 0 },
285
295
  },
296
+ skippedFiles: collectedCtx?.skippedFiles || 0,
297
+ skippedList: collectedCtx?.skippedList || [],
286
298
  durationMs,
287
299
  }
288
300
  }
@@ -461,6 +473,8 @@ export async function runMoAPipeline({
461
473
  allowCandidateOverride,
462
474
  ...(livePreview ? { liveCanvas: livePreview } : {}),
463
475
  usage: usageSummary,
476
+ skippedFiles: collectedCtx?.skippedFiles || 0,
477
+ skippedList: collectedCtx?.skippedList || [],
464
478
  durationMs,
465
479
  }
466
480
  }
package/lib/routes.js CHANGED
@@ -398,6 +398,10 @@ ctx.effect(() => {
398
398
  liveCanvas,
399
399
  })
400
400
 
401
+ if (result?.skippedFiles > 0) {
402
+ ctx.logger?.warn?.(`[dsh-moa] Workspace scan skipped ${result.skippedFiles} file(s): ${result.skippedList.join(', ')}`)
403
+ }
404
+
401
405
  writeJson(res, 200, { ok: true, ...result })
402
406
  } catch (err) {
403
407
  writeJson(res, 500, { ok: false, error: err?.message || String(err) })
package/lib/updater.js CHANGED
@@ -186,11 +186,13 @@ export async function getUpdateStatus(options, target = runtime()) {
186
186
  async function installExact(target, packageSpec, options) {
187
187
  if (target.cliEntry === undefined) throw new Error('Automatic update is unavailable in this runtime.');
188
188
  await new Promise((resolvePromise, reject) => {
189
- const child = spawn(process.execPath, [
189
+ // Note: Respect strict ecosystem quarantine policy without age bypass
190
+ const args = [
190
191
  target.cliEntry, 'plugin', '--profile', target.profileName, 'add',
191
- '--config.minimumReleaseAge=0', packageSpec,
192
+ packageSpec,
192
193
  `--registry=${options.registry ?? 'https://registry.npmjs.org/'}`,
193
- ], {
194
+ ];
195
+ const child = spawn(process.execPath, args, {
194
196
  cwd: target.profileDir,
195
197
  windowsHide: true,
196
198
  stdio: ['ignore', 'pipe', 'pipe'],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-moa",
3
- "version": "0.2.16",
3
+ "version": "0.2.17",
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",