agentaudit 3.10.10 → 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
@@ -554,7 +555,7 @@ function banner() {
554
555
  const ptsStr = `${fmtNum(cache.total_points)}pts`;
555
556
  const auditsStr = `${fmtNum(cache.total_reports)} audits`;
556
557
  const profile = [cache.agent_name, rankStr, ptsStr, auditsStr].filter(Boolean).join(' \u00b7 ');
557
- 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}`);
558
559
  } else {
559
560
  console.log(` ${c.bold}${c.cyan}AgentAudit${c.reset} ${c.dim}v${getVersion()}${c.reset}`);
560
561
  console.log(` ${c.dim}Security scanner for AI packages${c.reset}`);
@@ -575,10 +576,14 @@ function elapsed(startMs) {
575
576
  }
576
577
 
577
578
  function riskBadge(score) {
578
- if (score === 0) return `${c.bgGreen}${c.bold}${c.white} SAFE ${c.reset}`;
579
- if (score <= 10) return `${c.bgGreen}${c.white} LOW ${c.reset}`;
580
- if (score <= 30) return `${c.bgYellow}${c.bold} CAUTION ${c.reset}`;
581
- 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`;
582
587
  }
583
588
 
584
589
  function severityColor(sev) {
@@ -695,6 +700,47 @@ function sparkline(values) {
695
700
  }).join('');
696
701
  }
697
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
+
698
744
  function fmtNum(n) {
699
745
  if (n == null) return '0';
700
746
  return n.toLocaleString('en-US');
@@ -709,7 +755,7 @@ function dashboardBanner() {
709
755
  const ver = getVersion();
710
756
  return [
711
757
  ` ${BOX.tl}${c.dim}${BOX.h.repeat(35)}${c.reset}${BOX.tr}`,
712
- ` ${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}`,
713
759
  ` ${BOX.v} ${c.dim}Security Registry for AI Agents${c.reset} ${BOX.v}`,
714
760
  ` ${BOX.bl}${c.dim}${BOX.h.repeat(35)}${c.reset}${BOX.br}`,
715
761
  ];
@@ -1120,31 +1166,38 @@ function printScanResult(url, info, files, findings, registryData, duration) {
1120
1166
  console.log(`${icons.pipe} ${c.dim}(no tools or prompts detected)${c.reset}`);
1121
1167
  }
1122
1168
 
1123
- // Findings
1169
+ // Findings with severity stripe
1124
1170
  if (findings.length > 0) {
1125
- console.log(`${icons.pipe}`);
1126
- console.log(`${icons.pipe} ${c.bold}Findings (${findings.length})${c.reset} ${c.dim}static analysis — may include false positives${c.reset}`);
1127
- for (let i = 0; i < findings.length; i++) {
1128
- const f = findings[i];
1129
- const isLast = i === findings.length - 1;
1130
- const branch = isLast ? icons.treeLast : icons.tree;
1131
- 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) {
1132
1176
  const sc = severityColor(f.severity);
1133
- console.log(`${branch} ${severityIcon(f.severity)} ${sc}${f.severity.toUpperCase().padEnd(8)}${c.reset} ${f.title}`);
1134
- 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);
1135
1187
  }
1136
1188
  }
1137
-
1189
+
1138
1190
  // Registry status
1139
- console.log(`${icons.pipe}`);
1191
+ console.log();
1192
+ console.log(sectionHeader('Registry'));
1140
1193
  if (registryData) {
1141
1194
  const rd = registryData;
1142
1195
  const riskScore = rd.risk_score ?? rd.latest_risk_score ?? 0;
1143
- console.log(`${icons.treeLast} ${c.dim}registry${c.reset} ${riskBadge(riskScore)} Risk ${riskScore} ${c.dim}${REGISTRY_URL}/packages/${slug}${c.reset}`);
1196
+ console.log(` ${riskBadge(riskScore)} ${c.dim}${REGISTRY_URL}/packages/${slug}${c.reset}`);
1144
1197
  } else {
1145
- 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}`);
1146
1199
  }
1147
-
1200
+
1148
1201
  console.log();
1149
1202
  }
1150
1203
 
@@ -1153,27 +1206,23 @@ function printSummary(results) {
1153
1206
  const safe = results.filter(r => r.findings.length === 0).length;
1154
1207
  const withFindings = total - safe;
1155
1208
  const totalFindings = results.reduce((sum, r) => sum + r.findings.length, 0);
1156
-
1157
- console.log(`${c.dim}${'─'.repeat(60)}${c.reset}`);
1158
- 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`));
1159
1212
  console.log();
1160
1213
  if (safe > 0) console.log(` ${icons.safe} ${c.green}${safe} clean${c.reset}`);
1161
1214
  if (withFindings > 0) console.log(` ${icons.caution} ${c.yellow}${withFindings} with findings${c.reset} (${totalFindings} total)`);
1162
-
1163
- // Breakdown by severity
1164
- const bySev = {};
1165
- results.forEach(r => r.findings.forEach(f => {
1166
- bySev[f.severity] = (bySev[f.severity] || 0) + 1;
1167
- }));
1168
- 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) {
1169
1221
  console.log();
1170
- for (const sev of ['critical', 'high', 'medium', 'low']) {
1171
- if (bySev[sev]) {
1172
- console.log(` ${severityIcon(sev)} ${bySev[sev]}× ${severityColor(sev)}${sev}${c.reset}`);
1173
- }
1174
- }
1222
+ console.log(sectionHeader('Severity'));
1223
+ for (const line of histLines) console.log(line);
1175
1224
  }
1176
-
1225
+
1177
1226
  console.log();
1178
1227
  }
1179
1228
 
@@ -1230,48 +1279,290 @@ async function scanRepo(url) {
1230
1279
 
1231
1280
  // ── Discover local MCP configs ──────────────────────────
1232
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
+ */
1233
1405
  function findMcpConfigs() {
1234
1406
  const home = process.env.HOME || process.env.USERPROFILE || '';
1235
1407
  const platform = process.platform;
1236
-
1237
- // 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 }
1238
1420
  const candidates = [
1239
- // Claude Desktop
1240
- { name: 'Claude Desktop', path: path.join(home, '.claude', 'mcp.json') },
1241
- { name: 'Claude Desktop', path: path.join(home, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json') },
1242
- { name: 'Claude Desktop', path: path.join(home, 'AppData', 'Roaming', 'Claude', 'claude_desktop_config.json') },
1243
- { name: 'Claude Desktop', path: path.join(home, '.config', 'claude', 'claude_desktop_config.json') },
1244
- // Cursor
1245
- { name: 'Cursor', path: path.join(home, '.cursor', 'mcp.json') },
1246
- // Windsurf / Codeium
1247
- { name: 'Windsurf', path: path.join(home, '.codeium', 'windsurf', 'mcp_config.json') },
1248
- // VS Code
1249
- { name: 'VS Code', path: path.join(home, '.vscode', 'mcp.json') },
1250
- // Continue.dev
1251
- { 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' },
1252
1489
  ];
1253
-
1254
- // Also check AGENTAUDIT_TEST_CONFIG env for testing
1490
+
1491
+ // Test config override
1255
1492
  if (process.env.AGENTAUDIT_TEST_CONFIG) {
1256
- 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' });
1257
1494
  }
1258
-
1259
- // Also scan workspace .cursor/mcp.json, .vscode/mcp.json in cwd
1260
- const cwd = process.cwd();
1261
- candidates.push(
1262
- { name: 'Cursor (project)', path: path.join(cwd, '.cursor', 'mcp.json') },
1263
- { name: 'VS Code (project)', path: path.join(cwd, '.vscode', 'mcp.json') },
1264
- );
1265
-
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
+
1266
1520
  const found = [];
1521
+ const seenPaths = new Set();
1522
+
1267
1523
  for (const c of candidates) {
1268
- if (fs.existsSync(c.path)) {
1269
- try {
1270
- const content = JSON.parse(fs.readFileSync(c.path, 'utf8'));
1271
- found.push({ ...c, content });
1272
- } catch {}
1273
- }
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 {}
1274
1564
  }
1565
+
1275
1566
  return found;
1276
1567
  }
1277
1568
 
@@ -1450,7 +1741,7 @@ async function discoverCommand(options = {}) {
1450
1741
  const interactiveAudit = options.audit || false;
1451
1742
 
1452
1743
  if (!jsonMode) {
1453
- 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}`);
1454
1745
  console.log();
1455
1746
  }
1456
1747
 
@@ -1458,13 +1749,16 @@ async function discoverCommand(options = {}) {
1458
1749
 
1459
1750
  if (configs.length === 0) {
1460
1751
  console.log(` ${c.yellow}No MCP configurations found.${c.reset}`);
1461
- 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}`);
1462
1754
  console.log();
1463
- console.log(` ${c.dim}MCP config locations:${c.reset}`);
1464
- console.log(` ${c.dim} Claude: ~/.claude/mcp.json${c.reset}`);
1465
- console.log(` ${c.dim} Cursor: ~/.cursor/mcp.json${c.reset}`);
1466
- console.log(` ${c.dim} Windsurf: ~/.codeium/windsurf/mcp_config.json${c.reset}`);
1467
- 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}`);
1468
1762
  console.log();
1469
1763
  return;
1470
1764
  }
@@ -1527,7 +1821,7 @@ async function discoverCommand(options = {}) {
1527
1821
  const riskScore = regData.risk_score ?? regData.latest_risk_score ?? 0;
1528
1822
  const hasOfficial = regData.has_official_audit;
1529
1823
  console.log(`${branch} ${c.bold}${server.name}${c.reset} ${sourceLabel}`);
1530
- console.log(`${pipe} ${riskBadge(riskScore)} Risk ${riskScore} ${hasOfficial ? `${c.green}✔ official${c.reset} ` : ''}${c.dim}${REGISTRY_URL}/packages/${slug}${c.reset}`);
1824
+ console.log(`${pipe} ${riskBadge(riskScore)} ${hasOfficial ? `${c.green}✔ official${c.reset} ` : ''}${c.dim}${REGISTRY_URL}/packages/${slug}${c.reset}`);
1531
1825
  if (resolvedUrl) allServersWithUrls.push({ name: server.name, sourceUrl: resolvedUrl, hasAudit: true, regData });
1532
1826
  } else {
1533
1827
  unauditedServers++;
@@ -1550,11 +1844,14 @@ async function discoverCommand(options = {}) {
1550
1844
  }
1551
1845
 
1552
1846
  // Summary
1553
- console.log(`${c.dim}${''.repeat(60)}${c.reset}`);
1554
- 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' : ''}`));
1555
1848
  console.log();
1556
1849
  if (auditedServers > 0) console.log(` ${icons.safe} ${c.green}${auditedServers} audited${c.reset}`);
1557
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
+ }
1558
1855
  console.log();
1559
1856
 
1560
1857
  // --scan: automatically scan all servers with resolved source URLs (git-cloneable only)
@@ -1570,8 +1867,8 @@ async function discoverCommand(options = {}) {
1570
1867
  });
1571
1868
  const skipped = allServersWithUrls.filter(s => s.sourceUrl && !isCloneable(s.sourceUrl));
1572
1869
  if (dedupedTargets.length > 0) {
1573
- console.log(`${c.dim}${''.repeat(60)}${c.reset}`);
1574
- 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}`);
1575
1872
  if (skipped.length > 0) {
1576
1873
  console.log(` ${c.dim}(${skipped.length} skipped — no cloneable source URL)${c.reset}`);
1577
1874
  }
@@ -1585,8 +1882,7 @@ async function discoverCommand(options = {}) {
1585
1882
 
1586
1883
  if (scanResults.length > 1) {
1587
1884
  // Print combined scan summary
1588
- console.log(`${c.dim}${''.repeat(60)}${c.reset}`);
1589
- 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`));
1590
1886
  console.log();
1591
1887
 
1592
1888
  let totalFindings = 0;
@@ -1695,7 +1991,7 @@ async function auditRepo(url) {
1695
1991
  console.log();
1696
1992
 
1697
1993
  // Step 1: Clone
1698
- process.stdout.write(` ${c.dim}[1/4]${c.reset} Cloning repository...`);
1994
+ process.stdout.write(` ${stepProgress(1, 4)} Cloning repository...`);
1699
1995
  const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'agentaudit-'));
1700
1996
  const repoPath = path.join(tmpDir, 'repo');
1701
1997
  try {
@@ -1710,12 +2006,12 @@ async function auditRepo(url) {
1710
2006
  }
1711
2007
 
1712
2008
  // Step 2: Collect files
1713
- process.stdout.write(` ${c.dim}[2/4]${c.reset} Collecting source files...`);
2009
+ process.stdout.write(` ${stepProgress(2, 4)} Collecting source files...`);
1714
2010
  const files = collectFiles(repoPath);
1715
2011
  console.log(` ${c.green}${files.length} files${c.reset}`);
1716
2012
 
1717
2013
  // Step 3: Build audit payload
1718
- process.stdout.write(` ${c.dim}[3/4]${c.reset} Preparing audit payload...`);
2014
+ process.stdout.write(` ${stepProgress(3, 4)} Preparing audit payload...`);
1719
2015
  const auditPrompt = loadAuditPrompt();
1720
2016
 
1721
2017
  let codeBlock = '';
@@ -1788,7 +2084,7 @@ async function auditRepo(url) {
1788
2084
 
1789
2085
  // We have an API key — run LLM audit
1790
2086
  const modelLabel = modelOverride ? `${activeProvider} → ${activeLlm.model}` : activeProvider;
1791
- 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}...`);
1792
2088
 
1793
2089
  const systemPrompt = auditPrompt || 'You are a security auditor. Analyze the code and report findings as JSON.';
1794
2090
  const userMessage = [
@@ -2002,17 +2298,26 @@ async function auditRepo(url) {
2002
2298
  // Display results
2003
2299
  console.log();
2004
2300
  const riskScore = report.risk_score || 0;
2005
- console.log(` ${riskBadge(riskScore)} Risk ${riskScore}/100 ${c.bold}${report.result || 'unknown'}${c.reset}`);
2301
+ console.log(sectionHeader('Result'));
2302
+ console.log(` ${riskBadge(riskScore)}`);
2006
2303
  console.log();
2007
-
2304
+
2008
2305
  if (report.findings && report.findings.length > 0) {
2009
- console.log(` ${c.bold}Findings (${report.findings.length})${c.reset}`);
2306
+ console.log(sectionHeader(`Findings (${report.findings.length})`));
2010
2307
  console.log();
2011
2308
  for (const f of report.findings) {
2012
2309
  const sc = severityColor(f.severity);
2013
- console.log(` ${severityIcon(f.severity)} ${sc}${(f.severity || '').toUpperCase().padEnd(8)}${c.reset} ${f.title}`);
2014
- if (f.file) console.log(` ${c.dim}${f.file}${f.line ? ':' + f.line : ''}${c.reset}`);
2015
- 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);
2016
2321
  console.log();
2017
2322
  }
2018
2323
  } else {
@@ -2247,7 +2552,7 @@ function renderLeaderboardTab(data, width, opts = {}) {
2247
2552
  }
2248
2553
 
2249
2554
  const maxPts = leaderboard[0]?.total_points || 1;
2250
- const medals = ['🥇', '🥈', '🥉'];
2555
+ const medals = [`${c.yellow}★${c.reset}`, `${c.cyan}★${c.reset}`, `${c.magenta}★${c.reset}`];
2251
2556
 
2252
2557
  for (let i = 0; i < leaderboard.length; i++) {
2253
2558
  const entry = leaderboard[i];
@@ -2651,7 +2956,7 @@ async function leaderboardCommand(args) {
2651
2956
  }
2652
2957
 
2653
2958
  const maxPts = data[0]?.total_points || 1;
2654
- const medals = ['🥇', '🥈', '🥉'];
2959
+ const medals = [`${c.yellow}★${c.reset}`, `${c.cyan}★${c.reset}`, `${c.magenta}★${c.reset}`];
2655
2960
  const barW = 24;
2656
2961
 
2657
2962
  for (let i = 0; i < data.length; i++) {
@@ -2997,7 +3302,9 @@ async function main() {
2997
3302
  discover: [
2998
3303
  `${c.bold}agentaudit discover${c.reset} [options]`,
2999
3304
  ``,
3000
- `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.`,
3001
3308
  ``,
3002
3309
  `${c.bold}Options:${c.reset}`,
3003
3310
  ` --quick, -s Auto-scan all discovered servers (regex-based)`,
@@ -3185,6 +3492,21 @@ async function main() {
3185
3492
  ` agentaudit find fastmcp`,
3186
3493
  ],
3187
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
+ ],
3188
3510
  };
3189
3511
 
3190
3512
  // Show subcommand help: `agentaudit help <cmd>` or `agentaudit <cmd> --help`
@@ -3247,6 +3569,7 @@ async function main() {
3247
3569
  console.log(` ${c.cyan}model${c.reset} Configure LLM provider + model`);
3248
3570
  console.log(` ${c.cyan}setup${c.reset} Log in to agentaudit.dev (for report uploads)`);
3249
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`);
3250
3573
  console.log();
3251
3574
  console.log(` ${c.bold}FLAGS${c.reset}`);
3252
3575
  console.log(` ${c.dim}--json Machine-readable JSON output${c.reset}`);
@@ -3405,6 +3728,103 @@ async function main() {
3405
3728
  return;
3406
3729
  }
3407
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
+
3408
3828
  if (command === 'model') {
3409
3829
  const newModel = targets.filter(t => !t.startsWith('--'))[0];
3410
3830
  const config = loadLlmConfig();