@yemi33/minions 0.1.1896 → 0.1.1898
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/dashboard/js/render-other.js +3 -3
- package/dashboard/js/render-plans.js +5 -2
- package/dashboard.js +144 -20
- package/engine/shared.js +21 -5
- package/package.json +1 -1
|
@@ -85,18 +85,18 @@ function renderMcpServers(servers) {
|
|
|
85
85
|
const countEl = document.getElementById('mcp-count');
|
|
86
86
|
countEl.textContent = servers.length;
|
|
87
87
|
if (!servers.length) {
|
|
88
|
-
el.innerHTML = '<p class="empty">No MCP servers found. Add them
|
|
88
|
+
el.innerHTML = '<p class="empty">No MCP servers found. Add them via <code>claude mcp add</code> / <code>copilot mcp add</code>, install a plugin that ships one, or drop a <code>.mcp.json</code> into a registered project — they\'ll appear here automatically.</p>';
|
|
89
89
|
return;
|
|
90
90
|
}
|
|
91
91
|
el.innerHTML = '<div style="display:flex;flex-wrap:wrap;gap:6px;margin-bottom:8px">' +
|
|
92
92
|
servers.map(s =>
|
|
93
|
-
'<div style="font-size:11px;padding:5px 10px;background:var(--surface2);border:1px solid var(--border);border-radius:6px;color:var(--text)" title="' + escHtml([s.source, s.args || s.command].filter(Boolean).join('
|
|
93
|
+
'<div style="font-size:11px;padding:5px 10px;background:var(--surface2);border:1px solid var(--border);border-radius:6px;color:var(--text)" title="' + escHtml([s.source, s.status, s.args || s.command].filter(Boolean).join(' · ')) + '">' +
|
|
94
94
|
escHtml(s.name) +
|
|
95
95
|
(s.source ? ' <span style="color:var(--muted)">(' + escHtml(s.source) + ')</span>' : '') +
|
|
96
96
|
'</div>'
|
|
97
97
|
).join('') +
|
|
98
98
|
'</div>' +
|
|
99
|
-
'<p style="font-size:10px;color:var(--muted);margin:0">
|
|
99
|
+
'<p style="font-size:10px;color:var(--muted);margin:0">Mirrors <code style="color:var(--blue)">claude mcp list</code> + <code style="color:var(--blue)">copilot mcp list --json</code>, plus workspace <code>.mcp.json</code> from each registered project. Cached ~5 min.</p>';
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
function renderMetrics(metrics) {
|
|
@@ -412,13 +412,16 @@ async function planExecute(file, project, btn) {
|
|
|
412
412
|
const data = await res.json();
|
|
413
413
|
if (res.ok) {
|
|
414
414
|
// Inject into local work items so the next render immediately hides Execute
|
|
415
|
-
if (!data.alreadyQueued) {
|
|
415
|
+
if (!data.alreadyQueued && data.id) {
|
|
416
416
|
(window._lastWorkItems = window._lastWorkItems || []).push({
|
|
417
417
|
id: data.id, type: 'plan-to-prd', status: 'pending', planFile: file
|
|
418
418
|
});
|
|
419
419
|
}
|
|
420
420
|
closeModal();
|
|
421
|
-
|
|
421
|
+
// Fall back to a clean message if the server didn't return an id (#2375).
|
|
422
|
+
const idLabel = data.id ? ' (' + data.id + ')' : '';
|
|
423
|
+
const queuedLabel = data.id ? 'Queued ' + data.id + ' — agent will convert plan to PRD' : 'Plan queued — agent will convert plan to PRD';
|
|
424
|
+
showToast('cmd-toast', data.alreadyQueued ? 'Already queued' + idLabel : queuedLabel, true);
|
|
422
425
|
wakeEngine();
|
|
423
426
|
refreshPlans();
|
|
424
427
|
} else {
|
package/dashboard.js
CHANGED
|
@@ -1198,27 +1198,146 @@ function getDiskVersion() {
|
|
|
1198
1198
|
return _diskVersionCache;
|
|
1199
1199
|
}
|
|
1200
1200
|
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1201
|
+
// ── MCP server discovery ─────────────────────────────────────────────────────
|
|
1202
|
+
// Mirrors what each CLI actually loads at runtime:
|
|
1203
|
+
// • `claude mcp list` → user + plugin (+ workspace if cwd in project)
|
|
1204
|
+
// • `copilot mcp list --json` → user + workspace + plugin + builtin (merged)
|
|
1205
|
+
// • Each registered project's `<localPath>/.mcp.json` → workspace scope
|
|
1206
|
+
//
|
|
1207
|
+
// CLI calls are slow (claude health-checks every server on stdio), so we cache
|
|
1208
|
+
// for ~5 minutes and refresh in the background. getMcpServers() always returns
|
|
1209
|
+
// synchronously from cache; first call before background fill returns [].
|
|
1210
|
+
|
|
1211
|
+
const _MCP_CACHE_TTL = 5 * 60 * 1000;
|
|
1212
|
+
const _MCP_CLAUDE_TIMEOUT = 30000;
|
|
1213
|
+
const _MCP_COPILOT_TIMEOUT = 15000;
|
|
1214
|
+
let _mcpServersCache = null;
|
|
1215
|
+
let _mcpServersCacheTs = 0;
|
|
1216
|
+
let _mcpRefreshInflight = null;
|
|
1217
|
+
|
|
1218
|
+
function _parseClaudeMcpListLine(line) {
|
|
1219
|
+
const trimmed = (line || '').trim();
|
|
1220
|
+
if (!trimmed) return null;
|
|
1221
|
+
if (/^Checking\b/i.test(trimmed)) return null;
|
|
1222
|
+
if (/^No MCP servers/i.test(trimmed)) return null;
|
|
1223
|
+
const nameSep = trimmed.indexOf(': ');
|
|
1224
|
+
if (nameSep < 0) return null;
|
|
1225
|
+
const rawName = trimmed.slice(0, nameSep);
|
|
1226
|
+
let rest = trimmed.slice(nameSep + 2);
|
|
1227
|
+
let status = '';
|
|
1228
|
+
const statusSep = rest.lastIndexOf(' - ');
|
|
1229
|
+
if (statusSep >= 0) {
|
|
1230
|
+
status = rest.slice(statusSep + 3).trim();
|
|
1231
|
+
rest = rest.slice(0, statusSep).trim();
|
|
1232
|
+
}
|
|
1233
|
+
let source = 'Claude user';
|
|
1234
|
+
let displayName = rawName;
|
|
1235
|
+
if (rawName.startsWith('plugin:')) {
|
|
1236
|
+
const parts = rawName.split(':');
|
|
1237
|
+
source = parts.length >= 3 ? `Claude plugin: ${parts[1]}` : 'Claude plugin';
|
|
1238
|
+
displayName = parts.slice(2).join(':') || parts[1];
|
|
1239
|
+
} else if (rawName.startsWith('project:')) {
|
|
1240
|
+
const parts = rawName.split(':');
|
|
1241
|
+
source = parts.length >= 3 ? `Claude workspace: ${parts[1]}` : 'Claude workspace';
|
|
1242
|
+
displayName = parts.slice(2).join(':') || parts[1];
|
|
1243
|
+
}
|
|
1244
|
+
return { name: displayName, source, command: rest, args: '', status };
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
function _parseCopilotMcpListJson(raw) {
|
|
1248
|
+
try {
|
|
1249
|
+
const data = JSON.parse(raw);
|
|
1250
|
+
const servers = data?.mcpServers || {};
|
|
1251
|
+
return Object.entries(servers).map(([name, cfg]) => ({
|
|
1252
|
+
name,
|
|
1253
|
+
source: 'Copilot',
|
|
1254
|
+
command: cfg.command || cfg.url || '',
|
|
1255
|
+
args: Array.isArray(cfg.args) ? cfg.args.join(' ') : (cfg.args || ''),
|
|
1256
|
+
}));
|
|
1257
|
+
} catch { return []; }
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
function _runCliAsync(cmd, args, timeoutMs) {
|
|
1261
|
+
return new Promise(resolve => {
|
|
1262
|
+
try {
|
|
1263
|
+
const { exec } = require('child_process');
|
|
1264
|
+
// exec() runs through the shell so Windows resolves .cmd/.exe shims off
|
|
1265
|
+
// PATH. Args are static so there's no injection surface.
|
|
1266
|
+
const line = [cmd, ...args].join(' ');
|
|
1267
|
+
exec(line, {
|
|
1268
|
+
timeout: timeoutMs, windowsHide: true, encoding: 'utf8',
|
|
1269
|
+
maxBuffer: 1024 * 1024,
|
|
1270
|
+
}, (err, stdout) => resolve(err ? null : stdout));
|
|
1271
|
+
} catch { resolve(null); }
|
|
1272
|
+
});
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
function _readWorkspaceMcpServers(projects) {
|
|
1276
|
+
const out = [];
|
|
1277
|
+
for (const p of (projects || [])) {
|
|
1278
|
+
if (!p?.localPath) continue;
|
|
1279
|
+
const data = safeJsonObj(path.join(p.localPath, '.mcp.json'));
|
|
1280
|
+
const servers = data.mcpServers || {};
|
|
1281
|
+
for (const [name, cfg] of Object.entries(servers)) {
|
|
1282
|
+
out.push({
|
|
1283
|
+
name,
|
|
1284
|
+
source: `Workspace: ${p.name}`,
|
|
1285
|
+
command: cfg.command || cfg.url || '',
|
|
1286
|
+
args: Array.isArray(cfg.args) ? cfg.args.join(' ') : (cfg.args || ''),
|
|
1287
|
+
});
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
return out;
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
function _dedupeMcpServers(entries) {
|
|
1294
|
+
const seen = new Set();
|
|
1295
|
+
const out = [];
|
|
1296
|
+
for (const e of entries) {
|
|
1297
|
+
const key = `${e.source}::${e.name}`;
|
|
1298
|
+
if (seen.has(key)) continue;
|
|
1299
|
+
seen.add(key);
|
|
1300
|
+
out.push(e);
|
|
1301
|
+
}
|
|
1302
|
+
return out;
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
async function _refreshMcpServersCache() {
|
|
1306
|
+
if (_mcpRefreshInflight) return _mcpRefreshInflight;
|
|
1307
|
+
_mcpRefreshInflight = (async () => {
|
|
1308
|
+
try {
|
|
1309
|
+
const [claudeOut, copilotOut] = await Promise.all([
|
|
1310
|
+
_runCliAsync('claude', ['mcp', 'list'], _MCP_CLAUDE_TIMEOUT),
|
|
1311
|
+
_runCliAsync('copilot', ['mcp', 'list', '--json'], _MCP_COPILOT_TIMEOUT),
|
|
1312
|
+
]);
|
|
1313
|
+
const claudeEntries = (claudeOut || '').split(/\r?\n/).map(_parseClaudeMcpListLine).filter(Boolean);
|
|
1314
|
+
const copilotEntries = _parseCopilotMcpListJson(copilotOut || '{}');
|
|
1315
|
+
const workspaceEntries = _readWorkspaceMcpServers(PROJECTS);
|
|
1316
|
+
_mcpServersCache = _dedupeMcpServers([...claudeEntries, ...copilotEntries, ...workspaceEntries]);
|
|
1317
|
+
_mcpServersCacheTs = Date.now();
|
|
1318
|
+
} catch (e) {
|
|
1319
|
+
console.error('[mcp-discovery] refresh failed:', e.message?.split('\n')?.[0]);
|
|
1320
|
+
if (!_mcpServersCache) _mcpServersCache = [];
|
|
1321
|
+
_mcpServersCacheTs = Date.now();
|
|
1322
|
+
} finally {
|
|
1323
|
+
_mcpRefreshInflight = null;
|
|
1324
|
+
}
|
|
1325
|
+
})();
|
|
1326
|
+
return _mcpRefreshInflight;
|
|
1210
1327
|
}
|
|
1211
1328
|
|
|
1212
1329
|
function getMcpServers() {
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
];
|
|
1219
|
-
} catch { return []; }
|
|
1330
|
+
const now = Date.now();
|
|
1331
|
+
if (!_mcpServersCache || (now - _mcpServersCacheTs) >= _MCP_CACHE_TTL) {
|
|
1332
|
+
_refreshMcpServersCache().catch(() => {});
|
|
1333
|
+
}
|
|
1334
|
+
return _mcpServersCache || [];
|
|
1220
1335
|
}
|
|
1221
1336
|
|
|
1337
|
+
// Kick off first discovery on startup so the dashboard has data before the
|
|
1338
|
+
// first slow-state rebuild.
|
|
1339
|
+
_refreshMcpServersCache().catch(() => {});
|
|
1340
|
+
|
|
1222
1341
|
function parsePinnedEntries(content) {
|
|
1223
1342
|
if (!content) return [];
|
|
1224
1343
|
const entries = [];
|
|
@@ -4574,7 +4693,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
4574
4693
|
const projectTarget = resolveProjectSourceTarget(projectName, PROJECTS, { allowCentral: false });
|
|
4575
4694
|
const targetProject = projectTarget.project || (PROJECTS.length === 1 ? PROJECTS[0] : null);
|
|
4576
4695
|
if (targetProject) {
|
|
4577
|
-
|
|
4696
|
+
const diffAwareResult = shared.queuePlanToPrd({
|
|
4578
4697
|
planFile: plan.source_plan, prdFile: body.file,
|
|
4579
4698
|
title: `Update PRD from revised plan: ${plan.source_plan}`,
|
|
4580
4699
|
description: `mode: diff-aware-update\nPlan file: plans/${plan.source_plan}\nPRD file: prd/${body.file}\n\n` +
|
|
@@ -4584,6 +4703,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
4584
4703
|
project: targetProject.name, createdBy: 'dashboard:plan-resume',
|
|
4585
4704
|
extra: { _existingPrdFile: body.file },
|
|
4586
4705
|
});
|
|
4706
|
+
diffAwareQueued = diffAwareResult.queued;
|
|
4587
4707
|
}
|
|
4588
4708
|
}
|
|
4589
4709
|
|
|
@@ -4731,15 +4851,15 @@ const server = http.createServer(async (req, res) => {
|
|
|
4731
4851
|
'\n\nA prior PRD already exists for this plan. Read it, apply this feedback, and overwrite the PRD with the corrected version.';
|
|
4732
4852
|
}
|
|
4733
4853
|
|
|
4734
|
-
const
|
|
4854
|
+
const queueResult = shared.queuePlanToPrd({
|
|
4735
4855
|
planFile: body.file,
|
|
4736
4856
|
title: 'Convert plan to PRD: ' + body.file.replace('.md', ''),
|
|
4737
4857
|
description,
|
|
4738
4858
|
project: declaredProject || body.project || '', createdBy: 'dashboard:execute',
|
|
4739
4859
|
});
|
|
4740
|
-
if (!queued) return jsonReply(res, 200, { ok: true, alreadyQueued: true });
|
|
4860
|
+
if (!queueResult.queued) return jsonReply(res, 200, { ok: true, alreadyQueued: true, id: queueResult.id });
|
|
4741
4861
|
invalidateStatusCache();
|
|
4742
|
-
return jsonReply(res, 200, { ok: true });
|
|
4862
|
+
return jsonReply(res, 200, { ok: true, id: queueResult.id });
|
|
4743
4863
|
} catch (e) { return jsonReply(res, 400, { error: e.message }); }
|
|
4744
4864
|
}
|
|
4745
4865
|
|
|
@@ -7782,6 +7902,10 @@ function _installCrashHandlers() {
|
|
|
7782
7902
|
// Production entry points use the closures directly; tests import via require('./dashboard').
|
|
7783
7903
|
module.exports = {
|
|
7784
7904
|
getMcpServers,
|
|
7905
|
+
_parseClaudeMcpListLine,
|
|
7906
|
+
_parseCopilotMcpListJson,
|
|
7907
|
+
_readWorkspaceMcpServers,
|
|
7908
|
+
_dedupeMcpServers,
|
|
7785
7909
|
readBody,
|
|
7786
7910
|
_filterCcTabSessions,
|
|
7787
7911
|
_getVersionCheckInterval,
|
package/engine/shared.js
CHANGED
|
@@ -1628,17 +1628,30 @@ function trackReviewMetric(pr, newReviewStatus, config) {
|
|
|
1628
1628
|
} catch (err) { log('warn', `Metrics update: ${err.message}`); }
|
|
1629
1629
|
}
|
|
1630
1630
|
|
|
1631
|
-
/**
|
|
1631
|
+
/**
|
|
1632
|
+
* Queue a plan-to-prd work item with dedup check inside lock.
|
|
1633
|
+
*
|
|
1634
|
+
* Returns { queued, alreadyQueued, id, item } so callers (e.g. POST /api/plans/execute)
|
|
1635
|
+
* can surface the new or existing work item id back to the dashboard. Returning
|
|
1636
|
+
* just a boolean caused the toast to render "queued undefined" (issue #2375).
|
|
1637
|
+
*/
|
|
1632
1638
|
function queuePlanToPrd({ planFile, prdFile, title, description, project, createdBy, extra }) {
|
|
1633
1639
|
// Use MINIONS_DIR (honors MINIONS_TEST_DIR override) instead of resolving from
|
|
1634
1640
|
// __dirname — otherwise tests that exercise this helper leak work items into
|
|
1635
1641
|
// the real package-root work-items.json even after createTestMinionsDir().
|
|
1636
1642
|
const centralWiPath = path.join(MINIONS_DIR, 'work-items.json');
|
|
1637
1643
|
let queued = false;
|
|
1644
|
+
let id = null;
|
|
1645
|
+
let item = null;
|
|
1638
1646
|
mutateJsonFileLocked(centralWiPath, items => {
|
|
1639
1647
|
if (!Array.isArray(items)) items = [];
|
|
1640
|
-
|
|
1641
|
-
|
|
1648
|
+
const existing = items.find(w => w.type === 'plan-to-prd' && w.planFile === planFile && (w.status === 'pending' || w.status === 'dispatched'));
|
|
1649
|
+
if (existing) {
|
|
1650
|
+
id = existing.id;
|
|
1651
|
+
item = existing;
|
|
1652
|
+
return items;
|
|
1653
|
+
}
|
|
1654
|
+
const newItem = {
|
|
1642
1655
|
id: 'W-' + uid(),
|
|
1643
1656
|
title,
|
|
1644
1657
|
type: 'plan-to-prd',
|
|
@@ -1650,11 +1663,14 @@ function queuePlanToPrd({ planFile, prdFile, title, description, project, create
|
|
|
1650
1663
|
project,
|
|
1651
1664
|
planFile,
|
|
1652
1665
|
...(extra || {}),
|
|
1653
|
-
}
|
|
1666
|
+
};
|
|
1667
|
+
items.push(newItem);
|
|
1654
1668
|
queued = true;
|
|
1669
|
+
id = newItem.id;
|
|
1670
|
+
item = newItem;
|
|
1655
1671
|
return items;
|
|
1656
1672
|
}, { defaultValue: [] });
|
|
1657
|
-
return queued;
|
|
1673
|
+
return { queued, alreadyQueued: !queued, id, item };
|
|
1658
1674
|
}
|
|
1659
1675
|
|
|
1660
1676
|
function extractPlanDeclaredProject(planContent) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1898",
|
|
4
4
|
"description": "Multi-agent AI dev team that runs from ~/.minions/ — five autonomous agents share a single engine, dashboard, and knowledge base",
|
|
5
5
|
"bin": {
|
|
6
6
|
"minions": "bin/minions.js"
|