agentaudit 3.10.9 → 3.12.0

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/cli.mjs CHANGED
@@ -5,7 +5,7 @@
5
5
  * Usage: agentaudit <command> [options]
6
6
  *
7
7
  * Commands:
8
- * discover Find MCP servers in AI editors
8
+ * discover Find MCP servers across all AI tools
9
9
  * scan <url> [url...] Quick static scan (regex)
10
10
  * audit <url> [url...] Deep LLM-powered security audit
11
11
  * lookup <name> Look up package in registry
@@ -17,6 +17,7 @@
17
17
  * model [name|reset] Configure LLM provider + model
18
18
  * setup Log in to agentaudit.dev (for report uploads)
19
19
  * status Show current config + auth status
20
+ * profile Your profile — rank, points, audit stats
20
21
  * help [command] Show help
21
22
  *
22
23
  * Flags: --json, --quiet, --no-color, --no-upload, --model, --export, --debug
@@ -435,26 +436,31 @@ function singleSelect(items, { title = 'Select', hint = '↑↓=move Enter=sele
435
436
  });
436
437
  }
437
438
 
438
- async function registerAgent(agentName) {
439
- const res = await fetch(`${REGISTRY_URL}/api/register`, {
440
- method: 'POST',
441
- headers: { 'Content-Type': 'application/json' },
442
- body: JSON.stringify({ agent_name: agentName }),
443
- signal: AbortSignal.timeout(15_000),
444
- });
445
- if (!res.ok) throw new Error(`Registration failed (HTTP ${res.status}): ${await res.text()}`);
446
- return res.json();
439
+ async function validateApiKey(apiKey) {
440
+ try {
441
+ const res = await fetch(`${REGISTRY_URL}/api/auth/validate`, {
442
+ headers: { 'Authorization': `Bearer ${apiKey}` },
443
+ signal: AbortSignal.timeout(10_000),
444
+ });
445
+ if (res.ok) {
446
+ const data = await res.json();
447
+ return { valid: true, agent_name: data.agent_name || null };
448
+ }
449
+ return { valid: false, agent_name: null };
450
+ } catch {
451
+ return { valid: false, agent_name: null };
452
+ }
447
453
  }
448
454
 
449
455
  async function setupCommand() {
450
- console.log(` ${c.bold}AgentAudit Login${c.reset}`);
451
- console.log(` ${c.dim}Create an account to upload audit reports to agentaudit.dev${c.reset}`);
456
+ console.log(` ${c.bold}AgentAudit Setup${c.reset}`);
457
+ console.log(` ${c.dim}Link your API key to upload audit reports to agentaudit.dev${c.reset}`);
452
458
  console.log();
453
459
 
454
460
  const existing = loadCredentials();
455
461
  if (existing) {
456
- console.log(` ${icons.safe} Already logged in as ${c.bold}${existing.agent_name}${c.reset}`);
457
- console.log(` ${c.dim}Key: ${existing.api_key.slice(0, 8)}...${c.reset}`);
462
+ console.log(` ${icons.safe} Already configured as ${c.bold}${existing.agent_name}${c.reset}`);
463
+ console.log(` ${c.dim}Key: ${existing.api_key.slice(0, 12)}...${c.reset}`);
458
464
  console.log();
459
465
  const answer = await askQuestion(` Reconfigure? ${c.dim}(y/N)${c.reset} `);
460
466
  if (answer.toLowerCase() !== 'y') {
@@ -464,39 +470,28 @@ async function setupCommand() {
464
470
  console.log();
465
471
  }
466
472
 
467
- console.log(` ${c.bold}1)${c.reset} Register new agent ${c.dim}(free, creates API key automatically)${c.reset}`);
468
- console.log(` ${c.bold}2)${c.reset} Enter existing API key`);
469
- console.log();
470
- const choice = await askQuestion(` Choice ${c.dim}(1/2)${c.reset}: `);
473
+ console.log(` ${c.bold}Step 1:${c.reset} Create an API key at ${c.cyan}${REGISTRY_URL}/profile${c.reset}`);
474
+ console.log(` ${c.dim}Sign in with GitHub, then click "Create API Key".${c.reset}`);
471
475
  console.log();
476
+ const key = await askQuestion(` ${c.bold}Step 2:${c.reset} Paste your API key here: `);
477
+ if (!key || !key.trim()) {
478
+ console.log(` ${c.red}No key entered.${c.reset}`);
479
+ return;
480
+ }
472
481
 
473
- if (choice === '2') {
474
- const key = await askQuestion(` API Key: `);
475
- if (!key) { console.log(` ${c.red}No key entered.${c.reset}`); return; }
476
- const name = await askQuestion(` Agent name ${c.dim}(optional)${c.reset}: `);
477
- saveCredentials({ api_key: key, agent_name: name || 'custom' });
482
+ process.stdout.write(` Validating...`);
483
+ const validation = await validateApiKey(key.trim());
484
+ if (validation.valid) {
485
+ const agentName = validation.agent_name || 'agent';
486
+ saveCredentials({ api_key: key.trim(), agent_name: agentName });
487
+ console.log(` ${c.green}valid!${c.reset}`);
478
488
  console.log();
479
- console.log(` ${icons.safe} Saved! Key stored in ${c.dim}${USER_CRED_FILE}${c.reset}`);
489
+ console.log(` ${icons.safe} Logged in as ${c.bold}${agentName}${c.reset}`);
490
+ console.log(` ${c.dim}Key saved to: ${USER_CRED_FILE}${c.reset}`);
480
491
  } else {
481
- const name = await askQuestion(` Agent name ${c.dim}(e.g. my-scanner, claude-desktop)${c.reset}: `);
482
- if (!name || !/^[a-zA-Z0-9._-]{2,64}$/.test(name)) {
483
- console.log(` ${c.red}Invalid name. Use 2-64 chars: letters, numbers, dash, underscore, dot.${c.reset}`);
484
- return;
485
- }
486
- process.stdout.write(` Registering ${c.bold}${name}${c.reset}...`);
487
- try {
488
- const data = await registerAgent(name);
489
- saveCredentials({ api_key: data.api_key, agent_name: data.agent_name });
490
- console.log(` ${c.green}done!${c.reset}`);
491
- console.log();
492
- console.log(` ${icons.safe} Registered as ${c.bold}${data.agent_name}${c.reset}`);
493
- console.log(` ${c.dim}Key: ${data.api_key.slice(0, 12)}...${c.reset}`);
494
- console.log(` ${c.dim}Saved to: ${USER_CRED_FILE}${c.reset}`);
495
- } catch (err) {
496
- console.log(` ${c.red}failed${c.reset}`);
497
- console.log(` ${c.red}${err.message}${c.reset}`);
498
- return;
499
- }
492
+ console.log(` ${c.red}invalid${c.reset}`);
493
+ console.log(` ${c.red}Key not recognized. Make sure you copied the full key from ${REGISTRY_URL}/profile${c.reset}`);
494
+ return;
500
495
  }
501
496
 
502
497
  console.log();
@@ -560,7 +555,7 @@ function banner() {
560
555
  const ptsStr = `${fmtNum(cache.total_points)}pts`;
561
556
  const auditsStr = `${fmtNum(cache.total_reports)} audits`;
562
557
  const profile = [cache.agent_name, rankStr, ptsStr, auditsStr].filter(Boolean).join(' \u00b7 ');
563
- console.log(` ${c.bold}${c.cyan}\u26e8 AgentAudit${c.reset} ${c.dim}v${getVersion()}${c.reset} ${c.dim}\u2502${c.reset} ${profile}`);
558
+ console.log(` ${c.bold}${c.cyan}\u25c6 AgentAudit${c.reset} ${c.dim}v${getVersion()}${c.reset} ${c.dim}\u2502${c.reset} ${profile}`);
564
559
  } else {
565
560
  console.log(` ${c.bold}${c.cyan}AgentAudit${c.reset} ${c.dim}v${getVersion()}${c.reset}`);
566
561
  console.log(` ${c.dim}Security scanner for AI packages${c.reset}`);
@@ -581,10 +576,14 @@ function elapsed(startMs) {
581
576
  }
582
577
 
583
578
  function riskBadge(score) {
584
- if (score === 0) return `${c.bgGreen}${c.bold}${c.white} SAFE ${c.reset}`;
585
- if (score <= 10) return `${c.bgGreen}${c.white} LOW ${c.reset}`;
586
- if (score <= 30) return `${c.bgYellow}${c.bold} CAUTION ${c.reset}`;
587
- return `${c.bgRed}${c.bold}${c.white} UNSAFE ${c.reset}`;
579
+ const badge = score === 0 ? `${c.bgGreen}${c.bold}${c.white} SAFE ${c.reset}`
580
+ : score <= 10 ? `${c.bgGreen}${c.white} LOW ${c.reset}`
581
+ : score <= 30 ? `${c.bgYellow}${c.bold} CAUTION ${c.reset}`
582
+ : `${c.bgRed}${c.bold}${c.white} UNSAFE ${c.reset}`;
583
+ const filled = Math.min(Math.round(score / 20), 5);
584
+ const gaugeColor = score <= 10 ? c.green : score <= 30 ? c.yellow : c.red;
585
+ const gauge = `${gaugeColor}${'▰'.repeat(filled)}${c.dim}${'▱'.repeat(5 - filled)}${c.reset}`;
586
+ return `${badge} ${gauge} ${score}/100`;
588
587
  }
589
588
 
590
589
  function severityColor(sev) {
@@ -701,6 +700,47 @@ function sparkline(values) {
701
700
  }).join('');
702
701
  }
703
702
 
703
+ // ─── Section Header ─────────── labeled divider
704
+ function sectionHeader(title, width = 60) {
705
+ const dashAfter = Math.max(3, width - 5 - title.length);
706
+ return ` ${c.dim}───${c.reset} ${c.bold}${title}${c.reset} ${c.dim}${'─'.repeat(dashAfter)}${c.reset}`;
707
+ }
708
+
709
+ // █████░░░░░░░░░░░░░░ coverage bar
710
+ function coverageBar(filled, total, width = 20) {
711
+ if (total === 0) return `${c.dim}${'░'.repeat(width)}${c.reset} 0/0`;
712
+ const barFilled = Math.max(filled > 0 ? 1 : 0, Math.round((filled / total) * width));
713
+ const barEmpty = width - barFilled;
714
+ const pct = Math.round((filled / total) * 100);
715
+ const color = pct >= 80 ? c.green : pct >= 50 ? c.yellow : c.red;
716
+ return `${color}${'█'.repeat(barFilled)}${c.dim}${'░'.repeat(barEmpty)}${c.reset} ${filled}/${total} (${pct}%)`;
717
+ }
718
+
719
+ // Severity histogram for findings
720
+ function severityHistogram(findings) {
721
+ const counts = { critical: 0, high: 0, medium: 0, low: 0 };
722
+ for (const f of findings) {
723
+ const sev = (f.severity || '').toLowerCase();
724
+ if (counts[sev] !== undefined) counts[sev]++;
725
+ }
726
+ const max = Math.max(...Object.values(counts), 1);
727
+ const lines = [];
728
+ for (const sev of ['critical', 'high', 'medium', 'low']) {
729
+ const count = counts[sev];
730
+ if (count === 0) continue;
731
+ const barLen = Math.max(1, Math.round((count / max) * 24));
732
+ const sc = severityColor(sev);
733
+ const label = sev.toUpperCase().padEnd(10);
734
+ lines.push(` ${sc}${label}${c.reset} ${sc}${'█'.repeat(barLen)}${c.reset}${' '.repeat(24 - barLen)} ${count}`);
735
+ }
736
+ return lines;
737
+ }
738
+
739
+ // ▰▰▰▱ step progress indicator
740
+ function stepProgress(current, total) {
741
+ return `${c.cyan}${'▰'.repeat(current)}${c.dim}${'▱'.repeat(total - current)}${c.reset}`;
742
+ }
743
+
704
744
  function fmtNum(n) {
705
745
  if (n == null) return '0';
706
746
  return n.toLocaleString('en-US');
@@ -715,7 +755,7 @@ function dashboardBanner() {
715
755
  const ver = getVersion();
716
756
  return [
717
757
  ` ${BOX.tl}${c.dim}${BOX.h.repeat(35)}${c.reset}${BOX.tr}`,
718
- ` ${BOX.v} ${c.bold}${c.cyan} AgentAudit${c.reset} ${c.dim}v${ver}${c.reset}${' '.repeat(Math.max(0, 19 - ver.length))}${BOX.v}`,
758
+ ` ${BOX.v} ${c.bold}${c.cyan} AgentAudit${c.reset} ${c.dim}v${ver}${c.reset}${' '.repeat(Math.max(0, 19 - ver.length))}${BOX.v}`,
719
759
  ` ${BOX.v} ${c.dim}Security Registry for AI Agents${c.reset} ${BOX.v}`,
720
760
  ` ${BOX.bl}${c.dim}${BOX.h.repeat(35)}${c.reset}${BOX.br}`,
721
761
  ];
@@ -1056,7 +1096,7 @@ function quickChecks(files) {
1056
1096
 
1057
1097
  async function checkRegistry(slug) {
1058
1098
  try {
1059
- const res = await fetch(`${REGISTRY_URL}/api/skills/${encodeURIComponent(slug)}`, {
1099
+ const res = await fetch(`${REGISTRY_URL}/api/packages/${encodeURIComponent(slug)}`, {
1060
1100
  signal: AbortSignal.timeout(5000),
1061
1101
  });
1062
1102
  if (res.ok) return await res.json();
@@ -1126,31 +1166,38 @@ function printScanResult(url, info, files, findings, registryData, duration) {
1126
1166
  console.log(`${icons.pipe} ${c.dim}(no tools or prompts detected)${c.reset}`);
1127
1167
  }
1128
1168
 
1129
- // Findings
1169
+ // Findings with severity stripe
1130
1170
  if (findings.length > 0) {
1131
- console.log(`${icons.pipe}`);
1132
- console.log(`${icons.pipe} ${c.bold}Findings (${findings.length})${c.reset} ${c.dim}static analysis — may include false positives${c.reset}`);
1133
- for (let i = 0; i < findings.length; i++) {
1134
- const f = findings[i];
1135
- const isLast = i === findings.length - 1;
1136
- const branch = isLast ? icons.treeLast : icons.tree;
1137
- const pipeOrSpace = isLast ? ' ' : `${icons.pipe} `;
1171
+ console.log();
1172
+ console.log(sectionHeader(`Findings (${findings.length})`));
1173
+ console.log(` ${c.dim}static analysis may include false positives${c.reset}`);
1174
+ console.log();
1175
+ for (const f of findings) {
1138
1176
  const sc = severityColor(f.severity);
1139
- console.log(`${branch} ${severityIcon(f.severity)} ${sc}${f.severity.toUpperCase().padEnd(8)}${c.reset} ${f.title}`);
1140
- console.log(`${pipeOrSpace} ${c.dim}${f.file}:${f.line}${c.reset} ${c.dim}${f.snippet || ''}${c.reset}`);
1177
+ console.log(` ${sc}┃${c.reset} ${sc}${f.severity.toUpperCase().padEnd(8)}${c.reset} ${c.bold}${f.title}${c.reset}`);
1178
+ console.log(` ${sc}┃${c.reset} ${c.dim}${f.file}:${f.line}${c.reset} ${c.dim}${f.snippet || ''}${c.reset}`);
1179
+ }
1180
+
1181
+ // Severity histogram
1182
+ const histLines = severityHistogram(findings);
1183
+ if (histLines.length > 1) {
1184
+ console.log();
1185
+ console.log(sectionHeader('Severity'));
1186
+ for (const line of histLines) console.log(line);
1141
1187
  }
1142
1188
  }
1143
-
1189
+
1144
1190
  // Registry status
1145
- console.log(`${icons.pipe}`);
1191
+ console.log();
1192
+ console.log(sectionHeader('Registry'));
1146
1193
  if (registryData) {
1147
1194
  const rd = registryData;
1148
1195
  const riskScore = rd.risk_score ?? rd.latest_risk_score ?? 0;
1149
- console.log(`${icons.treeLast} ${c.dim}registry${c.reset} ${riskBadge(riskScore)} Risk ${riskScore} ${c.dim}${REGISTRY_URL}/skills/${slug}${c.reset}`);
1196
+ console.log(` ${riskBadge(riskScore)} ${c.dim}${REGISTRY_URL}/packages/${slug}${c.reset}`);
1150
1197
  } else {
1151
- console.log(`${icons.treeLast} ${c.dim}registry${c.reset} ${c.dim}not audited yet${c.reset}`);
1198
+ console.log(` ${c.dim}not audited yet${c.reset}`);
1152
1199
  }
1153
-
1200
+
1154
1201
  console.log();
1155
1202
  }
1156
1203
 
@@ -1159,27 +1206,23 @@ function printSummary(results) {
1159
1206
  const safe = results.filter(r => r.findings.length === 0).length;
1160
1207
  const withFindings = total - safe;
1161
1208
  const totalFindings = results.reduce((sum, r) => sum + r.findings.length, 0);
1162
-
1163
- console.log(`${c.dim}${'─'.repeat(60)}${c.reset}`);
1164
- console.log(` ${c.bold}Summary${c.reset} ${total} packages scanned`);
1209
+ const allFindings = results.flatMap(r => r.findings);
1210
+
1211
+ console.log(sectionHeader(`Summary${total} packages scanned`));
1165
1212
  console.log();
1166
1213
  if (safe > 0) console.log(` ${icons.safe} ${c.green}${safe} clean${c.reset}`);
1167
1214
  if (withFindings > 0) console.log(` ${icons.caution} ${c.yellow}${withFindings} with findings${c.reset} (${totalFindings} total)`);
1168
-
1169
- // Breakdown by severity
1170
- const bySev = {};
1171
- results.forEach(r => r.findings.forEach(f => {
1172
- bySev[f.severity] = (bySev[f.severity] || 0) + 1;
1173
- }));
1174
- if (Object.keys(bySev).length > 0) {
1215
+ console.log();
1216
+ console.log(` ${coverageBar(safe, total)}`);
1217
+
1218
+ // Severity histogram
1219
+ const histLines = severityHistogram(allFindings);
1220
+ if (histLines.length > 0) {
1175
1221
  console.log();
1176
- for (const sev of ['critical', 'high', 'medium', 'low']) {
1177
- if (bySev[sev]) {
1178
- console.log(` ${severityIcon(sev)} ${bySev[sev]}× ${severityColor(sev)}${sev}${c.reset}`);
1179
- }
1180
- }
1222
+ console.log(sectionHeader('Severity'));
1223
+ for (const line of histLines) console.log(line);
1181
1224
  }
1182
-
1225
+
1183
1226
  console.log();
1184
1227
  }
1185
1228
 
@@ -1236,48 +1279,290 @@ async function scanRepo(url) {
1236
1279
 
1237
1280
  // ── Discover local MCP configs ──────────────────────────
1238
1281
 
1282
+ /**
1283
+ * Minimal YAML parser — extracts MCP server list entries from
1284
+ * Continue.dev (mcpServers: list) and Goose (extensions: list).
1285
+ * Zero dependencies. Only handles the subset of YAML used by these tools.
1286
+ */
1287
+ function parseSimpleYaml(text, rootKey) {
1288
+ const result = { mcpServers: {} };
1289
+ const lines = text.split('\n');
1290
+ let inSection = false;
1291
+ let currentName = null;
1292
+ let currentServer = {};
1293
+ let collectingArgs = false;
1294
+ let argsIndent = -1;
1295
+
1296
+ for (const line of lines) {
1297
+ const trimmed = line.trimEnd();
1298
+ if (trimmed === '' || /^\s*#/.test(trimmed)) continue;
1299
+ const indent = line.search(/\S/);
1300
+
1301
+ if (indent === 0 && trimmed === rootKey + ':') { inSection = true; continue; }
1302
+ if (indent === 0 && inSection && /^\w/.test(trimmed)) {
1303
+ if (currentName) result.mcpServers[currentName] = currentServer;
1304
+ break;
1305
+ }
1306
+ if (!inSection) continue;
1307
+
1308
+ const nameMatch = trimmed.match(/^\s*-\s+name:\s*(.+)/);
1309
+ if (nameMatch) {
1310
+ if (currentName) result.mcpServers[currentName] = currentServer;
1311
+ currentName = nameMatch[1].replace(/^["']|["']$/g, '');
1312
+ currentServer = {};
1313
+ collectingArgs = false;
1314
+ continue;
1315
+ }
1316
+
1317
+ if (collectingArgs && indent > argsIndent) {
1318
+ const argVal = trimmed.match(/^\s*-\s+(.+)/);
1319
+ if (argVal) {
1320
+ if (!currentServer.args) currentServer.args = [];
1321
+ currentServer.args.push(argVal[1].replace(/^["']|["']$/g, ''));
1322
+ continue;
1323
+ }
1324
+ }
1325
+ if (collectingArgs && indent <= argsIndent) collectingArgs = false;
1326
+ if (!currentName) continue;
1327
+
1328
+ const kvMatch = trimmed.match(/^\s+(command|cmd|type|url):\s*(.+)/);
1329
+ if (kvMatch) {
1330
+ collectingArgs = false;
1331
+ const key = kvMatch[1] === 'cmd' ? 'command' : kvMatch[1];
1332
+ currentServer[key] = kvMatch[2].replace(/^["']|["']$/g, '');
1333
+ continue;
1334
+ }
1335
+
1336
+ const argsMatch = trimmed.match(/^\s+(args):\s*(.*)/);
1337
+ if (argsMatch) {
1338
+ const inlineArr = argsMatch[2].match(/^\[(.+)\]$/);
1339
+ if (inlineArr) {
1340
+ currentServer.args = inlineArr[1].split(',').map(s => s.trim().replace(/^["']|["']$/g, ''));
1341
+ collectingArgs = false;
1342
+ } else {
1343
+ collectingArgs = true;
1344
+ argsIndent = indent;
1345
+ currentServer.args = [];
1346
+ }
1347
+ continue;
1348
+ }
1349
+ }
1350
+ if (currentName) result.mcpServers[currentName] = currentServer;
1351
+ return result;
1352
+ }
1353
+
1354
+ /**
1355
+ * Minimal TOML parser — extracts [mcp_servers.xxx] sections
1356
+ * from OpenAI Codex CLI config. Zero dependencies.
1357
+ */
1358
+ function parseSimpleToml(text) {
1359
+ const result = { mcpServers: {} };
1360
+ const lines = text.split('\n');
1361
+ let currentName = null;
1362
+ let currentServer = {};
1363
+
1364
+ for (const line of lines) {
1365
+ const trimmed = line.trim();
1366
+ if (trimmed === '' || trimmed.startsWith('#')) continue;
1367
+
1368
+ const sectionMatch = trimmed.match(/^\[mcp_servers\.(.+)\]$/);
1369
+ if (sectionMatch) {
1370
+ if (currentName) result.mcpServers[currentName] = currentServer;
1371
+ currentName = sectionMatch[1];
1372
+ currentServer = {};
1373
+ continue;
1374
+ }
1375
+ if (trimmed.startsWith('[') && !trimmed.startsWith('[mcp_servers.')) {
1376
+ if (currentName) result.mcpServers[currentName] = currentServer;
1377
+ currentName = null;
1378
+ continue;
1379
+ }
1380
+ if (!currentName) continue;
1381
+
1382
+ const strMatch = trimmed.match(/^(command|url)\s*=\s*"(.+?)"/);
1383
+ if (strMatch) { currentServer[strMatch[1]] = strMatch[2]; continue; }
1384
+
1385
+ const argsMatch = trimmed.match(/^args\s*=\s*\[(.+)\]/);
1386
+ if (argsMatch) {
1387
+ currentServer.args = argsMatch[1].split(',').map(s => s.trim().replace(/^["']|["']$/g, ''));
1388
+ continue;
1389
+ }
1390
+
1391
+ const boolMatch = trimmed.match(/^enabled\s*=\s*(true|false)/);
1392
+ if (boolMatch && boolMatch[1] === 'false') currentServer.disabled = true;
1393
+ }
1394
+ if (currentName) result.mcpServers[currentName] = currentServer;
1395
+ return result;
1396
+ }
1397
+
1398
+ /**
1399
+ * Comprehensive MCP config discovery across all major AI editors & tools.
1400
+ *
1401
+ * Supports: Claude Desktop, Claude Code, Cursor, Windsurf, VS Code,
1402
+ * Cline, Roo Code, Amazon Q, Gemini CLI, Zed, Continue.dev, Goose,
1403
+ * OpenAI Codex CLI, Visual Studio — global + project-level configs.
1404
+ */
1239
1405
  function findMcpConfigs() {
1240
1406
  const home = process.env.HOME || process.env.USERPROFILE || '';
1241
1407
  const platform = process.platform;
1242
-
1243
- // All known MCP config locations
1408
+ const cwd = process.cwd();
1409
+ const xdgConfig = process.env.XDG_CONFIG_HOME || path.join(home, '.config');
1410
+
1411
+ // Platform-specific app data directory
1412
+ // macOS: ~/Library/Application Support, Windows: ~/AppData/Roaming, Linux: ~/.config
1413
+ const appData = platform === 'darwin'
1414
+ ? path.join(home, 'Library', 'Application Support')
1415
+ : platform === 'win32'
1416
+ ? path.join(home, 'AppData', 'Roaming')
1417
+ : xdgConfig;
1418
+
1419
+ // Each candidate: { name, path, format: 'json'|'yaml'|'toml', key: top-level key }
1244
1420
  const candidates = [
1245
- // Claude Desktop
1246
- { name: 'Claude Desktop', path: path.join(home, '.claude', 'mcp.json') },
1247
- { name: 'Claude Desktop', path: path.join(home, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json') },
1248
- { name: 'Claude Desktop', path: path.join(home, 'AppData', 'Roaming', 'Claude', 'claude_desktop_config.json') },
1249
- { name: 'Claude Desktop', path: path.join(home, '.config', 'claude', 'claude_desktop_config.json') },
1250
- // Cursor
1251
- { name: 'Cursor', path: path.join(home, '.cursor', 'mcp.json') },
1252
- // Windsurf / Codeium
1253
- { name: 'Windsurf', path: path.join(home, '.codeium', 'windsurf', 'mcp_config.json') },
1254
- // VS Code
1255
- { name: 'VS Code', path: path.join(home, '.vscode', 'mcp.json') },
1256
- // Continue.dev
1257
- { name: 'Continue', path: path.join(home, '.continue', 'config.json') },
1421
+ // ── Claude Desktop ──
1422
+ ...(platform === 'darwin' ? [{ name: 'Claude Desktop', path: path.join(home, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json'), format: 'json', key: 'mcpServers' }] : []),
1423
+ ...(platform === 'win32' ? [{ name: 'Claude Desktop', path: path.join(home, 'AppData', 'Roaming', 'Claude', 'claude_desktop_config.json'), format: 'json', key: 'mcpServers' }] : []),
1424
+ ...(platform === 'linux' ? [{ name: 'Claude Desktop', path: path.join(xdgConfig, 'Claude', 'claude_desktop_config.json'), format: 'json', key: 'mcpServers' }] : []),
1425
+
1426
+ // ── Claude Code ──
1427
+ { name: 'Claude Code', path: path.join(home, '.claude.json'), format: 'json', key: 'mcpServers' },
1428
+ { name: 'Claude Code', path: path.join(home, '.claude', 'mcp.json'), format: 'json', key: 'mcpServers' },
1429
+
1430
+ // ── Cursor (global) ──
1431
+ { name: 'Cursor', path: path.join(home, '.cursor', 'mcp.json'), format: 'json', key: 'mcpServers' },
1432
+
1433
+ // ── Windsurf / Codeium ──
1434
+ { name: 'Windsurf', path: path.join(home, '.codeium', 'windsurf', 'mcp_config.json'), format: 'json', key: 'mcpServers' },
1435
+
1436
+ // ── VS Code (global mcp.json — uses 'servers' key) ──
1437
+ ...(platform === 'darwin' ? [{ name: 'VS Code', path: path.join(home, 'Library', 'Application Support', 'Code', 'User', 'mcp.json'), format: 'json', key: 'servers' }] : []),
1438
+ ...(platform === 'win32' ? [{ name: 'VS Code', path: path.join(home, 'AppData', 'Roaming', 'Code', 'User', 'mcp.json'), format: 'json', key: 'servers' }] : []),
1439
+ ...(platform === 'linux' ? [{ name: 'VS Code', path: path.join(xdgConfig, 'Code', 'User', 'mcp.json'), format: 'json', key: 'servers' }] : []),
1440
+
1441
+ // ── VS Code settings.json (mcp.servers nested key) ──
1442
+ ...(platform === 'darwin' ? [{ name: 'VS Code (settings)', path: path.join(home, 'Library', 'Application Support', 'Code', 'User', 'settings.json'), format: 'json', key: 'mcp.servers' }] : []),
1443
+ ...(platform === 'win32' ? [{ name: 'VS Code (settings)', path: path.join(home, 'AppData', 'Roaming', 'Code', 'User', 'settings.json'), format: 'json', key: 'mcp.servers' }] : []),
1444
+ ...(platform === 'linux' ? [{ name: 'VS Code (settings)', path: path.join(xdgConfig, 'Code', 'User', 'settings.json'), format: 'json', key: 'mcp.servers' }] : []),
1445
+
1446
+ // ── Cline (VS Code extension) ──
1447
+ ...(platform === 'darwin' ? [{ name: 'Cline', path: path.join(home, 'Library', 'Application Support', 'Code', 'User', 'globalStorage', 'saoudrizwan.claude-dev', 'settings', 'cline_mcp_settings.json'), format: 'json', key: 'mcpServers' }] : []),
1448
+ ...(platform === 'win32' ? [{ name: 'Cline', path: path.join(home, 'AppData', 'Roaming', 'Code', 'User', 'globalStorage', 'saoudrizwan.claude-dev', 'settings', 'cline_mcp_settings.json'), format: 'json', key: 'mcpServers' }] : []),
1449
+ ...(platform === 'linux' ? [{ name: 'Cline', path: path.join(xdgConfig, 'Code', 'User', 'globalStorage', 'saoudrizwan.claude-dev', 'settings', 'cline_mcp_settings.json'), format: 'json', key: 'mcpServers' }] : []),
1450
+
1451
+ // ── Roo Code (VS Code extension) ──
1452
+ ...(platform === 'darwin' ? [{ name: 'Roo Code', path: path.join(home, 'Library', 'Application Support', 'Code', 'User', 'globalStorage', 'rooveterinaryinc.roo-cline', 'settings', 'mcp_settings.json'), format: 'json', key: 'mcpServers' }] : []),
1453
+ ...(platform === 'win32' ? [{ name: 'Roo Code', path: path.join(home, 'AppData', 'Roaming', 'Code', 'User', 'globalStorage', 'rooveterinaryinc.roo-cline', 'settings', 'mcp_settings.json'), format: 'json', key: 'mcpServers' }] : []),
1454
+ ...(platform === 'linux' ? [{ name: 'Roo Code', path: path.join(xdgConfig, 'Code', 'User', 'globalStorage', 'rooveterinaryinc.roo-cline', 'settings', 'mcp_settings.json'), format: 'json', key: 'mcpServers' }] : []),
1455
+
1456
+ // ── Amazon Q Developer ──
1457
+ { name: 'Amazon Q', path: path.join(home, '.aws', 'amazonq', 'mcp.json'), format: 'json', key: 'mcpServers' },
1458
+ { name: 'Amazon Q (IDE)', path: path.join(home, '.aws', 'amazonq', 'default.json'), format: 'json', key: 'mcpServers' },
1459
+
1460
+ // ── Gemini CLI ──
1461
+ { name: 'Gemini CLI', path: path.join(home, '.gemini', 'settings.json'), format: 'json', key: 'mcpServers' },
1462
+
1463
+ // ── Zed (macOS + Linux only, uses 'context_servers' key) ──
1464
+ ...(platform === 'darwin' ? [{ name: 'Zed', path: path.join(home, '.zed', 'settings.json'), format: 'json', key: 'context_servers' }] : []),
1465
+ ...(platform === 'linux' ? [{ name: 'Zed', path: path.join(xdgConfig, 'zed', 'settings.json'), format: 'json', key: 'context_servers' }] : []),
1466
+
1467
+ // ── Continue.dev ──
1468
+ { name: 'Continue', path: path.join(home, '.continue', 'config.json'), format: 'json', key: 'mcpServers' },
1469
+ { name: 'Continue', path: path.join(home, '.continue', 'config.yaml'), format: 'yaml', key: 'mcpServers' },
1470
+
1471
+ // ── Goose (Block/Square) ──
1472
+ { name: 'Goose', path: path.join(xdgConfig, 'goose', 'config.yaml'), format: 'yaml', key: 'extensions' },
1473
+
1474
+ // ── OpenAI Codex CLI ──
1475
+ { name: 'Codex CLI', path: path.join(home, '.codex', 'config.toml'), format: 'toml', key: 'mcp_servers' },
1476
+
1477
+ // ── Visual Studio (Windows only) ──
1478
+ ...(platform === 'win32' ? [{ name: 'Visual Studio', path: path.join(home, '.mcp.json'), format: 'json', key: 'mcpServers' }] : []),
1479
+
1480
+ // ── Project-level configs (cwd) ──
1481
+ { name: 'Claude Code (project)', path: path.join(cwd, '.mcp.json'), format: 'json', key: 'mcpServers' },
1482
+ { name: 'Cursor (project)', path: path.join(cwd, '.cursor', 'mcp.json'), format: 'json', key: 'mcpServers' },
1483
+ { name: 'VS Code (project)', path: path.join(cwd, '.vscode', 'mcp.json'), format: 'json', key: 'servers' },
1484
+ { name: 'Roo Code (project)', path: path.join(cwd, '.roo', 'mcp.json'), format: 'json', key: 'mcpServers' },
1485
+ { name: 'Amazon Q (project)', path: path.join(cwd, '.amazonq', 'mcp.json'), format: 'json', key: 'mcpServers' },
1486
+ { name: 'Gemini CLI (project)', path: path.join(cwd, '.gemini', 'settings.json'), format: 'json', key: 'mcpServers' },
1487
+ ...(platform !== 'win32' ? [{ name: 'Zed (project)', path: path.join(cwd, '.zed', 'settings.json'), format: 'json', key: 'context_servers' }] : []),
1488
+ { name: 'Codex CLI (project)', path: path.join(cwd, '.codex', 'config.toml'), format: 'toml', key: 'mcp_servers' },
1258
1489
  ];
1259
-
1260
- // Also check AGENTAUDIT_TEST_CONFIG env for testing
1490
+
1491
+ // Test config override
1261
1492
  if (process.env.AGENTAUDIT_TEST_CONFIG) {
1262
- candidates.push({ name: 'Test Config', path: process.env.AGENTAUDIT_TEST_CONFIG });
1493
+ candidates.push({ name: 'Test Config', path: process.env.AGENTAUDIT_TEST_CONFIG, format: 'json', key: 'mcpServers' });
1263
1494
  }
1264
-
1265
- // Also scan workspace .cursor/mcp.json, .vscode/mcp.json in cwd
1266
- const cwd = process.cwd();
1267
- candidates.push(
1268
- { name: 'Cursor (project)', path: path.join(cwd, '.cursor', 'mcp.json') },
1269
- { name: 'VS Code (project)', path: path.join(cwd, '.vscode', 'mcp.json') },
1270
- );
1271
-
1495
+
1496
+ // Continue.dev mcpServers drop-in directory (individual JSON files)
1497
+ const continueDropIn = path.join(home, '.continue', 'mcpServers');
1498
+ try {
1499
+ if (fs.existsSync(continueDropIn)) {
1500
+ for (const f of fs.readdirSync(continueDropIn)) {
1501
+ if (f.endsWith('.json')) {
1502
+ candidates.push({ name: 'Continue (drop-in)', path: path.join(continueDropIn, f), format: 'json', key: 'mcpServers' });
1503
+ }
1504
+ }
1505
+ }
1506
+ } catch {}
1507
+
1508
+ // Project-level Continue.dev drop-ins
1509
+ const cwdContinueDropIn = path.join(cwd, '.continue', 'mcpServers');
1510
+ try {
1511
+ if (fs.existsSync(cwdContinueDropIn)) {
1512
+ for (const f of fs.readdirSync(cwdContinueDropIn)) {
1513
+ if (f.endsWith('.json')) {
1514
+ candidates.push({ name: 'Continue (project drop-in)', path: path.join(cwdContinueDropIn, f), format: 'json', key: 'mcpServers' });
1515
+ }
1516
+ }
1517
+ }
1518
+ } catch {}
1519
+
1272
1520
  const found = [];
1521
+ const seenPaths = new Set();
1522
+
1273
1523
  for (const c of candidates) {
1274
- if (fs.existsSync(c.path)) {
1275
- try {
1276
- const content = JSON.parse(fs.readFileSync(c.path, 'utf8'));
1277
- found.push({ ...c, content });
1278
- } catch {}
1279
- }
1524
+ const resolved = path.resolve(c.path);
1525
+ if (seenPaths.has(resolved)) continue;
1526
+ if (!fs.existsSync(c.path)) continue;
1527
+ seenPaths.add(resolved);
1528
+
1529
+ try {
1530
+ const raw = fs.readFileSync(c.path, 'utf8');
1531
+ let content;
1532
+
1533
+ if (c.format === 'yaml') {
1534
+ content = parseSimpleYaml(raw, c.key);
1535
+ } else if (c.format === 'toml') {
1536
+ content = parseSimpleToml(raw);
1537
+ } else {
1538
+ content = JSON.parse(raw);
1539
+ // Normalize different JSON key structures to mcpServers
1540
+ if (c.key === 'mcp.servers' && content.mcp?.servers) {
1541
+ content = { mcpServers: content.mcp.servers };
1542
+ } else if (c.key === 'context_servers' && content.context_servers) {
1543
+ // Zed: normalize nested { command: { path, args } } → { command, args }
1544
+ const normalized = {};
1545
+ for (const [name, cfg] of Object.entries(content.context_servers)) {
1546
+ if (cfg.command && typeof cfg.command === 'object') {
1547
+ normalized[name] = { command: cfg.command.path || cfg.command.command, args: cfg.command.args || [], env: cfg.command.env || {} };
1548
+ } else {
1549
+ normalized[name] = cfg;
1550
+ }
1551
+ }
1552
+ content = { mcpServers: normalized };
1553
+ } else if (c.key === 'servers' && content.servers && !content.mcpServers) {
1554
+ content = { mcpServers: content.servers };
1555
+ }
1556
+ }
1557
+
1558
+ // Only include configs that actually have servers
1559
+ const servers = content?.mcpServers || content?.servers || {};
1560
+ if (Object.keys(servers).length > 0) {
1561
+ found.push({ name: c.name, path: c.path, content });
1562
+ }
1563
+ } catch {}
1280
1564
  }
1565
+
1281
1566
  return found;
1282
1567
  }
1283
1568
 
@@ -1456,7 +1741,7 @@ async function discoverCommand(options = {}) {
1456
1741
  const interactiveAudit = options.audit || false;
1457
1742
 
1458
1743
  if (!jsonMode) {
1459
- console.log(` ${c.bold}Discovering MCP servers in your AI editors...${c.reset}`);
1744
+ console.log(` ${c.bold}Discovering MCP servers across all AI tools...${c.reset}`);
1460
1745
  console.log();
1461
1746
  }
1462
1747
 
@@ -1464,13 +1749,16 @@ async function discoverCommand(options = {}) {
1464
1749
 
1465
1750
  if (configs.length === 0) {
1466
1751
  console.log(` ${c.yellow}No MCP configurations found.${c.reset}`);
1467
- console.log(` ${c.dim}Searched: Claude Desktop, Cursor, Windsurf, VS Code${c.reset}`);
1752
+ console.log(` ${c.dim}Searched 15+ tools: Claude Desktop, Claude Code, Cursor, Windsurf, VS Code,${c.reset}`);
1753
+ console.log(` ${c.dim}Cline, Roo Code, Amazon Q, Gemini CLI, Zed, Continue.dev, Goose, Codex CLI${c.reset}`);
1468
1754
  console.log();
1469
- console.log(` ${c.dim}MCP config locations:${c.reset}`);
1470
- console.log(` ${c.dim} Claude: ~/.claude/mcp.json${c.reset}`);
1471
- console.log(` ${c.dim} Cursor: ~/.cursor/mcp.json${c.reset}`);
1472
- console.log(` ${c.dim} Windsurf: ~/.codeium/windsurf/mcp_config.json${c.reset}`);
1473
- console.log(` ${c.dim} VS Code: ~/.vscode/mcp.json${c.reset}`);
1755
+ console.log(` ${c.dim}Common MCP config locations:${c.reset}`);
1756
+ console.log(` ${c.dim} Claude Desktop: ~/Library/Application Support/Claude/claude_desktop_config.json${c.reset}`);
1757
+ console.log(` ${c.dim} Claude Code: ~/.claude.json${c.reset}`);
1758
+ console.log(` ${c.dim} Cursor: ~/.cursor/mcp.json${c.reset}`);
1759
+ console.log(` ${c.dim} Windsurf: ~/.codeium/windsurf/mcp_config.json${c.reset}`);
1760
+ console.log(` ${c.dim} VS Code: (platform)/Code/User/mcp.json${c.reset}`);
1761
+ console.log(` ${c.dim} Project-level: .mcp.json / .cursor/mcp.json / .vscode/mcp.json${c.reset}`);
1474
1762
  console.log();
1475
1763
  return;
1476
1764
  }
@@ -1533,7 +1821,7 @@ async function discoverCommand(options = {}) {
1533
1821
  const riskScore = regData.risk_score ?? regData.latest_risk_score ?? 0;
1534
1822
  const hasOfficial = regData.has_official_audit;
1535
1823
  console.log(`${branch} ${c.bold}${server.name}${c.reset} ${sourceLabel}`);
1536
- console.log(`${pipe} ${riskBadge(riskScore)} Risk ${riskScore} ${hasOfficial ? `${c.green}✔ official${c.reset} ` : ''}${c.dim}${REGISTRY_URL}/skills/${slug}${c.reset}`);
1824
+ console.log(`${pipe} ${riskBadge(riskScore)} ${hasOfficial ? `${c.green}✔ official${c.reset} ` : ''}${c.dim}${REGISTRY_URL}/packages/${slug}${c.reset}`);
1537
1825
  if (resolvedUrl) allServersWithUrls.push({ name: server.name, sourceUrl: resolvedUrl, hasAudit: true, regData });
1538
1826
  } else {
1539
1827
  unauditedServers++;
@@ -1556,11 +1844,14 @@ async function discoverCommand(options = {}) {
1556
1844
  }
1557
1845
 
1558
1846
  // Summary
1559
- console.log(`${c.dim}${''.repeat(60)}${c.reset}`);
1560
- console.log(` ${c.bold}Summary${c.reset} ${totalServers} server${totalServers !== 1 ? 's' : ''} across ${configs.length} config${configs.length !== 1 ? 's' : ''}`);
1847
+ console.log(sectionHeader(`Summary — ${totalServers} server${totalServers !== 1 ? 's' : ''} across ${configs.length} config${configs.length !== 1 ? 's' : ''}`));
1561
1848
  console.log();
1562
1849
  if (auditedServers > 0) console.log(` ${icons.safe} ${c.green}${auditedServers} audited${c.reset}`);
1563
1850
  if (unauditedServers > 0) console.log(` ${icons.caution} ${c.yellow}${unauditedServers} not audited${c.reset}`);
1851
+ if (totalServers > 0) {
1852
+ console.log();
1853
+ console.log(` ${coverageBar(auditedServers, totalServers)}`);
1854
+ }
1564
1855
  console.log();
1565
1856
 
1566
1857
  // --scan: automatically scan all servers with resolved source URLs (git-cloneable only)
@@ -1576,8 +1867,8 @@ async function discoverCommand(options = {}) {
1576
1867
  });
1577
1868
  const skipped = allServersWithUrls.filter(s => s.sourceUrl && !isCloneable(s.sourceUrl));
1578
1869
  if (dedupedTargets.length > 0) {
1579
- console.log(`${c.dim}${''.repeat(60)}${c.reset}`);
1580
- console.log(` ${c.bold}${icons.scan} Auto-scanning ${dedupedTargets.length} server${dedupedTargets.length !== 1 ? 's' : ''}...${c.reset}`);
1870
+ console.log(sectionHeader(`Auto-scanning ${dedupedTargets.length} server${dedupedTargets.length !== 1 ? 's' : ''}`));
1871
+ console.log(` ${c.bold}${icons.scan} Starting scans...${c.reset}`);
1581
1872
  if (skipped.length > 0) {
1582
1873
  console.log(` ${c.dim}(${skipped.length} skipped — no cloneable source URL)${c.reset}`);
1583
1874
  }
@@ -1591,8 +1882,7 @@ async function discoverCommand(options = {}) {
1591
1882
 
1592
1883
  if (scanResults.length > 1) {
1593
1884
  // Print combined scan summary
1594
- console.log(`${c.dim}${''.repeat(60)}${c.reset}`);
1595
- console.log(` ${c.bold}Scan Summary${c.reset} ${scanResults.length} server${scanResults.length !== 1 ? 's' : ''} scanned`);
1885
+ console.log(sectionHeader(`Scan Summary — ${scanResults.length} server${scanResults.length !== 1 ? 's' : ''} scanned`));
1596
1886
  console.log();
1597
1887
 
1598
1888
  let totalFindings = 0;
@@ -1701,7 +1991,7 @@ async function auditRepo(url) {
1701
1991
  console.log();
1702
1992
 
1703
1993
  // Step 1: Clone
1704
- process.stdout.write(` ${c.dim}[1/4]${c.reset} Cloning repository...`);
1994
+ process.stdout.write(` ${stepProgress(1, 4)} Cloning repository...`);
1705
1995
  const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'agentaudit-'));
1706
1996
  const repoPath = path.join(tmpDir, 'repo');
1707
1997
  try {
@@ -1716,12 +2006,12 @@ async function auditRepo(url) {
1716
2006
  }
1717
2007
 
1718
2008
  // Step 2: Collect files
1719
- process.stdout.write(` ${c.dim}[2/4]${c.reset} Collecting source files...`);
2009
+ process.stdout.write(` ${stepProgress(2, 4)} Collecting source files...`);
1720
2010
  const files = collectFiles(repoPath);
1721
2011
  console.log(` ${c.green}${files.length} files${c.reset}`);
1722
2012
 
1723
2013
  // Step 3: Build audit payload
1724
- process.stdout.write(` ${c.dim}[3/4]${c.reset} Preparing audit payload...`);
2014
+ process.stdout.write(` ${stepProgress(3, 4)} Preparing audit payload...`);
1725
2015
  const auditPrompt = loadAuditPrompt();
1726
2016
 
1727
2017
  let codeBlock = '';
@@ -1794,7 +2084,7 @@ async function auditRepo(url) {
1794
2084
 
1795
2085
  // We have an API key — run LLM audit
1796
2086
  const modelLabel = modelOverride ? `${activeProvider} → ${activeLlm.model}` : activeProvider;
1797
- process.stdout.write(` ${c.dim}[4/4]${c.reset} Running LLM analysis ${c.dim}(${modelLabel})${c.reset}...`);
2087
+ process.stdout.write(` ${stepProgress(4, 4)} Running LLM analysis ${c.dim}(${modelLabel})${c.reset}...`);
1798
2088
 
1799
2089
  const systemPrompt = auditPrompt || 'You are a security auditor. Analyze the code and report findings as JSON.';
1800
2090
  const userMessage = [
@@ -2008,17 +2298,26 @@ async function auditRepo(url) {
2008
2298
  // Display results
2009
2299
  console.log();
2010
2300
  const riskScore = report.risk_score || 0;
2011
- console.log(` ${riskBadge(riskScore)} Risk ${riskScore}/100 ${c.bold}${report.result || 'unknown'}${c.reset}`);
2301
+ console.log(sectionHeader('Result'));
2302
+ console.log(` ${riskBadge(riskScore)}`);
2012
2303
  console.log();
2013
-
2304
+
2014
2305
  if (report.findings && report.findings.length > 0) {
2015
- console.log(` ${c.bold}Findings (${report.findings.length})${c.reset}`);
2306
+ console.log(sectionHeader(`Findings (${report.findings.length})`));
2016
2307
  console.log();
2017
2308
  for (const f of report.findings) {
2018
2309
  const sc = severityColor(f.severity);
2019
- console.log(` ${severityIcon(f.severity)} ${sc}${(f.severity || '').toUpperCase().padEnd(8)}${c.reset} ${f.title}`);
2020
- if (f.file) console.log(` ${c.dim}${f.file}${f.line ? ':' + f.line : ''}${c.reset}`);
2021
- if (f.description) console.log(` ${c.dim}${f.description.slice(0, 120)}${c.reset}`);
2310
+ console.log(` ${sc}┃${c.reset} ${sc}${(f.severity || '').toUpperCase().padEnd(8)}${c.reset} ${c.bold}${f.title}${c.reset}`);
2311
+ if (f.file) console.log(` ${sc}┃${c.reset} ${c.dim}${f.file}${f.line ? ':' + f.line : ''}${c.reset}`);
2312
+ if (f.description) console.log(` ${sc}┃${c.reset} ${c.dim}${f.description.slice(0, 120)}${c.reset}`);
2313
+ console.log();
2314
+ }
2315
+
2316
+ // Severity histogram
2317
+ const histLines = severityHistogram(report.findings);
2318
+ if (histLines.length > 1) {
2319
+ console.log(sectionHeader('Severity'));
2320
+ for (const line of histLines) console.log(line);
2022
2321
  console.log();
2023
2322
  }
2024
2323
  } else {
@@ -2046,7 +2345,7 @@ async function auditRepo(url) {
2046
2345
  if (res.ok) {
2047
2346
  const data = await res.json();
2048
2347
  console.log(` ${c.green}done${c.reset}`);
2049
- console.log(` ${c.dim}Report: ${REGISTRY_URL}/skills/${slug}${c.reset}`);
2348
+ console.log(` ${c.dim}Report: ${REGISTRY_URL}/packages/${slug}${c.reset}`);
2050
2349
  } else {
2051
2350
  let errBody = '';
2052
2351
  try { errBody = await res.text(); } catch {}
@@ -2059,27 +2358,22 @@ async function auditRepo(url) {
2059
2358
  console.log(` ${c.yellow}failed${c.reset}`);
2060
2359
  }
2061
2360
  } else if (process.stdin.isTTY) {
2062
- // No credentials — offer inline registration
2063
- console.log();
2064
- console.log(` ${c.bold}Share this audit with the community?${c.reset}`);
2065
- console.log(` ${c.dim}Uploading helps others assess package security. Account is free.${c.reset}`);
2361
+ // No credentials — prompt to paste key or set up
2066
2362
  console.log();
2067
- console.log(` ${c.bold}1)${c.reset} Create account + upload ${c.dim}(free)${c.reset}`);
2068
- console.log(` ${c.bold}2)${c.reset} Skip`);
2363
+ console.log(` ${c.bold}Want to upload this report to agentaudit.dev?${c.reset}`);
2364
+ console.log(` ${c.dim}Create an API key at ${c.cyan}${REGISTRY_URL}/profile${c.dim} (sign in with GitHub)${c.reset}`);
2069
2365
  console.log();
2070
- const uploadChoice = await askQuestion(` Choice ${c.dim}(1/2)${c.reset}: `);
2071
- if (uploadChoice === '1') {
2072
- const name = await askQuestion(` Agent name ${c.dim}(e.g. my-scanner, claude-desktop)${c.reset}: `);
2073
- if (!name || !/^[a-zA-Z0-9._-]{2,64}$/.test(name)) {
2074
- console.log(` ${c.red}Invalid name. Use 2-64 chars: letters, numbers, dash, underscore, dot.${c.reset}`);
2075
- } else {
2366
+ const pastedKey = await askQuestion(` Paste API key ${c.dim}(or Enter to skip)${c.reset}: `);
2367
+ if (pastedKey && pastedKey.trim()) {
2368
+ process.stdout.write(` Validating...`);
2369
+ const validation = await validateApiKey(pastedKey.trim());
2370
+ if (validation.valid) {
2371
+ const agentName = validation.agent_name || 'agent';
2372
+ saveCredentials({ api_key: pastedKey.trim(), agent_name: agentName });
2373
+ creds = { api_key: pastedKey.trim(), agent_name: agentName };
2374
+ console.log(` ${c.green}valid!${c.reset}`);
2375
+ process.stdout.write(` Uploading report...`);
2076
2376
  try {
2077
- process.stdout.write(` Registering ${c.bold}${name}${c.reset}...`);
2078
- const regData = await registerAgent(name);
2079
- saveCredentials({ api_key: regData.api_key, agent_name: regData.agent_name });
2080
- console.log(` ${c.green}done!${c.reset}`);
2081
- creds = { api_key: regData.api_key, agent_name: regData.agent_name };
2082
- process.stdout.write(` Uploading report...`);
2083
2377
  const res = await fetch(`${REGISTRY_URL}/api/reports`, {
2084
2378
  method: 'POST',
2085
2379
  headers: {
@@ -2091,7 +2385,7 @@ async function auditRepo(url) {
2091
2385
  });
2092
2386
  if (res.ok) {
2093
2387
  console.log(` ${c.green}done${c.reset}`);
2094
- console.log(` ${c.dim}Report: ${REGISTRY_URL}/skills/${slug}${c.reset}`);
2388
+ console.log(` ${c.dim}Report: ${REGISTRY_URL}/packages/${slug}${c.reset}`);
2095
2389
  } else {
2096
2390
  console.log(` ${c.yellow}failed (HTTP ${res.status})${c.reset}`);
2097
2391
  }
@@ -2099,10 +2393,13 @@ async function auditRepo(url) {
2099
2393
  console.log(` ${c.red}failed${c.reset}`);
2100
2394
  console.log(` ${c.dim}${err.message}${c.reset}`);
2101
2395
  }
2396
+ } else {
2397
+ console.log(` ${c.red}invalid key${c.reset}`);
2398
+ console.log(` ${c.dim}Run ${c.cyan}agentaudit setup${c.dim} to configure.${c.reset}`);
2102
2399
  }
2103
2400
  }
2104
2401
  } else {
2105
- console.log(` ${c.dim}Run ${c.cyan}agentaudit setup${c.dim} to create an account and upload reports${c.reset}`);
2402
+ console.log(` ${c.dim}Run ${c.cyan}agentaudit setup${c.dim} to configure your API key and upload reports${c.reset}`);
2106
2403
  }
2107
2404
 
2108
2405
  console.log();
@@ -2131,7 +2428,7 @@ async function checkPackage(name) {
2131
2428
  console.log(` ${c.bold}${name}${c.reset} ${riskBadge(riskScore)}`);
2132
2429
  console.log(` ${c.dim}Risk Score: ${riskScore}/100${c.reset}`);
2133
2430
  if (data.source_url) console.log(` ${c.dim}Source: ${data.source_url}${c.reset}`);
2134
- console.log(` ${c.dim}Registry: ${REGISTRY_URL}/skills/${name}${c.reset}`);
2431
+ console.log(` ${c.dim}Registry: ${REGISTRY_URL}/packages/${name}${c.reset}`);
2135
2432
  if (data.has_official_audit) console.log(` ${c.green}✔ Officially audited${c.reset}`);
2136
2433
  console.log();
2137
2434
  }
@@ -2255,7 +2552,7 @@ function renderLeaderboardTab(data, width, opts = {}) {
2255
2552
  }
2256
2553
 
2257
2554
  const maxPts = leaderboard[0]?.total_points || 1;
2258
- const medals = ['🥇', '🥈', '🥉'];
2555
+ const medals = [`${c.yellow}★${c.reset}`, `${c.cyan}★${c.reset}`, `${c.magenta}★${c.reset}`];
2259
2556
 
2260
2557
  for (let i = 0; i < leaderboard.length; i++) {
2261
2558
  const entry = leaderboard[i];
@@ -2659,7 +2956,7 @@ async function leaderboardCommand(args) {
2659
2956
  }
2660
2957
 
2661
2958
  const maxPts = data[0]?.total_points || 1;
2662
- const medals = ['🥇', '🥈', '🥉'];
2959
+ const medals = [`${c.yellow}★${c.reset}`, `${c.cyan}★${c.reset}`, `${c.magenta}★${c.reset}`];
2663
2960
  const barW = 24;
2664
2961
 
2665
2962
  for (let i = 0; i < data.length; i++) {
@@ -3005,7 +3302,9 @@ async function main() {
3005
3302
  discover: [
3006
3303
  `${c.bold}agentaudit discover${c.reset} [options]`,
3007
3304
  ``,
3008
- `Find MCP servers configured in your AI editors (Cursor, Claude Desktop, VS Code, Windsurf).`,
3305
+ `Find MCP servers across 15+ AI tools (Claude Desktop, Claude Code, Cursor, VS Code,`,
3306
+ `Windsurf, Cline, Roo Code, Amazon Q, Gemini CLI, Zed, Continue.dev, Goose, Codex CLI).`,
3307
+ `Checks global + project-level configs on macOS, Windows, and Linux.`,
3009
3308
  ``,
3010
3309
  `${c.bold}Options:${c.reset}`,
3011
3310
  ` --quick, -s Auto-scan all discovered servers (regex-based)`,
@@ -3193,6 +3492,21 @@ async function main() {
3193
3492
  ` agentaudit find fastmcp`,
3194
3493
  ],
3195
3494
  find: null, // alias → search
3495
+ profile: [
3496
+ `${c.bold}agentaudit profile${c.reset}`,
3497
+ ``,
3498
+ `Show your AgentAudit profile — rank, points, audit stats,`,
3499
+ `and a link to your public profile on agentaudit.dev.`,
3500
+ ``,
3501
+ `Requires being logged in (run ${c.cyan}agentaudit setup${c.reset} first).`,
3502
+ ``,
3503
+ `${c.bold}Options:${c.reset}`,
3504
+ ` --json Machine-readable JSON output`,
3505
+ ``,
3506
+ `${c.bold}Examples:${c.reset}`,
3507
+ ` agentaudit profile`,
3508
+ ` agentaudit profile --json`,
3509
+ ],
3196
3510
  };
3197
3511
 
3198
3512
  // Show subcommand help: `agentaudit help <cmd>` or `agentaudit <cmd> --help`
@@ -3255,6 +3569,7 @@ async function main() {
3255
3569
  console.log(` ${c.cyan}model${c.reset} Configure LLM provider + model`);
3256
3570
  console.log(` ${c.cyan}setup${c.reset} Log in to agentaudit.dev (for report uploads)`);
3257
3571
  console.log(` ${c.cyan}status${c.reset} Show current config + auth status`);
3572
+ console.log(` ${c.cyan}profile${c.reset} Your profile — rank, points, audit stats`);
3258
3573
  console.log();
3259
3574
  console.log(` ${c.bold}FLAGS${c.reset}`);
3260
3575
  console.log(` ${c.dim}--json Machine-readable JSON output${c.reset}`);
@@ -3413,6 +3728,103 @@ async function main() {
3413
3728
  return;
3414
3729
  }
3415
3730
 
3731
+ if (command === 'profile') {
3732
+ const creds = loadCredentials();
3733
+ if (!creds) {
3734
+ console.log(` ${c.yellow}Not logged in.${c.reset}`);
3735
+ console.log(` ${c.dim}Run ${c.cyan}agentaudit setup${c.dim} to link your API key.${c.reset}`);
3736
+ console.log();
3737
+ return;
3738
+ }
3739
+
3740
+ const agentName = creds.agent_name || 'unknown';
3741
+ console.log(` ${c.bold}Profile${c.reset} ${c.cyan}${agentName}${c.reset}`);
3742
+ console.log();
3743
+
3744
+ try {
3745
+ process.stdout.write(` ${c.dim}Fetching profile data...${c.reset}`);
3746
+ const [agentRes, lbRes] = await Promise.all([
3747
+ fetch(`${REGISTRY_URL}/api/agents/${encodeURIComponent(agentName)}`, {
3748
+ headers: { 'Authorization': `Bearer ${creds.api_key}` },
3749
+ signal: AbortSignal.timeout(10_000),
3750
+ }).then(r => r.ok ? r.json() : null),
3751
+ fetch(`${REGISTRY_URL}/api/leaderboard?limit=100`, {
3752
+ signal: AbortSignal.timeout(10_000),
3753
+ }).then(r => r.ok ? r.json() : null),
3754
+ ]);
3755
+ process.stdout.write('\r\x1b[K');
3756
+
3757
+ if (!agentRes) {
3758
+ console.log(` ${c.yellow}Could not fetch profile data.${c.reset}`);
3759
+ console.log(` ${c.dim}Your account may not have submitted any audits yet.${c.reset}`);
3760
+ console.log();
3761
+ console.log(` ${c.dim}Web profile: ${c.cyan}${REGISTRY_URL}/profile${c.reset}`);
3762
+ console.log();
3763
+ return;
3764
+ }
3765
+
3766
+ let rank = null;
3767
+ if (Array.isArray(lbRes)) {
3768
+ const idx = lbRes.findIndex(e => e.agent_name === agentName);
3769
+ if (idx >= 0) rank = idx + 1;
3770
+ }
3771
+
3772
+ // Update cache
3773
+ saveProfileCache({
3774
+ agent_name: agentName,
3775
+ rank,
3776
+ total_points: agentRes.total_points || 0,
3777
+ total_reports: agentRes.total_reports || 0,
3778
+ });
3779
+
3780
+ if (jsonMode) {
3781
+ console.log(JSON.stringify({
3782
+ agent_name: agentName,
3783
+ rank: rank ? { position: rank, total: lbRes?.length || 0 } : null,
3784
+ total_points: agentRes.total_points || 0,
3785
+ total_reports: agentRes.total_reports || 0,
3786
+ total_findings_submitted: agentRes.total_findings_submitted || 0,
3787
+ total_findings_confirmed: agentRes.total_findings_confirmed || 0,
3788
+ profile_url: `${REGISTRY_URL}/profile`,
3789
+ }, null, 2));
3790
+ return;
3791
+ }
3792
+
3793
+ const boxW = 44;
3794
+ const rankStr = rank ? `#${rank} of ${lbRes.length}` : '—';
3795
+ const pts = agentRes.total_points || 0;
3796
+ const audits = agentRes.total_reports || 0;
3797
+ const findingsSubmitted = agentRes.total_findings_submitted || 0;
3798
+ const findingsConfirmed = agentRes.total_findings_confirmed || 0;
3799
+ const ptsBar = renderBar(pts, Math.max(pts, 1000), boxW - 16);
3800
+
3801
+ const contentLines = [
3802
+ '',
3803
+ `${c.bold}${c.cyan}${agentName}${c.reset}${' '.repeat(Math.max(1, boxW - 6 - agentName.length - rankStr.length))}${c.bold}${rankStr}${c.reset}`,
3804
+ '',
3805
+ `Points ${c.bold}${padLeft(fmtNum(pts), 8)}${c.reset}`,
3806
+ `Audits ${c.bold}${padLeft(fmtNum(audits), 8)}${c.reset}`,
3807
+ `Findings ${c.bold}${padLeft(fmtNum(findingsSubmitted), 8)}${c.reset} ${c.dim}(${fmtNum(findingsConfirmed)} confirmed)${c.reset}`,
3808
+ '',
3809
+ ptsBar,
3810
+ '',
3811
+ `${c.dim}${REGISTRY_URL}/profile${c.reset}`,
3812
+ `${c.dim}Key: ${creds.api_key.slice(0, 12)}...${c.reset}`,
3813
+ '',
3814
+ ];
3815
+ const boxLines = drawBox('Profile', contentLines, boxW + 4);
3816
+ for (const line of boxLines) console.log(line);
3817
+ console.log();
3818
+ } catch (err) {
3819
+ process.stdout.write('\r\x1b[K');
3820
+ console.log(` ${c.yellow}Failed to fetch profile: ${err.message}${c.reset}`);
3821
+ console.log();
3822
+ console.log(` ${c.dim}Web profile: ${c.cyan}${REGISTRY_URL}/profile${c.reset}`);
3823
+ console.log();
3824
+ }
3825
+ return;
3826
+ }
3827
+
3416
3828
  if (command === 'model') {
3417
3829
  const newModel = targets.filter(t => !t.startsWith('--'))[0];
3418
3830
  const config = loadLlmConfig();