agentaudit 3.10.2 → 3.10.3

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.
Files changed (2) hide show
  1. package/cli.mjs +668 -12
  2. package/package.json +1 -1
package/cli.mjs CHANGED
@@ -9,8 +9,11 @@
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
12
+ * dashboard Interactive full-screen dashboard
13
+ * leaderboard Top contributors ranking
14
+ * benchmark LLM model performance comparison
12
15
  * model [name|reset] Configure LLM provider + model
13
- * setup Create account / enter API key
16
+ * setup Log in to agentaudit.dev (for report uploads)
14
17
  * status Show current config + auth status
15
18
  * help [command] Show help
16
19
  *
@@ -417,12 +420,13 @@ async function registerAgent(agentName) {
417
420
  }
418
421
 
419
422
  async function setupCommand() {
420
- console.log(` ${c.bold}Setup${c.reset}`);
423
+ console.log(` ${c.bold}AgentAudit Login${c.reset}`);
424
+ console.log(` ${c.dim}Create an account to upload audit reports to agentaudit.dev${c.reset}`);
421
425
  console.log();
422
426
 
423
427
  const existing = loadCredentials();
424
428
  if (existing) {
425
- console.log(` ${icons.safe} Already configured as ${c.bold}${existing.agent_name}${c.reset}`);
429
+ console.log(` ${icons.safe} Already logged in as ${c.bold}${existing.agent_name}${c.reset}`);
426
430
  console.log(` ${c.dim}Key: ${existing.api_key.slice(0, 8)}...${c.reset}`);
427
431
  console.log();
428
432
  const answer = await askQuestion(` Reconfigure? ${c.dim}(y/N)${c.reset} `);
@@ -567,6 +571,120 @@ function severityIcon(sev) {
567
571
  }
568
572
  }
569
573
 
574
+ // ── TUI Rendering Helpers ───────────────────────────────
575
+
576
+ const term = {
577
+ clearScreen: '\x1b[2J\x1b[H',
578
+ hideCursor: '\x1b[?25l',
579
+ showCursor: '\x1b[?25h',
580
+ altScreenOn: '\x1b[?1049h',
581
+ altScreenOff: '\x1b[?1049l',
582
+ moveTo: (r, col) => `\x1b[${r};${col}H`,
583
+ clearLine: '\x1b[2K',
584
+ underline: '\x1b[4m',
585
+ noUnderline: '\x1b[24m',
586
+ };
587
+
588
+ const BOX = { tl: '╭', tr: '╮', bl: '╰', br: '╯', h: '─', v: '│', lt: '├', rt: '┤', tt: '┬', bt: '┴', x: '┼' };
589
+
590
+ // Strip ANSI escape codes for length calculations
591
+ function stripAnsi(str) {
592
+ return str.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '');
593
+ }
594
+
595
+ function visLen(str) {
596
+ return stripAnsi(str).length;
597
+ }
598
+
599
+ function padRight(str, len) {
600
+ const diff = len - visLen(str);
601
+ return diff > 0 ? str + ' '.repeat(diff) : str;
602
+ }
603
+
604
+ function padLeft(str, len) {
605
+ const diff = len - visLen(str);
606
+ return diff > 0 ? ' '.repeat(diff) + str : str;
607
+ }
608
+
609
+ function drawBox(title, contentLines, width) {
610
+ const inner = width - 4; // 2 for "│ " + 2 for " │"
611
+ const lines = [];
612
+ const titleStr = title ? ` ${title} ` : '';
613
+ const titleLen = visLen(titleStr);
614
+ const topDash = BOX.h.repeat(Math.max(1, inner + 2 - titleLen));
615
+ lines.push(` ${BOX.tl}${c.dim}─${c.reset}${c.bold}${titleStr}${c.reset}${c.dim}${topDash}${c.reset}${BOX.tr}`);
616
+ for (const line of contentLines) {
617
+ lines.push(` ${BOX.v} ${padRight(line, inner + 1)}${BOX.v}`);
618
+ }
619
+ lines.push(` ${BOX.bl}${c.dim}${BOX.h.repeat(inner + 2)}${c.reset}${BOX.br}`);
620
+ return lines;
621
+ }
622
+
623
+ // ████████░░░░ proportional bar
624
+ function renderBar(value, maxValue, maxWidth) {
625
+ if (maxValue <= 0 || value <= 0) return c.dim + '░'.repeat(maxWidth) + c.reset;
626
+ const filled = Math.min(Math.round((value / maxValue) * maxWidth), maxWidth);
627
+ const empty = maxWidth - filled;
628
+ return c.cyan + '█'.repeat(filled) + c.dim + '░'.repeat(empty) + c.reset;
629
+ }
630
+
631
+ // [████████░░] 89%
632
+ function renderGauge(value, max, width) {
633
+ const pct = max > 0 ? Math.round((value / max) * 100) : 0;
634
+ const inner = width - 2; // subtract brackets
635
+ const filled = Math.min(Math.round((pct / 100) * inner), inner);
636
+ const empty = inner - filled;
637
+ const color = pct >= 80 ? c.green : pct >= 50 ? c.yellow : c.red;
638
+ return `[${color}${'█'.repeat(filled)}${c.dim}${'░'.repeat(empty)}${c.reset}] ${pct}%`;
639
+ }
640
+
641
+ // ●●●○○ colored severity dots
642
+ function severityDots(breakdown) {
643
+ const parts = [];
644
+ const critical = breakdown?.critical || 0;
645
+ const high = breakdown?.high || 0;
646
+ const medium = breakdown?.medium || 0;
647
+ const low = breakdown?.low || 0;
648
+ for (let i = 0; i < Math.min(critical, 3); i++) parts.push(`${c.red}●${c.reset}`);
649
+ for (let i = 0; i < Math.min(high, 3); i++) parts.push(`${c.red}●${c.reset}`);
650
+ for (let i = 0; i < Math.min(medium, 2); i++) parts.push(`${c.yellow}●${c.reset}`);
651
+ for (let i = 0; i < Math.min(low, 2); i++) parts.push(`${c.blue}●${c.reset}`);
652
+ // fill remaining with empty dots up to 5
653
+ while (parts.length < 5) parts.push(`${c.dim}○${c.reset}`);
654
+ return parts.slice(0, 5).join('');
655
+ }
656
+
657
+ // ▁▂▃▅▇█▆▃ mini sparkline
658
+ function sparkline(values) {
659
+ const chars = '▁▂▃▄▅▆▇█';
660
+ if (!values || values.length === 0) return '';
661
+ const max = Math.max(...values, 1);
662
+ return values.map(v => {
663
+ const idx = Math.min(Math.round((v / max) * (chars.length - 1)), chars.length - 1);
664
+ return chars[idx];
665
+ }).join('');
666
+ }
667
+
668
+ function fmtNum(n) {
669
+ if (n == null) return '0';
670
+ return n.toLocaleString('en-US');
671
+ }
672
+
673
+ function fmtPct(n) {
674
+ if (n == null) return '0%';
675
+ return Math.round(n) + '%';
676
+ }
677
+
678
+ function dashboardBanner() {
679
+ const ver = getVersion();
680
+ return [
681
+ ` ${BOX.tl}${c.dim}${BOX.h.repeat(35)}${c.reset}${BOX.tr}`,
682
+ ` ${BOX.v} ${c.bold}${c.cyan}⛨ AgentAudit${c.reset} ${c.dim}v${ver}${c.reset}${' '.repeat(Math.max(0, 19 - ver.length))}${BOX.v}`,
683
+ ` ${BOX.v} ${c.dim}Security Registry for AI Agents${c.reset} ${BOX.v}`,
684
+ ` ${BOX.bl}${c.dim}${BOX.h.repeat(35)}${c.reset}${BOX.br}`,
685
+ ];
686
+ }
687
+
570
688
  // ── File Collection (same logic as MCP server) ──────────
571
689
 
572
690
  function formatApiError(error, provider, statusCode) {
@@ -1940,6 +2058,443 @@ async function checkPackage(name) {
1940
2058
  return data;
1941
2059
  }
1942
2060
 
2061
+ // ── Dashboard / Leaderboard / Benchmark Commands ────────
2062
+
2063
+ async function fetchDashboardData() {
2064
+ const creds = loadCredentials();
2065
+ const fetches = [
2066
+ fetch(`${REGISTRY_URL}/api/stats`, { signal: AbortSignal.timeout(15_000) }).then(r => r.ok ? r.json() : null).catch(() => null),
2067
+ fetch(`${REGISTRY_URL}/api/leaderboard?limit=50`, { signal: AbortSignal.timeout(15_000) }).then(r => r.ok ? r.json() : null).catch(() => null),
2068
+ fetch(`${REGISTRY_URL}/api/benchmark`, { signal: AbortSignal.timeout(15_000) }).then(r => r.ok ? r.json() : null).catch(() => null),
2069
+ ];
2070
+ if (creds?.agent_name && creds.agent_name !== 'env') {
2071
+ fetches.push(
2072
+ fetch(`${REGISTRY_URL}/api/agents/${encodeURIComponent(creds.agent_name)}`, {
2073
+ headers: { 'Authorization': `Bearer ${creds.api_key}` },
2074
+ signal: AbortSignal.timeout(15_000),
2075
+ }).then(r => r.ok ? r.json() : null).catch(() => null)
2076
+ );
2077
+ } else {
2078
+ fetches.push(Promise.resolve(null));
2079
+ }
2080
+ const [stats, leaderboard, benchmark, agent] = await Promise.all(fetches);
2081
+ return { stats, leaderboard, benchmark, agent, creds };
2082
+ }
2083
+
2084
+ function renderOverviewTab(data, width) {
2085
+ const { stats, agent, leaderboard, creds } = data;
2086
+ const lines = [];
2087
+ const halfW = Math.min(Math.floor((width - 6) / 2), 40);
2088
+
2089
+ // Profile box
2090
+ const profileLines = [];
2091
+ if (agent && creds) {
2092
+ // Find rank
2093
+ let rank = '-';
2094
+ if (leaderboard && Array.isArray(leaderboard)) {
2095
+ const idx = leaderboard.findIndex(e => e.agent_name === creds.agent_name);
2096
+ if (idx >= 0) rank = `#${idx + 1} of ${leaderboard.length}`;
2097
+ }
2098
+ profileLines.push(`${c.bold}${creds.agent_name}${c.reset}${' '.repeat(Math.max(1, halfW - 14 - visLen(creds.agent_name) - visLen(rank)))}${c.dim}${rank}${c.reset}`);
2099
+ profileLines.push(`Points ${c.bold}${fmtNum(agent.total_points)}${c.reset}`);
2100
+ profileLines.push(`Audits ${c.bold}${fmtNum(agent.total_reports)}${c.reset}`);
2101
+ profileLines.push(`Findings ${c.bold}${fmtNum(agent.total_findings_submitted)}${c.reset} ${c.dim}(${fmtNum(agent.total_findings_confirmed)} confirmed)${c.reset}`);
2102
+ const sev = agent.severity_breakdown || {};
2103
+ profileLines.push('');
2104
+ const sevParts = [];
2105
+ if (sev.critical) sevParts.push(`${c.red}${sev.critical} crit${c.reset}`);
2106
+ if (sev.high) sevParts.push(`${c.red}${sev.high} high${c.reset}`);
2107
+ if (sev.medium) sevParts.push(`${c.yellow}${sev.medium} med${c.reset}`);
2108
+ if (sev.low) sevParts.push(`${c.blue}${sev.low} low${c.reset}`);
2109
+ profileLines.push(sevParts.join(' ') || `${c.dim}no findings yet${c.reset}`);
2110
+ } else {
2111
+ profileLines.push(`${c.dim}Not logged in${c.reset}`);
2112
+ profileLines.push(`${c.dim}Run ${c.cyan}agentaudit setup${c.dim} to create account${c.reset}`);
2113
+ }
2114
+
2115
+ // Registry box
2116
+ const regLines = [];
2117
+ if (stats) {
2118
+ regLines.push(`Packages Audited ${c.bold}${fmtNum(stats.skills_audited)}${c.reset}`);
2119
+ regLines.push(`Total Findings ${c.bold}${fmtNum(stats.total_findings)}${c.reset}`);
2120
+ regLines.push(`Total Reports ${c.bold}${fmtNum(stats.total_reports)}${c.reset}`);
2121
+ regLines.push(`Contributors ${c.bold}${fmtNum(stats.reporters)}${c.reset}`);
2122
+ regLines.push(`Avg Trust Score ${c.bold}${stats.avg_trust_score || 0}${c.reset}`);
2123
+ regLines.push('');
2124
+ const parts = [];
2125
+ if (stats.safe_packages) parts.push(`${c.green}●${fmtNum(stats.safe_packages)} safe${c.reset}`);
2126
+ if (stats.caution_packages) parts.push(`${c.yellow}●${fmtNum(stats.caution_packages)} caution${c.reset}`);
2127
+ if (stats.unsafe_packages) parts.push(`${c.red}●${fmtNum(stats.unsafe_packages)} unsafe${c.reset}`);
2128
+ regLines.push(parts.join(' ') || `${c.dim}no packages yet${c.reset}`);
2129
+ } else {
2130
+ regLines.push(`${c.dim}Could not load registry stats${c.reset}`);
2131
+ }
2132
+
2133
+ const boxW = halfW + 4;
2134
+ const profileBox = drawBox('Your Profile', profileLines, boxW);
2135
+ const registryBox = drawBox('Registry', regLines, boxW);
2136
+
2137
+ // Side by side if wide enough, stacked otherwise
2138
+ if (width >= boxW * 2 + 4) {
2139
+ const maxLen = Math.max(profileBox.length, registryBox.length);
2140
+ while (profileBox.length < maxLen) profileBox.push(` ${BOX.v} ${' '.repeat(halfW + 1)}${BOX.v}`);
2141
+ while (registryBox.length < maxLen) registryBox.push(` ${BOX.v} ${' '.repeat(halfW + 1)}${BOX.v}`);
2142
+ for (let i = 0; i < maxLen; i++) {
2143
+ lines.push(profileBox[i] + ' ' + registryBox[i].trimStart());
2144
+ }
2145
+ } else {
2146
+ lines.push(...profileBox, '', ...registryBox);
2147
+ }
2148
+
2149
+ return lines;
2150
+ }
2151
+
2152
+ function renderLeaderboardTab(data, width, opts = {}) {
2153
+ const { leaderboard, creds } = data;
2154
+ const lines = [];
2155
+ const maxNameW = 20;
2156
+ const barW = Math.min(Math.max(10, width - 70), 30);
2157
+
2158
+ if (!leaderboard || !Array.isArray(leaderboard) || leaderboard.length === 0) {
2159
+ lines.push(` ${c.dim}No leaderboard data available${c.reset}`);
2160
+ return lines;
2161
+ }
2162
+
2163
+ const maxPts = leaderboard[0]?.total_points || 1;
2164
+ const medals = ['🥇', '🥈', '🥉'];
2165
+
2166
+ for (let i = 0; i < leaderboard.length; i++) {
2167
+ const entry = leaderboard[i];
2168
+ const name = (entry.agent_name || '').slice(0, maxNameW);
2169
+ const isMe = creds && entry.agent_name === creds.agent_name;
2170
+ const prefix = i < 3 ? ` ${medals[i]} ` : ` ${c.dim}#${String(i + 1).padStart(2)}${c.reset} `;
2171
+ const nameStr = isMe ? `${c.green}${c.bold}${name}${c.reset}` : name;
2172
+ const bar = renderBar(entry.total_points || 0, maxPts, barW);
2173
+ const pts = padLeft(`${fmtNum(entry.total_points || 0)} pts`, 12);
2174
+ const audits = padLeft(`${fmtNum(entry.total_reports || 0)} audits`, 12);
2175
+ const extra = entry.monthly_reports != null ? padLeft(`${fmtNum(entry.monthly_reports)} this mo`, 12) : '';
2176
+ lines.push(`${prefix}${padRight(nameStr, maxNameW + (isMe ? 14 : 0))} ${bar} ${pts} ${audits}${extra}`);
2177
+
2178
+ // Separator after top 3
2179
+ if (i === 2 && leaderboard.length > 3) {
2180
+ lines.push(` ${c.dim}${'─'.repeat(Math.min(width - 4, 76))}${c.reset}`);
2181
+ }
2182
+ }
2183
+
2184
+ // Highlight current user if not in top list
2185
+ if (creds && !leaderboard.find(e => e.agent_name === creds.agent_name)) {
2186
+ lines.push('');
2187
+ lines.push(` ${c.dim}← you are not on this leaderboard yet${c.reset}`);
2188
+ }
2189
+
2190
+ return lines;
2191
+ }
2192
+
2193
+ function renderBenchmarkTab(data, width) {
2194
+ const { benchmark } = data;
2195
+ const lines = [];
2196
+
2197
+ if (!benchmark || !benchmark.models || benchmark.models.length === 0) {
2198
+ lines.push(` ${c.dim}No benchmark data available${c.reset}`);
2199
+ return lines;
2200
+ }
2201
+
2202
+ const overview = benchmark.overview || {};
2203
+ lines.push(` ${c.bold}${fmtNum(benchmark.models.length)}${c.reset} models ${c.dim}│${c.reset} ${c.bold}${fmtNum(overview.total_reports || 0)}${c.reset} audits ${c.dim}│${c.reset} ${c.bold}${fmtNum(overview.total_findings || 0)}${c.reset} findings`);
2204
+ lines.push('');
2205
+
2206
+ // Header
2207
+ const nameW = 26;
2208
+ const hdr = ` ${padRight(`${c.bold}Model${c.reset}`, nameW + 9)} ${padRight('Audits', 8)} ${padRight('Risk', 8)} ${padRight('Detection', 16)} Severity`;
2209
+ lines.push(hdr);
2210
+ lines.push(` ${c.dim}${'─'.repeat(Math.min(width - 4, 80))}${c.reset}`);
2211
+
2212
+ for (const m of benchmark.models) {
2213
+ const name = (m.audit_model || 'unknown').slice(0, nameW - 2);
2214
+ const audits = padLeft(fmtNum(m.total_audits), 6);
2215
+ const riskVal = parseFloat(m.avg_risk_score) || 0;
2216
+ const riskColor = riskVal <= 20 ? c.green : riskVal <= 40 ? c.yellow : c.red;
2217
+ const risk = `${riskColor}${padLeft(String(Math.round(riskVal)), 3)}${c.reset}`;
2218
+ const detection = renderGauge(m.detection_rate || 0, 100, 10);
2219
+ const dots = severityDots(m.severity_breakdown);
2220
+ lines.push(` ${padRight(name, nameW)} ${audits} ${risk} ${detection} ${dots}`);
2221
+ }
2222
+
2223
+ // Vulnerability landscape
2224
+ const cats = benchmark.global_categories;
2225
+ if (cats && cats.length > 0) {
2226
+ lines.push('');
2227
+ const catTotal = cats.reduce((sum, cat) => sum + (cat.count || 0), 0) || 1;
2228
+ const catW = Math.min(width - 8, 56);
2229
+ const catLines = [];
2230
+ for (const cat of cats.slice(0, 6)) {
2231
+ const pct = Math.round((cat.count / catTotal) * 100);
2232
+ const barFill = Math.round((cat.count / catTotal) * (catW - 30));
2233
+ catLines.push(`${padRight(cat.category || 'Other', 22)} ${c.cyan}${'█'.repeat(Math.max(1, barFill))}${c.dim}${'░'.repeat(Math.max(0, catW - 30 - barFill))}${c.reset} ${padLeft(fmtPct(pct), 4)}`);
2234
+ }
2235
+ lines.push(...drawBox('Vulnerability Landscape', catLines, catW + 4));
2236
+ }
2237
+
2238
+ // Cross-model findings
2239
+ const cross = benchmark.cross_model_findings;
2240
+ if (cross && cross.length > 0) {
2241
+ lines.push('');
2242
+ lines.push(` ${c.bold}Cross-Model Findings${c.reset} ${c.dim}(confirmed by multiple models)${c.reset}`);
2243
+ for (const cf of cross.slice(0, 5)) {
2244
+ const models = (cf.models || []).slice(0, 3).join(', ');
2245
+ lines.push(` ${c.dim}•${c.reset} ${cf.title || cf.pattern_id} ${c.dim}[${cf.model_count} models: ${models}]${c.reset}`);
2246
+ }
2247
+ }
2248
+
2249
+ return lines;
2250
+ }
2251
+
2252
+ async function leaderboardCommand(args) {
2253
+ const tabArg = args.find((a, i) => args[i - 1] === '--tab') || 'overall';
2254
+ const showAll = args.includes('--all');
2255
+ const limit = showAll ? 200 : 20;
2256
+
2257
+ if (!quietMode && !jsonMode) {
2258
+ banner();
2259
+ process.stdout.write(` ${c.dim}Loading leaderboard...${c.reset}`);
2260
+ }
2261
+
2262
+ let data;
2263
+ try {
2264
+ const url = `${REGISTRY_URL}/api/leaderboard?tab=${encodeURIComponent(tabArg)}&limit=${limit}`;
2265
+ const res = await fetch(url, { signal: AbortSignal.timeout(15_000) });
2266
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
2267
+ data = await res.json();
2268
+ } catch (err) {
2269
+ if (!jsonMode) {
2270
+ process.stdout.write('\r\x1b[2K');
2271
+ console.log(` ${c.red}Failed to load leaderboard: ${err.message}${c.reset}`);
2272
+ }
2273
+ process.exitCode = 1;
2274
+ return;
2275
+ }
2276
+
2277
+ if (jsonMode) {
2278
+ console.log(JSON.stringify(data, null, 2));
2279
+ return;
2280
+ }
2281
+
2282
+ process.stdout.write('\r\x1b[2K');
2283
+ const creds = loadCredentials();
2284
+ console.log(` ${c.bold}Leaderboard${c.reset} ${c.dim}${tabArg}${c.reset}`);
2285
+ console.log();
2286
+
2287
+ if (!Array.isArray(data) || data.length === 0) {
2288
+ console.log(` ${c.dim}No data available${c.reset}`);
2289
+ return;
2290
+ }
2291
+
2292
+ const maxPts = data[0]?.total_points || 1;
2293
+ const medals = ['🥇', '🥈', '🥉'];
2294
+ const barW = 24;
2295
+
2296
+ for (let i = 0; i < data.length; i++) {
2297
+ const entry = data[i];
2298
+ const name = (entry.agent_name || '').slice(0, 20);
2299
+ const isMe = creds && entry.agent_name === creds.agent_name;
2300
+ const prefix = i < 3 ? ` ${medals[i]} ` : ` #${String(i + 1).padStart(2)} `;
2301
+ const nameStr = isMe ? `${c.green}${c.bold}${name}${c.reset}` : name;
2302
+ const bar = renderBar(entry.total_points || 0, maxPts, barW);
2303
+ const pts = `${fmtNum(entry.total_points || 0)} pts`;
2304
+ const audits = `${fmtNum(entry.total_reports || 0)} audits`;
2305
+ console.log(`${prefix}${padRight(nameStr, 22 + (isMe ? 14 : 0))} ${bar} ${padLeft(pts, 12)} ${padLeft(audits, 10)}`);
2306
+ if (i === 2 && data.length > 3) {
2307
+ console.log(` ${c.dim}${'─'.repeat(74)}${c.reset}`);
2308
+ }
2309
+ }
2310
+ console.log();
2311
+ }
2312
+
2313
+ async function benchmarkCommand(args) {
2314
+ if (!quietMode && !jsonMode) {
2315
+ banner();
2316
+ process.stdout.write(` ${c.dim}Loading benchmark data...${c.reset}`);
2317
+ }
2318
+
2319
+ let data;
2320
+ try {
2321
+ const res = await fetch(`${REGISTRY_URL}/api/benchmark`, { signal: AbortSignal.timeout(15_000) });
2322
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
2323
+ data = await res.json();
2324
+ } catch (err) {
2325
+ if (!jsonMode) {
2326
+ process.stdout.write('\r\x1b[2K');
2327
+ console.log(` ${c.red}Failed to load benchmark: ${err.message}${c.reset}`);
2328
+ }
2329
+ process.exitCode = 1;
2330
+ return;
2331
+ }
2332
+
2333
+ if (jsonMode) {
2334
+ console.log(JSON.stringify(data, null, 2));
2335
+ return;
2336
+ }
2337
+
2338
+ process.stdout.write('\r\x1b[2K');
2339
+ const width = process.stdout.columns || 80;
2340
+ const benchLines = renderBenchmarkTab({ benchmark: data }, width);
2341
+ console.log(` ${c.bold}Model Benchmark${c.reset} ${c.dim}— LLM Audit Performance${c.reset}`);
2342
+ console.log();
2343
+ for (const line of benchLines) console.log(line);
2344
+ console.log();
2345
+ }
2346
+
2347
+ async function dashboardCommand() {
2348
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
2349
+ console.log(`${c.red}Error: dashboard requires an interactive terminal${c.reset}`);
2350
+ console.log(`${c.dim}Tip: use ${c.cyan}agentaudit leaderboard${c.dim} or ${c.cyan}agentaudit benchmark${c.dim} for non-interactive output${c.reset}`);
2351
+ process.exitCode = 1;
2352
+ return;
2353
+ }
2354
+
2355
+ const tabs = [
2356
+ { key: '1', label: 'Overview' },
2357
+ { key: '2', label: 'Leaderboard' },
2358
+ { key: '3', label: 'Benchmark' },
2359
+ ];
2360
+ let activeTab = 0;
2361
+ let scrollOffset = 0;
2362
+ let running = true;
2363
+
2364
+ // Enter alt screen
2365
+ process.stdout.write(term.altScreenOn + term.hideCursor);
2366
+
2367
+ // Loading screen
2368
+ process.stdout.write(term.clearScreen);
2369
+ const bannerLines = dashboardBanner();
2370
+ for (const line of bannerLines) process.stdout.write(line + '\n');
2371
+ process.stdout.write(`\n ${c.dim}Loading data...${c.reset}\n`);
2372
+
2373
+ // Fetch data
2374
+ const data = await fetchDashboardData();
2375
+
2376
+ function render() {
2377
+ const cols = process.stdout.columns || 80;
2378
+ const rows = process.stdout.rows || 24;
2379
+ let output = term.clearScreen;
2380
+
2381
+ // Banner
2382
+ const bLines = dashboardBanner();
2383
+ for (const line of bLines) output += line + '\n';
2384
+ output += '\n';
2385
+
2386
+ // Tab bar
2387
+ let tabBar = ' ';
2388
+ for (let i = 0; i < tabs.length; i++) {
2389
+ const tab = tabs[i];
2390
+ if (i === activeTab) {
2391
+ tabBar += `${c.bold}${c.cyan}${term.underline} [${tab.key}] ${tab.label} ${term.noUnderline}${c.reset}`;
2392
+ } else {
2393
+ tabBar += `${c.dim} [${tab.key}] ${tab.label} ${c.reset}`;
2394
+ }
2395
+ if (i < tabs.length - 1) tabBar += `${c.dim}──${c.reset}`;
2396
+ }
2397
+ output += tabBar + '\n\n';
2398
+
2399
+ // Tab content
2400
+ let contentLines = [];
2401
+ switch (activeTab) {
2402
+ case 0:
2403
+ contentLines = renderOverviewTab(data, cols);
2404
+ break;
2405
+ case 1:
2406
+ contentLines = renderLeaderboardTab(data, cols);
2407
+ break;
2408
+ case 2:
2409
+ contentLines = renderBenchmarkTab(data, cols);
2410
+ break;
2411
+ }
2412
+
2413
+ // Apply scroll
2414
+ const availableRows = rows - 10; // header + tab bar + footer
2415
+ const maxScroll = Math.max(0, contentLines.length - availableRows);
2416
+ scrollOffset = Math.min(scrollOffset, maxScroll);
2417
+ scrollOffset = Math.max(0, scrollOffset);
2418
+
2419
+ const visible = contentLines.slice(scrollOffset, scrollOffset + availableRows);
2420
+ for (const line of visible) output += line + '\n';
2421
+
2422
+ // Footer
2423
+ output += '\n';
2424
+ const scrollInfo = contentLines.length > availableRows ? ` ${c.dim}[${scrollOffset + 1}-${Math.min(scrollOffset + availableRows, contentLines.length)}/${contentLines.length}]${c.reset}` : '';
2425
+ output += ` ${c.dim}←→ tab ↑↓ scroll 1-3 jump q quit${c.reset}${scrollInfo}\n`;
2426
+
2427
+ process.stdout.write(output);
2428
+ }
2429
+
2430
+ function cleanup() {
2431
+ if (!running) return;
2432
+ running = false;
2433
+ process.stdout.write(term.showCursor + term.altScreenOff);
2434
+ try { process.stdin.setRawMode(false); } catch {}
2435
+ process.stdin.pause();
2436
+ process.stdin.removeListener('data', onKeypress);
2437
+ process.stdout.removeListener('resize', render);
2438
+ }
2439
+
2440
+ function onKeypress(key) {
2441
+ if (!running) return;
2442
+
2443
+ // q or Ctrl+C = quit
2444
+ if (key === 'q' || key === '\x03') {
2445
+ cleanup();
2446
+ return;
2447
+ }
2448
+
2449
+ // Tab navigation
2450
+ if (key === '\x1b[C' || key === '\t') { // Right arrow or Tab
2451
+ activeTab = (activeTab + 1) % tabs.length;
2452
+ scrollOffset = 0;
2453
+ render();
2454
+ return;
2455
+ }
2456
+ if (key === '\x1b[D') { // Left arrow
2457
+ activeTab = (activeTab - 1 + tabs.length) % tabs.length;
2458
+ scrollOffset = 0;
2459
+ render();
2460
+ return;
2461
+ }
2462
+
2463
+ // Number keys
2464
+ if (key === '1') { activeTab = 0; scrollOffset = 0; render(); return; }
2465
+ if (key === '2') { activeTab = 1; scrollOffset = 0; render(); return; }
2466
+ if (key === '3') { activeTab = 2; scrollOffset = 0; render(); return; }
2467
+
2468
+ // Scroll
2469
+ if (key === '\x1b[A' || key === 'k') { scrollOffset = Math.max(0, scrollOffset - 1); render(); return; } // Up
2470
+ if (key === '\x1b[B' || key === 'j') { scrollOffset++; render(); return; } // Down
2471
+ if (key === '\x1b[5~') { scrollOffset = Math.max(0, scrollOffset - 10); render(); return; } // PageUp
2472
+ if (key === '\x1b[6~') { scrollOffset += 10; render(); return; } // PageDown
2473
+ }
2474
+
2475
+ // Setup input
2476
+ process.stdin.setRawMode(true);
2477
+ process.stdin.resume();
2478
+ process.stdin.setEncoding('utf8');
2479
+ process.stdin.on('data', onKeypress);
2480
+ process.stdout.on('resize', render);
2481
+
2482
+ // Graceful cleanup
2483
+ process.on('exit', cleanup);
2484
+ process.on('SIGINT', () => { cleanup(); process.exit(0); });
2485
+ process.on('SIGTERM', () => { cleanup(); process.exit(0); });
2486
+
2487
+ // Initial render
2488
+ render();
2489
+
2490
+ // Keep alive until user quits
2491
+ await new Promise(resolve => {
2492
+ const check = setInterval(() => {
2493
+ if (!running) { clearInterval(check); resolve(); }
2494
+ }, 100);
2495
+ });
2496
+ }
2497
+
1943
2498
  // ── Main ────────────────────────────────────────────────
1944
2499
 
1945
2500
  async function main() {
@@ -2054,9 +2609,10 @@ async function main() {
2054
2609
  setup: [
2055
2610
  `${c.bold}agentaudit setup${c.reset}`,
2056
2611
  ``,
2057
- `Create an AgentAudit account or enter an existing API key.`,
2058
- `This is for uploading audit reports to the registry — not for`,
2059
- `LLM provider configuration (use \`agentaudit model\` for that).`,
2612
+ `Log in to agentaudit.dev — create an account or enter an existing API key.`,
2613
+ `This enables uploading audit reports to the public registry.`,
2614
+ ``,
2615
+ `${c.bold}Note:${c.reset} This is NOT for LLM/provider configuration. Use ${c.cyan}agentaudit model${c.reset} for that.`,
2060
2616
  ``,
2061
2617
  `${c.bold}Examples:${c.reset}`,
2062
2618
  ` agentaudit setup`,
@@ -2073,15 +2629,70 @@ async function main() {
2073
2629
  ` agentaudit status`,
2074
2630
  ` agentaudit status --json`,
2075
2631
  ],
2632
+ dashboard: [
2633
+ `${c.bold}agentaudit dashboard${c.reset}`,
2634
+ ``,
2635
+ `Interactive full-screen dashboard with stats, leaderboard, and benchmark.`,
2636
+ `Uses alternate screen buffer — your scrollback stays intact.`,
2637
+ ``,
2638
+ `${c.bold}Navigation:${c.reset}`,
2639
+ ` ←→ / Tab Switch between tabs`,
2640
+ ` 1-3 Jump to tab by number`,
2641
+ ` ↑↓ / j/k Scroll content`,
2642
+ ` PgUp/PgDn Scroll fast`,
2643
+ ` q / Ctrl+C Quit`,
2644
+ ``,
2645
+ `${c.bold}Tabs:${c.reset}`,
2646
+ ` [1] Overview Your profile + registry stats`,
2647
+ ` [2] Leaderboard Top contributors ranking`,
2648
+ ` [3] Benchmark LLM model performance comparison`,
2649
+ ``,
2650
+ `${c.bold}Aliases:${c.reset} dashboard, dash`,
2651
+ ],
2652
+ dash: null, // alias → dashboard
2653
+ leaderboard: [
2654
+ `${c.bold}agentaudit leaderboard${c.reset} [options]`,
2655
+ ``,
2656
+ `Show top contributors ranking. Pipe-friendly output (no alt-screen).`,
2657
+ ``,
2658
+ `${c.bold}Options:${c.reset}`,
2659
+ ` --tab <name> Category: overall (default), monthly, reviewers, verifiers, streaks`,
2660
+ ` --all Show all entries (default: top 20)`,
2661
+ ` --json Machine-readable JSON output`,
2662
+ ``,
2663
+ `${c.bold}Aliases:${c.reset} leaderboard, lb`,
2664
+ ``,
2665
+ `${c.bold}Examples:${c.reset}`,
2666
+ ` agentaudit leaderboard`,
2667
+ ` agentaudit leaderboard --tab monthly`,
2668
+ ` agentaudit leaderboard --all --json`,
2669
+ ],
2670
+ lb: null, // alias → leaderboard
2671
+ benchmark: [
2672
+ `${c.bold}agentaudit benchmark${c.reset} [options]`,
2673
+ ``,
2674
+ `Compare LLM model performance across security audits.`,
2675
+ `Shows detection rates, severity breakdown, and vulnerability landscape.`,
2676
+ ``,
2677
+ `${c.bold}Options:${c.reset}`,
2678
+ ` --json Machine-readable JSON output`,
2679
+ ``,
2680
+ `${c.bold}Aliases:${c.reset} benchmark, bench`,
2681
+ ``,
2682
+ `${c.bold}Examples:${c.reset}`,
2683
+ ` agentaudit benchmark`,
2684
+ ` agentaudit benchmark --json`,
2685
+ ],
2686
+ bench: null, // alias → benchmark
2076
2687
  };
2077
2688
 
2078
2689
  // Show subcommand help: `agentaudit help <cmd>` or `agentaudit <cmd> --help`
2079
2690
  function showSubcommandHelp(cmd) {
2080
2691
  let helpLines = subcommandHelp[cmd];
2081
- if (helpLines === null && subcommandHelp[cmd] === null) {
2692
+ if (helpLines === null) {
2082
2693
  // alias redirect
2083
- const alias = cmd === 'check' ? 'lookup' : cmd;
2084
- helpLines = subcommandHelp[alias];
2694
+ const aliases = { check: 'lookup', dash: 'dashboard', lb: 'leaderboard', bench: 'benchmark' };
2695
+ helpLines = subcommandHelp[aliases[cmd] || cmd];
2085
2696
  }
2086
2697
  if (!helpLines) {
2087
2698
  console.log(` ${c.red}No help available for "${cmd}"${c.reset}`);
@@ -2124,9 +2735,14 @@ async function main() {
2124
2735
  console.log(` ${c.cyan}audit${c.reset} <url> [url...] Deep LLM-powered security audit (~30s)`);
2125
2736
  console.log(` ${c.cyan}lookup${c.reset} <name> Look up package in registry`);
2126
2737
  console.log();
2738
+ console.log(` ${c.bold}COMMUNITY${c.reset}`);
2739
+ console.log(` ${c.cyan}dashboard${c.reset} Interactive dashboard (full-screen)`);
2740
+ console.log(` ${c.cyan}leaderboard${c.reset} Top contributors ranking`);
2741
+ console.log(` ${c.cyan}benchmark${c.reset} LLM model performance comparison`);
2742
+ console.log();
2127
2743
  console.log(` ${c.bold}CONFIGURATION${c.reset}`);
2128
2744
  console.log(` ${c.cyan}model${c.reset} Configure LLM provider + model`);
2129
- console.log(` ${c.cyan}setup${c.reset} Create account / enter API key`);
2745
+ console.log(` ${c.cyan}setup${c.reset} Log in to agentaudit.dev (for report uploads)`);
2130
2746
  console.log(` ${c.cyan}status${c.reset} Show current config + auth status`);
2131
2747
  console.log();
2132
2748
  console.log(` ${c.bold}FLAGS${c.reset}`);
@@ -2154,9 +2770,23 @@ async function main() {
2154
2770
  // Default no-arg → discover
2155
2771
  const command = args.length === 0 ? 'discover' : args[0];
2156
2772
  const targets = args.slice(1);
2157
-
2773
+
2774
+ // Dashboard/Leaderboard/Benchmark — before banner() since dashboard uses alt-screen
2775
+ if (command === 'dashboard' || command === 'dash') {
2776
+ await dashboardCommand();
2777
+ return;
2778
+ }
2779
+ if (command === 'leaderboard' || command === 'lb') {
2780
+ await leaderboardCommand(targets);
2781
+ return;
2782
+ }
2783
+ if (command === 'benchmark' || command === 'bench') {
2784
+ await benchmarkCommand(targets);
2785
+ return;
2786
+ }
2787
+
2158
2788
  banner();
2159
-
2789
+
2160
2790
  if (command === 'setup') {
2161
2791
  await setupCommand();
2162
2792
  return;
@@ -2218,6 +2848,32 @@ async function main() {
2218
2848
  }
2219
2849
  console.log();
2220
2850
 
2851
+ // Personal stats (from registry, silently skip on error)
2852
+ if (creds?.agent_name && creds.agent_name !== 'env') {
2853
+ try {
2854
+ const [agentRes, lbRes] = await Promise.all([
2855
+ fetch(`${REGISTRY_URL}/api/agents/${encodeURIComponent(creds.agent_name)}`, {
2856
+ headers: { 'Authorization': `Bearer ${creds.api_key}` },
2857
+ signal: AbortSignal.timeout(10_000),
2858
+ }).then(r => r.ok ? r.json() : null),
2859
+ fetch(`${REGISTRY_URL}/api/leaderboard?limit=100`, { signal: AbortSignal.timeout(10_000) }).then(r => r.ok ? r.json() : null),
2860
+ ]);
2861
+ if (agentRes) {
2862
+ console.log(` ${c.bold}Profile${c.reset}`);
2863
+ let rank = '-';
2864
+ if (Array.isArray(lbRes)) {
2865
+ const idx = lbRes.findIndex(e => e.agent_name === creds.agent_name);
2866
+ if (idx >= 0) rank = `#${idx + 1} of ${lbRes.length}`;
2867
+ }
2868
+ console.log(` Rank ${c.bold}${rank}${c.reset}`);
2869
+ console.log(` Points ${c.bold}${fmtNum(agentRes.total_points || 0)}${c.reset}`);
2870
+ console.log(` Audits ${c.bold}${fmtNum(agentRes.total_reports || 0)}${c.reset}`);
2871
+ console.log(` Findings ${c.bold}${fmtNum(agentRes.total_findings_submitted || 0)}${c.reset} ${c.dim}(${fmtNum(agentRes.total_findings_confirmed || 0)} confirmed)${c.reset}`);
2872
+ console.log();
2873
+ }
2874
+ } catch {}
2875
+ }
2876
+
2221
2877
  // JSON mode
2222
2878
  if (jsonMode) {
2223
2879
  const status = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentaudit",
3
- "version": "3.10.2",
3
+ "version": "3.10.3",
4
4
  "description": "Security scanner for AI packages — MCP server + CLI",
5
5
  "type": "module",
6
6
  "bin": {