@yemi33/minions 0.1.1897 → 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 +139 -16
- 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) {
|
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 = [];
|
|
@@ -7783,6 +7902,10 @@ function _installCrashHandlers() {
|
|
|
7783
7902
|
// Production entry points use the closures directly; tests import via require('./dashboard').
|
|
7784
7903
|
module.exports = {
|
|
7785
7904
|
getMcpServers,
|
|
7905
|
+
_parseClaudeMcpListLine,
|
|
7906
|
+
_parseCopilotMcpListJson,
|
|
7907
|
+
_readWorkspaceMcpServers,
|
|
7908
|
+
_dedupeMcpServers,
|
|
7786
7909
|
readBody,
|
|
7787
7910
|
_filterCcTabSessions,
|
|
7788
7911
|
_getVersionCheckInterval,
|
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"
|