@yemi33/minions 0.1.1924 → 0.1.1925

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 CHANGED
@@ -1190,21 +1190,24 @@ function getDiskVersion() {
1190
1190
  }
1191
1191
 
1192
1192
  // ── MCP server discovery ─────────────────────────────────────────────────────
1193
- // Mirrors what each CLI actually loads at runtime:
1194
- // • `claude mcp list` user + plugin (+ workspace if cwd in project)
1195
- // • `copilot mcp list --json` → user + workspace + plugin + builtin (merged)
1193
+ // Reads MCP server registrations directly from each CLI's config file:
1194
+ // • `~/.claude.json` `mcpServers` (Claude user scope)
1195
+ // • `~/.copilot/mcp-config.json` → `mcpServers` (Copilot user scope)
1196
1196
  // • Each registered project's `<localPath>/.mcp.json` → workspace scope
1197
1197
  //
1198
- // CLI calls are slow (claude health-checks every server on stdio), so we cache
1199
- // for ~5 minutes and refresh in the background. getMcpServers() always returns
1200
- // synchronously from cache; first call before background fill returns [].
1198
+ // We used to shell out to `claude mcp list` + `copilot mcp list --json`, but
1199
+ // `claude mcp list` blocks on a TTY check when the dashboard daemon has no
1200
+ // parent console (bin/minions.js spawns it with windowsHide:true), so every
1201
+ // refresh timed out and the cache stayed empty. File reads are zero-spawn and
1202
+ // the config files are the authoritative source for registration.
1203
+ //
1204
+ // Cost: we lose live connection status (✓ Connected / ! Needs auth) and the
1205
+ // Claude plugin: <plugin>:<server> rows. The UI shows status only in a hover
1206
+ // tooltip, and plugin servers are rare; reliability matters more.
1201
1207
 
1202
1208
  const _MCP_CACHE_TTL = 5 * 60 * 1000;
1203
- const _MCP_CLAUDE_TIMEOUT = 30000;
1204
- const _MCP_COPILOT_TIMEOUT = 15000;
1205
1209
  let _mcpServersCache = null;
1206
1210
  let _mcpServersCacheTs = 0;
1207
- let _mcpRefreshInflight = null;
1208
1211
 
1209
1212
  function _parseClaudeMcpListLine(line) {
1210
1213
  const trimmed = (line || '').trim();
@@ -1248,19 +1251,43 @@ function _parseCopilotMcpListJson(raw) {
1248
1251
  } catch { return []; }
1249
1252
  }
1250
1253
 
1251
- function _runCliAsync(cmd, args, timeoutMs) {
1252
- return new Promise(resolve => {
1253
- try {
1254
- const { exec } = require('child_process');
1255
- // exec() runs through the shell so Windows resolves .cmd/.exe shims off
1256
- // PATH. Args are static so there's no injection surface.
1257
- const line = [cmd, ...args].join(' ');
1258
- exec(line, {
1259
- timeout: timeoutMs, windowsHide: true, encoding: 'utf8',
1260
- maxBuffer: 1024 * 1024,
1261
- }, (err, stdout) => resolve(err ? null : stdout));
1262
- } catch { resolve(null); }
1263
- });
1254
+ // Read Claude's user-scope MCP servers from `~/.claude.json` directly.
1255
+ // Shelling out to `claude mcp list` is unreliable from the dashboard daemon —
1256
+ // the CLI's MCP health probes block on a TTY check when the parent has no
1257
+ // console (bin/minions.js launches the dashboard with windowsHide:true), and
1258
+ // neither exec() nor spawn(..., {detached:true}) reliably sidesteps it.
1259
+ // Config-file reads are zero-spawn, instant, and the file is the authoritative
1260
+ // source for which servers are registered.
1261
+ function _readClaudeUserMcpServers() {
1262
+ try {
1263
+ const data = safeJsonObj(path.join(os.homedir(), '.claude.json'));
1264
+ const servers = data?.mcpServers || {};
1265
+ return Object.entries(servers).map(([name, cfg]) => ({
1266
+ name,
1267
+ source: 'Claude user',
1268
+ command: cfg.command || cfg.url || '',
1269
+ args: Array.isArray(cfg.args) ? cfg.args.join(' ') : (cfg.args || ''),
1270
+ }));
1271
+ } catch { return []; }
1272
+ }
1273
+
1274
+ // Copilot stores its MCP config at `~/.copilot/mcp-config.json` (may have a
1275
+ // UTF-8 BOM — Windows tooling tends to write one).
1276
+ function _readCopilotUserMcpServers() {
1277
+ try {
1278
+ const configPath = path.join(os.homedir(), '.copilot', 'mcp-config.json');
1279
+ if (!fs.existsSync(configPath)) return [];
1280
+ let raw = fs.readFileSync(configPath, 'utf8');
1281
+ if (raw.charCodeAt(0) === 0xFEFF) raw = raw.slice(1);
1282
+ const data = JSON.parse(raw);
1283
+ const servers = data?.mcpServers || {};
1284
+ return Object.entries(servers).map(([name, cfg]) => ({
1285
+ name,
1286
+ source: 'Copilot',
1287
+ command: cfg.command || cfg.url || '',
1288
+ args: Array.isArray(cfg.args) ? cfg.args.join(' ') : (cfg.args || ''),
1289
+ }));
1290
+ } catch { return []; }
1264
1291
  }
1265
1292
 
1266
1293
  function _readWorkspaceMcpServers(projects) {
@@ -1293,42 +1320,26 @@ function _dedupeMcpServers(entries) {
1293
1320
  return out;
1294
1321
  }
1295
1322
 
1296
- async function _refreshMcpServersCache() {
1297
- if (_mcpRefreshInflight) return _mcpRefreshInflight;
1298
- _mcpRefreshInflight = (async () => {
1299
- try {
1300
- const [claudeOut, copilotOut] = await Promise.all([
1301
- _runCliAsync('claude', ['mcp', 'list'], _MCP_CLAUDE_TIMEOUT),
1302
- _runCliAsync('copilot', ['mcp', 'list', '--json'], _MCP_COPILOT_TIMEOUT),
1303
- ]);
1304
- const claudeEntries = (claudeOut || '').split(/\r?\n/).map(_parseClaudeMcpListLine).filter(Boolean);
1305
- const copilotEntries = _parseCopilotMcpListJson(copilotOut || '{}');
1306
- const workspaceEntries = _readWorkspaceMcpServers(PROJECTS);
1307
- _mcpServersCache = _dedupeMcpServers([...claudeEntries, ...copilotEntries, ...workspaceEntries]);
1308
- _mcpServersCacheTs = Date.now();
1309
- } catch (e) {
1310
- console.error('[mcp-discovery] refresh failed:', e.message?.split('\n')?.[0]);
1311
- if (!_mcpServersCache) _mcpServersCache = [];
1312
- _mcpServersCacheTs = Date.now();
1313
- } finally {
1314
- _mcpRefreshInflight = null;
1315
- }
1316
- })();
1317
- return _mcpRefreshInflight;
1318
- }
1319
-
1320
1323
  function getMcpServers() {
1321
1324
  const now = Date.now();
1322
- if (!_mcpServersCache || (now - _mcpServersCacheTs) >= _MCP_CACHE_TTL) {
1323
- _refreshMcpServersCache().catch(() => {});
1325
+ if (_mcpServersCache && (now - _mcpServersCacheTs) < _MCP_CACHE_TTL) {
1326
+ return _mcpServersCache;
1327
+ }
1328
+ try {
1329
+ const entries = [
1330
+ ..._readClaudeUserMcpServers(),
1331
+ ..._readCopilotUserMcpServers(),
1332
+ ..._readWorkspaceMcpServers(PROJECTS),
1333
+ ];
1334
+ _mcpServersCache = _dedupeMcpServers(entries);
1335
+ } catch (e) {
1336
+ console.error('[mcp-discovery] refresh failed:', e.message?.split('\n')?.[0]);
1337
+ if (!_mcpServersCache) _mcpServersCache = [];
1324
1338
  }
1325
- return _mcpServersCache || [];
1339
+ _mcpServersCacheTs = now;
1340
+ return _mcpServersCache;
1326
1341
  }
1327
1342
 
1328
- // Kick off first discovery on startup so the dashboard has data before the
1329
- // first slow-state rebuild.
1330
- _refreshMcpServersCache().catch(() => {});
1331
-
1332
1343
  function parsePinnedEntries(content) {
1333
1344
  if (!content) return [];
1334
1345
  const entries = [];
@@ -0,0 +1,5 @@
1
+ {
2
+ "runtime": "copilot",
3
+ "models": null,
4
+ "cachedAt": "2026-05-14T02:03:25.197Z"
5
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.1924",
3
+ "version": "0.1.1925",
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"