agentaudit 3.10.2 → 3.10.4

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 (3) hide show
  1. package/README.md +99 -54
  2. package/cli.mjs +1151 -14
  3. package/package.json +1 -1
package/cli.mjs CHANGED
@@ -9,8 +9,13 @@
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
15
+ * activity Your recent audits & findings
16
+ * search <query> Search packages in registry
12
17
  * model [name|reset] Configure LLM provider + model
13
- * setup Create account / enter API key
18
+ * setup Log in to agentaudit.dev (for report uploads)
14
19
  * status Show current config + auth status
15
20
  * help [command] Show help
16
21
  *
@@ -142,6 +147,7 @@ const xdgConfig = process.env.XDG_CONFIG_HOME || path.join(home, '.config');
142
147
  const USER_CRED_DIR = path.join(xdgConfig, 'agentaudit');
143
148
  const USER_CRED_FILE = path.join(USER_CRED_DIR, 'credentials.json');
144
149
  const SKILL_CRED_FILE = path.join(SKILL_DIR, 'config', 'credentials.json');
150
+ const PROFILE_CACHE_FILE = path.join(USER_CRED_DIR, 'profile-cache.json');
145
151
 
146
152
  function loadCredentials() {
147
153
  for (const f of [SKILL_CRED_FILE, USER_CRED_FILE]) {
@@ -224,6 +230,29 @@ function saveCredentials(data) {
224
230
  } catch {}
225
231
  }
226
232
 
233
+ function loadProfileCache() {
234
+ try {
235
+ if (!fs.existsSync(PROFILE_CACHE_FILE)) return null;
236
+ const data = JSON.parse(fs.readFileSync(PROFILE_CACHE_FILE, 'utf8'));
237
+ // TTL: 10 minutes
238
+ if (data.fetched_at && Date.now() - data.fetched_at < 10 * 60 * 1000) return data;
239
+ return null; // expired
240
+ } catch { return null; }
241
+ }
242
+
243
+ function saveProfileCache(data) {
244
+ try {
245
+ fs.mkdirSync(USER_CRED_DIR, { recursive: true });
246
+ fs.writeFileSync(PROFILE_CACHE_FILE, JSON.stringify({
247
+ agent_name: data.agent_name,
248
+ rank: data.rank,
249
+ total_points: data.total_points,
250
+ total_reports: data.total_reports,
251
+ fetched_at: Date.now(),
252
+ }, null, 2), { mode: 0o600 });
253
+ } catch {}
254
+ }
255
+
227
256
  function askQuestion(question) {
228
257
  const rl = createInterface({ input: process.stdin, output: process.stdout });
229
258
  return new Promise(resolve => rl.question(question, answer => { rl.close(); resolve(answer.trim()); }));
@@ -417,12 +446,13 @@ async function registerAgent(agentName) {
417
446
  }
418
447
 
419
448
  async function setupCommand() {
420
- console.log(` ${c.bold}Setup${c.reset}`);
449
+ console.log(` ${c.bold}AgentAudit Login${c.reset}`);
450
+ console.log(` ${c.dim}Create an account to upload audit reports to agentaudit.dev${c.reset}`);
421
451
  console.log();
422
452
 
423
453
  const existing = loadCredentials();
424
454
  if (existing) {
425
- console.log(` ${icons.safe} Already configured as ${c.bold}${existing.agent_name}${c.reset}`);
455
+ console.log(` ${icons.safe} Already logged in as ${c.bold}${existing.agent_name}${c.reset}`);
426
456
  console.log(` ${c.dim}Key: ${existing.api_key.slice(0, 8)}...${c.reset}`);
427
457
  console.log();
428
458
  const answer = await askQuestion(` Reconfigure? ${c.dim}(y/N)${c.reset} `);
@@ -523,8 +553,17 @@ function getVersion() {
523
553
  function banner() {
524
554
  if (quietMode || jsonMode) return;
525
555
  console.log();
526
- console.log(` ${c.bold}${c.cyan}AgentAudit${c.reset} ${c.dim}v${getVersion()}${c.reset}`);
527
- console.log(` ${c.dim}Security scanner for AI packages${c.reset}`);
556
+ const cache = loadProfileCache();
557
+ if (cache) {
558
+ const rankStr = cache.rank != null ? `#${cache.rank}` : '';
559
+ const ptsStr = `${fmtNum(cache.total_points)}pts`;
560
+ const auditsStr = `${fmtNum(cache.total_reports)} audits`;
561
+ const profile = [cache.agent_name, rankStr, ptsStr, auditsStr].filter(Boolean).join(' \u00b7 ');
562
+ console.log(` ${c.bold}${c.cyan}\u26e8 AgentAudit${c.reset} ${c.dim}v${getVersion()}${c.reset} ${c.dim}\u2502${c.reset} ${profile}`);
563
+ } else {
564
+ console.log(` ${c.bold}${c.cyan}AgentAudit${c.reset} ${c.dim}v${getVersion()}${c.reset}`);
565
+ console.log(` ${c.dim}Security scanner for AI packages${c.reset}`);
566
+ }
528
567
  console.log();
529
568
  }
530
569
 
@@ -567,6 +606,120 @@ function severityIcon(sev) {
567
606
  }
568
607
  }
569
608
 
609
+ // ── TUI Rendering Helpers ───────────────────────────────
610
+
611
+ const term = {
612
+ clearScreen: '\x1b[2J\x1b[H',
613
+ hideCursor: '\x1b[?25l',
614
+ showCursor: '\x1b[?25h',
615
+ altScreenOn: '\x1b[?1049h',
616
+ altScreenOff: '\x1b[?1049l',
617
+ moveTo: (r, col) => `\x1b[${r};${col}H`,
618
+ clearLine: '\x1b[2K',
619
+ underline: '\x1b[4m',
620
+ noUnderline: '\x1b[24m',
621
+ };
622
+
623
+ const BOX = { tl: '╭', tr: '╮', bl: '╰', br: '╯', h: '─', v: '│', lt: '├', rt: '┤', tt: '┬', bt: '┴', x: '┼' };
624
+
625
+ // Strip ANSI escape codes for length calculations
626
+ function stripAnsi(str) {
627
+ return str.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '');
628
+ }
629
+
630
+ function visLen(str) {
631
+ return stripAnsi(str).length;
632
+ }
633
+
634
+ function padRight(str, len) {
635
+ const diff = len - visLen(str);
636
+ return diff > 0 ? str + ' '.repeat(diff) : str;
637
+ }
638
+
639
+ function padLeft(str, len) {
640
+ const diff = len - visLen(str);
641
+ return diff > 0 ? ' '.repeat(diff) + str : str;
642
+ }
643
+
644
+ function drawBox(title, contentLines, width) {
645
+ const inner = width - 4; // 2 for "│ " + 2 for " │"
646
+ const lines = [];
647
+ const titleStr = title ? ` ${title} ` : '';
648
+ const titleLen = visLen(titleStr);
649
+ const topDash = BOX.h.repeat(Math.max(1, inner + 2 - titleLen));
650
+ lines.push(` ${BOX.tl}${c.dim}─${c.reset}${c.bold}${titleStr}${c.reset}${c.dim}${topDash}${c.reset}${BOX.tr}`);
651
+ for (const line of contentLines) {
652
+ lines.push(` ${BOX.v} ${padRight(line, inner + 1)}${BOX.v}`);
653
+ }
654
+ lines.push(` ${BOX.bl}${c.dim}${BOX.h.repeat(inner + 2)}${c.reset}${BOX.br}`);
655
+ return lines;
656
+ }
657
+
658
+ // ████████░░░░ proportional bar
659
+ function renderBar(value, maxValue, maxWidth) {
660
+ if (maxValue <= 0 || value <= 0) return c.dim + '░'.repeat(maxWidth) + c.reset;
661
+ const filled = Math.min(Math.round((value / maxValue) * maxWidth), maxWidth);
662
+ const empty = maxWidth - filled;
663
+ return c.cyan + '█'.repeat(filled) + c.dim + '░'.repeat(empty) + c.reset;
664
+ }
665
+
666
+ // [████████░░] 89%
667
+ function renderGauge(value, max, width) {
668
+ const pct = max > 0 ? Math.round((value / max) * 100) : 0;
669
+ const inner = width - 2; // subtract brackets
670
+ const filled = Math.min(Math.round((pct / 100) * inner), inner);
671
+ const empty = inner - filled;
672
+ const color = pct >= 80 ? c.green : pct >= 50 ? c.yellow : c.red;
673
+ return `[${color}${'█'.repeat(filled)}${c.dim}${'░'.repeat(empty)}${c.reset}] ${pct}%`;
674
+ }
675
+
676
+ // ●●●○○ colored severity dots
677
+ function severityDots(breakdown) {
678
+ const parts = [];
679
+ const critical = breakdown?.critical || 0;
680
+ const high = breakdown?.high || 0;
681
+ const medium = breakdown?.medium || 0;
682
+ const low = breakdown?.low || 0;
683
+ for (let i = 0; i < Math.min(critical, 3); i++) parts.push(`${c.red}●${c.reset}`);
684
+ for (let i = 0; i < Math.min(high, 3); i++) parts.push(`${c.red}●${c.reset}`);
685
+ for (let i = 0; i < Math.min(medium, 2); i++) parts.push(`${c.yellow}●${c.reset}`);
686
+ for (let i = 0; i < Math.min(low, 2); i++) parts.push(`${c.blue}●${c.reset}`);
687
+ // fill remaining with empty dots up to 5
688
+ while (parts.length < 5) parts.push(`${c.dim}○${c.reset}`);
689
+ return parts.slice(0, 5).join('');
690
+ }
691
+
692
+ // ▁▂▃▅▇█▆▃ mini sparkline
693
+ function sparkline(values) {
694
+ const chars = '▁▂▃▄▅▆▇█';
695
+ if (!values || values.length === 0) return '';
696
+ const max = Math.max(...values, 1);
697
+ return values.map(v => {
698
+ const idx = Math.min(Math.round((v / max) * (chars.length - 1)), chars.length - 1);
699
+ return chars[idx];
700
+ }).join('');
701
+ }
702
+
703
+ function fmtNum(n) {
704
+ if (n == null) return '0';
705
+ return n.toLocaleString('en-US');
706
+ }
707
+
708
+ function fmtPct(n) {
709
+ if (n == null) return '0%';
710
+ return Math.round(n) + '%';
711
+ }
712
+
713
+ function dashboardBanner() {
714
+ const ver = getVersion();
715
+ return [
716
+ ` ${BOX.tl}${c.dim}${BOX.h.repeat(35)}${c.reset}${BOX.tr}`,
717
+ ` ${BOX.v} ${c.bold}${c.cyan}⛨ AgentAudit${c.reset} ${c.dim}v${ver}${c.reset}${' '.repeat(Math.max(0, 19 - ver.length))}${BOX.v}`,
718
+ ` ${BOX.v} ${c.dim}Security Registry for AI Agents${c.reset} ${BOX.v}`,
719
+ ` ${BOX.bl}${c.dim}${BOX.h.repeat(35)}${c.reset}${BOX.br}`,
720
+ ];
721
+ }
722
+
570
723
  // ── File Collection (same logic as MCP server) ──────────
571
724
 
572
725
  function formatApiError(error, provider, statusCode) {
@@ -1940,6 +2093,835 @@ async function checkPackage(name) {
1940
2093
  return data;
1941
2094
  }
1942
2095
 
2096
+ // ── Dashboard / Leaderboard / Benchmark Commands ────────
2097
+
2098
+ async function fetchDashboardData() {
2099
+ const creds = loadCredentials();
2100
+ const fetches = [
2101
+ fetch(`${REGISTRY_URL}/api/stats`, { signal: AbortSignal.timeout(15_000) }).then(r => r.ok ? r.json() : null).catch(() => null),
2102
+ fetch(`${REGISTRY_URL}/api/leaderboard?limit=50`, { signal: AbortSignal.timeout(15_000) }).then(r => r.ok ? r.json() : null).catch(() => null),
2103
+ fetch(`${REGISTRY_URL}/api/benchmark`, { signal: AbortSignal.timeout(15_000) }).then(r => r.ok ? r.json() : null).catch(() => null),
2104
+ ];
2105
+ if (creds?.agent_name && creds.agent_name !== 'env') {
2106
+ fetches.push(
2107
+ fetch(`${REGISTRY_URL}/api/agents/${encodeURIComponent(creds.agent_name)}`, {
2108
+ headers: { 'Authorization': `Bearer ${creds.api_key}` },
2109
+ signal: AbortSignal.timeout(15_000),
2110
+ }).then(r => r.ok ? r.json() : null).catch(() => null)
2111
+ );
2112
+ } else {
2113
+ fetches.push(Promise.resolve(null));
2114
+ }
2115
+ const [stats, leaderboard, benchmark, agent] = await Promise.all(fetches);
2116
+ // Update profile cache if we have agent data
2117
+ if (agent && creds) {
2118
+ let rank = null;
2119
+ if (Array.isArray(leaderboard)) {
2120
+ const idx = leaderboard.findIndex(e => e.agent_name === creds.agent_name);
2121
+ if (idx >= 0) rank = idx + 1;
2122
+ }
2123
+ saveProfileCache({
2124
+ agent_name: creds.agent_name,
2125
+ rank,
2126
+ total_points: agent.total_points || 0,
2127
+ total_reports: agent.total_reports || 0,
2128
+ });
2129
+ }
2130
+ return { stats, leaderboard, benchmark, agent, creds };
2131
+ }
2132
+
2133
+ function renderOverviewTab(data, width) {
2134
+ const { stats, agent, leaderboard, creds } = data;
2135
+ const lines = [];
2136
+ const halfW = Math.min(Math.floor((width - 6) / 2), 40);
2137
+
2138
+ // Profile box
2139
+ const profileLines = [];
2140
+ if (agent && creds) {
2141
+ // Find rank
2142
+ let rank = '-';
2143
+ if (leaderboard && Array.isArray(leaderboard)) {
2144
+ const idx = leaderboard.findIndex(e => e.agent_name === creds.agent_name);
2145
+ if (idx >= 0) rank = `#${idx + 1} of ${leaderboard.length}`;
2146
+ }
2147
+ 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}`);
2148
+ profileLines.push(`Points ${c.bold}${fmtNum(agent.total_points)}${c.reset}`);
2149
+ profileLines.push(`Audits ${c.bold}${fmtNum(agent.total_reports)}${c.reset}`);
2150
+ profileLines.push(`Findings ${c.bold}${fmtNum(agent.total_findings_submitted)}${c.reset} ${c.dim}(${fmtNum(agent.total_findings_confirmed)} confirmed)${c.reset}`);
2151
+ const sev = agent.severity_breakdown || {};
2152
+ profileLines.push('');
2153
+ const sevParts = [];
2154
+ if (sev.critical) sevParts.push(`${c.red}${sev.critical} crit${c.reset}`);
2155
+ if (sev.high) sevParts.push(`${c.red}${sev.high} high${c.reset}`);
2156
+ if (sev.medium) sevParts.push(`${c.yellow}${sev.medium} med${c.reset}`);
2157
+ if (sev.low) sevParts.push(`${c.blue}${sev.low} low${c.reset}`);
2158
+ profileLines.push(sevParts.join(' ') || `${c.dim}no findings yet${c.reset}`);
2159
+ } else {
2160
+ profileLines.push(`${c.dim}Not logged in${c.reset}`);
2161
+ profileLines.push(`${c.dim}Run ${c.cyan}agentaudit setup${c.dim} to create account${c.reset}`);
2162
+ }
2163
+
2164
+ // Registry box
2165
+ const regLines = [];
2166
+ if (stats) {
2167
+ regLines.push(`Packages Audited ${c.bold}${fmtNum(stats.skills_audited)}${c.reset}`);
2168
+ regLines.push(`Total Findings ${c.bold}${fmtNum(stats.total_findings)}${c.reset}`);
2169
+ regLines.push(`Total Reports ${c.bold}${fmtNum(stats.total_reports)}${c.reset}`);
2170
+ regLines.push(`Contributors ${c.bold}${fmtNum(stats.reporters)}${c.reset}`);
2171
+ regLines.push(`Avg Trust Score ${c.bold}${stats.avg_trust_score || 0}${c.reset}`);
2172
+ regLines.push('');
2173
+ const parts = [];
2174
+ if (stats.safe_packages) parts.push(`${c.green}●${fmtNum(stats.safe_packages)} safe${c.reset}`);
2175
+ if (stats.caution_packages) parts.push(`${c.yellow}●${fmtNum(stats.caution_packages)} caution${c.reset}`);
2176
+ if (stats.unsafe_packages) parts.push(`${c.red}●${fmtNum(stats.unsafe_packages)} unsafe${c.reset}`);
2177
+ regLines.push(parts.join(' ') || `${c.dim}no packages yet${c.reset}`);
2178
+ } else {
2179
+ regLines.push(`${c.dim}Could not load registry stats${c.reset}`);
2180
+ }
2181
+
2182
+ const boxW = halfW + 4;
2183
+ const profileBox = drawBox('Your Profile', profileLines, boxW);
2184
+ const registryBox = drawBox('Registry', regLines, boxW);
2185
+
2186
+ // Side by side if wide enough, stacked otherwise
2187
+ if (width >= boxW * 2 + 4) {
2188
+ const maxLen = Math.max(profileBox.length, registryBox.length);
2189
+ while (profileBox.length < maxLen) profileBox.push(` ${BOX.v} ${' '.repeat(halfW + 1)}${BOX.v}`);
2190
+ while (registryBox.length < maxLen) registryBox.push(` ${BOX.v} ${' '.repeat(halfW + 1)}${BOX.v}`);
2191
+ for (let i = 0; i < maxLen; i++) {
2192
+ lines.push(profileBox[i] + ' ' + registryBox[i].trimStart());
2193
+ }
2194
+ } else {
2195
+ lines.push(...profileBox, '', ...registryBox);
2196
+ }
2197
+
2198
+ return lines;
2199
+ }
2200
+
2201
+ function renderLeaderboardTab(data, width, opts = {}) {
2202
+ const { leaderboard, creds } = data;
2203
+ const lines = [];
2204
+ const maxNameW = 20;
2205
+ const barW = Math.min(Math.max(10, width - 70), 30);
2206
+
2207
+ if (!leaderboard || !Array.isArray(leaderboard) || leaderboard.length === 0) {
2208
+ lines.push(` ${c.dim}No leaderboard data available${c.reset}`);
2209
+ return lines;
2210
+ }
2211
+
2212
+ const maxPts = leaderboard[0]?.total_points || 1;
2213
+ const medals = ['🥇', '🥈', '🥉'];
2214
+
2215
+ for (let i = 0; i < leaderboard.length; i++) {
2216
+ const entry = leaderboard[i];
2217
+ const name = (entry.agent_name || '').slice(0, maxNameW);
2218
+ const isMe = creds && entry.agent_name === creds.agent_name;
2219
+ const prefix = i < 3 ? ` ${medals[i]} ` : ` ${c.dim}#${String(i + 1).padStart(2)}${c.reset} `;
2220
+ const nameStr = isMe ? `${c.green}${c.bold}${name}${c.reset}` : name;
2221
+ const bar = renderBar(entry.total_points || 0, maxPts, barW);
2222
+ const pts = padLeft(`${fmtNum(entry.total_points || 0)} pts`, 12);
2223
+ const audits = padLeft(`${fmtNum(entry.total_reports || 0)} audits`, 12);
2224
+ const extra = entry.monthly_reports != null ? padLeft(`${fmtNum(entry.monthly_reports)} this mo`, 12) : '';
2225
+ lines.push(`${prefix}${padRight(nameStr, maxNameW)} ${bar} ${pts} ${audits}${extra}`);
2226
+
2227
+ // Separator after top 3
2228
+ if (i === 2 && leaderboard.length > 3) {
2229
+ lines.push(` ${c.dim}${'─'.repeat(Math.min(width - 4, 76))}${c.reset}`);
2230
+ }
2231
+ }
2232
+
2233
+ // Highlight current user if not in top list
2234
+ if (creds && !leaderboard.find(e => e.agent_name === creds.agent_name)) {
2235
+ lines.push('');
2236
+ lines.push(` ${c.dim}← you are not on this leaderboard yet${c.reset}`);
2237
+ }
2238
+
2239
+ return lines;
2240
+ }
2241
+
2242
+ function renderBenchmarkTab(data, width) {
2243
+ const { benchmark } = data;
2244
+ const lines = [];
2245
+
2246
+ if (!benchmark || !benchmark.models || benchmark.models.length === 0) {
2247
+ lines.push(` ${c.dim}No benchmark data available${c.reset}`);
2248
+ return lines;
2249
+ }
2250
+
2251
+ const overview = benchmark.overview || {};
2252
+ 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`);
2253
+ lines.push('');
2254
+
2255
+ // Header
2256
+ const nameW = 28;
2257
+ const hdr = ` ${padRight(`${c.bold}Model${c.reset}`, nameW + 9)} ${padRight('Audits', 7)} ${padRight('Risk', 5)} ${padRight('Detection', 16)} Severity`;
2258
+ lines.push(hdr);
2259
+ lines.push(` ${c.dim}${'─'.repeat(Math.min(width - 4, 86))}${c.reset}`);
2260
+
2261
+ for (const m of benchmark.models) {
2262
+ const name = (m.audit_model || 'unknown').slice(0, nameW - 2);
2263
+ const audits = padLeft(fmtNum(m.total_audits), 5);
2264
+ const riskVal = parseFloat(m.avg_risk_score) || 0;
2265
+ const riskColor = riskVal <= 20 ? c.green : riskVal <= 40 ? c.yellow : c.red;
2266
+ const risk = `${riskColor}${padLeft(String(Math.round(riskVal)), 3)}${c.reset}`;
2267
+ const detection = renderGauge(m.detection_rate || 0, 100, 10);
2268
+ // Severity as compact text instead of dots
2269
+ const sev = m.severity_breakdown || {};
2270
+ const sevParts = [];
2271
+ if (sev.critical) sevParts.push(`${c.red}${sev.critical}C${c.reset}`);
2272
+ if (sev.high) sevParts.push(`${c.red}${sev.high}H${c.reset}`);
2273
+ if (sev.medium) sevParts.push(`${c.yellow}${sev.medium}M${c.reset}`);
2274
+ if (sev.low) sevParts.push(`${c.blue}${sev.low}L${c.reset}`);
2275
+ const sevStr = sevParts.length > 0 ? sevParts.join(' ') : `${c.dim}—${c.reset}`;
2276
+ lines.push(` ${padRight(name, nameW)} ${audits} ${risk} ${detection} ${sevStr}`);
2277
+ }
2278
+
2279
+ // Vulnerability landscape
2280
+ const cats = benchmark.global_categories;
2281
+ if (cats && cats.length > 0) {
2282
+ lines.push('');
2283
+ const catTotal = cats.reduce((sum, cat) => sum + (cat.count || 0), 0) || 1;
2284
+ const catW = Math.min(width - 8, 56);
2285
+ const catLines = [];
2286
+ for (const cat of cats.slice(0, 6)) {
2287
+ const pct = Math.round((cat.count / catTotal) * 100);
2288
+ const barFill = Math.round((cat.count / catTotal) * (catW - 30));
2289
+ 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)}`);
2290
+ }
2291
+ lines.push(...drawBox('Vulnerability Landscape', catLines, catW + 4));
2292
+ }
2293
+
2294
+ // Cross-model findings
2295
+ const cross = benchmark.cross_model_findings;
2296
+ if (cross && cross.length > 0) {
2297
+ lines.push('');
2298
+ lines.push(` ${c.bold}Cross-Model Findings${c.reset} ${c.dim}(confirmed by multiple models)${c.reset}`);
2299
+ for (const cf of cross.slice(0, 5)) {
2300
+ const models = (cf.models || []).slice(0, 3).join(', ');
2301
+ lines.push(` ${c.dim}•${c.reset} ${cf.title || cf.pattern_id} ${c.dim}[${cf.model_count} models: ${models}]${c.reset}`);
2302
+ }
2303
+ }
2304
+
2305
+ return lines;
2306
+ }
2307
+
2308
+ function timeAgo(dateStr) {
2309
+ if (!dateStr) return '';
2310
+ const diff = Date.now() - new Date(dateStr).getTime();
2311
+ const mins = Math.floor(diff / 60000);
2312
+ if (mins < 60) return `${mins}m ago`;
2313
+ const hours = Math.floor(mins / 60);
2314
+ if (hours < 24) return `${hours}h ago`;
2315
+ const days = Math.floor(hours / 24);
2316
+ if (days < 30) return `${days}d ago`;
2317
+ return `${Math.floor(days / 30)}mo ago`;
2318
+ }
2319
+
2320
+ function renderActivityTab(data, width) {
2321
+ const { agent, creds } = data;
2322
+ const lines = [];
2323
+
2324
+ if (!creds || !agent) {
2325
+ lines.push(` ${c.dim}Not logged in${c.reset}`);
2326
+ lines.push(` ${c.dim}Run ${c.cyan}agentaudit setup${c.dim} to see your activity${c.reset}`);
2327
+ return lines;
2328
+ }
2329
+
2330
+ // Recent Audits
2331
+ const reports = agent.recent_reports || [];
2332
+ const auditLines = [];
2333
+ if (reports.length > 0) {
2334
+ for (const r of reports.slice(0, 10)) {
2335
+ const time = padRight(timeAgo(r.created_at), 8);
2336
+ const name = padRight((r.skill_slug || r.package_name || '').slice(0, 20), 20);
2337
+ const score = r.risk_score ?? r.latest_risk_score ?? 0;
2338
+ let status;
2339
+ if (score === 0) status = `${c.green}safe${c.reset} `;
2340
+ else if (score <= 30) status = `${c.yellow}caution${c.reset}`;
2341
+ else status = `${c.red}unsafe${c.reset} `;
2342
+ const findCount = r.finding_count != null ? `${r.finding_count} findings` : '';
2343
+ const model = r.audit_model ? `${c.dim}${(r.audit_model || '').slice(0, 14)}${c.reset}` : '';
2344
+ auditLines.push(`${c.dim}${time}${c.reset} ${name} ${status} ${padRight(findCount, 12)} ${model}`);
2345
+ }
2346
+ } else {
2347
+ auditLines.push(`${c.dim}No audits yet — run ${c.cyan}agentaudit audit <url>${c.dim} to get started${c.reset}`);
2348
+ }
2349
+ lines.push(...drawBox('Recent Audits', auditLines, Math.min(width - 4, 72)));
2350
+ lines.push('');
2351
+
2352
+ // Recent Findings
2353
+ const findings = agent.recent_findings || [];
2354
+ const findingLines = [];
2355
+ if (findings.length > 0) {
2356
+ for (const f of findings.slice(0, 10)) {
2357
+ const asfId = padRight(f.asf_id || f.id || '', 16);
2358
+ const sev = severityIcon(f.severity);
2359
+ const sevLabel = padRight(f.severity || '', 9);
2360
+ const title = (f.title || f.pattern_id || '').slice(0, 40);
2361
+ findingLines.push(`${asfId} ${sev} ${sevLabel} ${title}`);
2362
+ }
2363
+ } else {
2364
+ findingLines.push(`${c.dim}No findings yet${c.reset}`);
2365
+ }
2366
+ lines.push(...drawBox('Recent Findings', findingLines, Math.min(width - 4, 72)));
2367
+
2368
+ return lines;
2369
+ }
2370
+
2371
+ async function activityCommand(args) {
2372
+ const creds = loadCredentials();
2373
+
2374
+ if (!creds?.agent_name || creds.agent_name === 'env') {
2375
+ if (jsonMode) {
2376
+ console.log(JSON.stringify({ error: 'not_logged_in' }));
2377
+ } else {
2378
+ banner();
2379
+ console.log(` ${c.yellow}Not logged in${c.reset}`);
2380
+ console.log(` ${c.dim}Run ${c.cyan}agentaudit setup${c.dim} to create an account${c.reset}`);
2381
+ }
2382
+ return;
2383
+ }
2384
+
2385
+ if (!quietMode && !jsonMode) {
2386
+ banner();
2387
+ process.stdout.write(` ${c.dim}Loading activity...${c.reset}`);
2388
+ }
2389
+
2390
+ let agentData;
2391
+ try {
2392
+ const res = await fetch(`${REGISTRY_URL}/api/agents/${encodeURIComponent(creds.agent_name)}`, {
2393
+ headers: { 'Authorization': `Bearer ${creds.api_key}` },
2394
+ signal: AbortSignal.timeout(15_000),
2395
+ });
2396
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
2397
+ agentData = await res.json();
2398
+ } catch (err) {
2399
+ if (!jsonMode) {
2400
+ process.stdout.write('\r\x1b[2K');
2401
+ console.log(` ${c.red}Failed to load activity: ${err.message}${c.reset}`);
2402
+ }
2403
+ process.exitCode = 1;
2404
+ return;
2405
+ }
2406
+
2407
+ if (jsonMode) {
2408
+ console.log(JSON.stringify(agentData, null, 2));
2409
+ return;
2410
+ }
2411
+
2412
+ process.stdout.write('\r\x1b[2K');
2413
+
2414
+ // Update profile cache
2415
+ try {
2416
+ const lbRes = await fetch(`${REGISTRY_URL}/api/leaderboard?limit=100`, { signal: AbortSignal.timeout(10_000) }).then(r => r.ok ? r.json() : null);
2417
+ let rank = null;
2418
+ if (Array.isArray(lbRes)) {
2419
+ const idx = lbRes.findIndex(e => e.agent_name === creds.agent_name);
2420
+ if (idx >= 0) rank = idx + 1;
2421
+ }
2422
+ saveProfileCache({
2423
+ agent_name: creds.agent_name,
2424
+ rank,
2425
+ total_points: agentData.total_points || 0,
2426
+ total_reports: agentData.total_reports || 0,
2427
+ });
2428
+ } catch {}
2429
+
2430
+ const width = process.stdout.columns || 80;
2431
+ const activityLines = renderActivityTab({ agent: agentData, creds }, width);
2432
+ console.log(` ${c.bold}Activity${c.reset} ${c.dim}${creds.agent_name}${c.reset}`);
2433
+ console.log();
2434
+ for (const line of activityLines) console.log(line);
2435
+ console.log();
2436
+ }
2437
+
2438
+ function renderSearchResults(data, width) {
2439
+ const lines = [];
2440
+ const boxW = Math.min(width - 4, 72);
2441
+
2442
+ // Lookup API returns { reports: [], findings: [], total_matches }
2443
+ const lookupData = Array.isArray(data) ? data[0] : data;
2444
+ const reports = lookupData?.reports || [];
2445
+ const findings = lookupData?.findings || [];
2446
+ const total = lookupData?.total_matches || (reports.length + findings.length);
2447
+
2448
+ if (total === 0) return lines;
2449
+
2450
+ // Packages (from reports)
2451
+ if (reports.length > 0) {
2452
+ const pkgLines = [];
2453
+ for (const r of reports.slice(0, 10)) {
2454
+ const name = padRight((r.skill_slug || '').slice(0, 22), 22);
2455
+ const riskScore = r.risk_score ?? 0;
2456
+ let badge;
2457
+ if (riskScore === 0) badge = `${c.green}SAFE${c.reset} `;
2458
+ else if (riskScore <= 30) badge = `${c.yellow}CAUTION${c.reset}`;
2459
+ else badge = `${c.red}UNSAFE${c.reset} `;
2460
+ const time = padRight(timeAgo(r.created_at), 8);
2461
+ pkgLines.push(`${name} ${badge} ${c.dim}${time}${c.reset}`);
2462
+ }
2463
+ lines.push(...drawBox(`Packages (${reports.length})`, pkgLines, boxW));
2464
+ lines.push('');
2465
+ }
2466
+
2467
+ // Findings
2468
+ if (findings.length > 0) {
2469
+ const findLines = [];
2470
+ for (const f of findings.slice(0, 10)) {
2471
+ const asfId = padRight(f.asf_id || '', 16);
2472
+ const sev = severityIcon(f.severity);
2473
+ const sevLabel = padRight(f.severity || '', 9);
2474
+ const title = (f.title || '').slice(0, 36);
2475
+ findLines.push(`${asfId} ${sev} ${sevLabel} ${title}`);
2476
+ }
2477
+ lines.push(...drawBox(`Findings (${findings.length})`, findLines, boxW));
2478
+ }
2479
+
2480
+ return lines;
2481
+ }
2482
+
2483
+ function renderSearchTab(searchState, width) {
2484
+ const { query, results, loading, error } = searchState;
2485
+ const lines = [];
2486
+
2487
+ // Search input
2488
+ lines.push(` ${c.bold}Search:${c.reset} ${query || ''}${c.dim}\u2588${c.reset}`);
2489
+ lines.push('');
2490
+
2491
+ if (loading) {
2492
+ lines.push(` ${c.dim}Searching...${c.reset}`);
2493
+ return lines;
2494
+ }
2495
+
2496
+ if (error) {
2497
+ lines.push(` ${c.red}${error}${c.reset}`);
2498
+ return lines;
2499
+ }
2500
+
2501
+ if (results) {
2502
+ const resultLines = renderSearchResults(results, width);
2503
+ if (resultLines.length > 0) {
2504
+ lines.push(...resultLines);
2505
+ } else {
2506
+ lines.push(` ${c.dim}No results found for "${query}"${c.reset}`);
2507
+ }
2508
+ } else if (!query) {
2509
+ lines.push(` ${c.dim}Type to search packages in the registry${c.reset}`);
2510
+ }
2511
+
2512
+ lines.push('');
2513
+ lines.push(` ${c.dim}Type to search \u2502 Enter=search \u2502 Esc=clear \u2502 Tab=switch tab${c.reset}`);
2514
+
2515
+ return lines;
2516
+ }
2517
+
2518
+ async function searchCommand(args) {
2519
+ const query = args.filter(a => !a.startsWith('--')).join(' ').trim();
2520
+
2521
+ if (!query) {
2522
+ if (jsonMode) {
2523
+ console.log(JSON.stringify({ error: 'query_required' }));
2524
+ } else {
2525
+ banner();
2526
+ console.log(` ${c.red}Error: search query required${c.reset}`);
2527
+ console.log(` ${c.dim}Usage: ${c.cyan}agentaudit search <query>${c.dim} \u2014 e.g. agentaudit search fastmcp${c.reset}`);
2528
+ }
2529
+ process.exitCode = 2;
2530
+ return;
2531
+ }
2532
+
2533
+ if (!quietMode && !jsonMode) {
2534
+ banner();
2535
+ process.stdout.write(` ${c.dim}Searching "${query}"...${c.reset}`);
2536
+ }
2537
+
2538
+ let data;
2539
+ try {
2540
+ const res = await fetch(`${REGISTRY_URL}/api/lookup?hash=${encodeURIComponent(query)}`, {
2541
+ signal: AbortSignal.timeout(15_000),
2542
+ });
2543
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
2544
+ data = await res.json();
2545
+ } catch (err) {
2546
+ if (!jsonMode) {
2547
+ process.stdout.write('\r\x1b[2K');
2548
+ console.log(` ${c.red}Search failed: ${err.message}${c.reset}`);
2549
+ }
2550
+ process.exitCode = 1;
2551
+ return;
2552
+ }
2553
+
2554
+ if (jsonMode) {
2555
+ console.log(JSON.stringify(data, null, 2));
2556
+ return;
2557
+ }
2558
+
2559
+ process.stdout.write('\r\x1b[2K');
2560
+ console.log(` ${c.bold}Search${c.reset} ${c.dim}"${query}"${c.reset}`);
2561
+ console.log();
2562
+
2563
+ const width = process.stdout.columns || 80;
2564
+ const resultLines = renderSearchResults(data, width);
2565
+ if (resultLines.length === 0) {
2566
+ console.log(` ${c.dim}No results found${c.reset}`);
2567
+ console.log(` ${c.dim}Tip: try ${c.cyan}agentaudit scan <repo-url>${c.dim} to audit a new package${c.reset}`);
2568
+ console.log();
2569
+ return;
2570
+ }
2571
+
2572
+ for (const line of resultLines) console.log(line);
2573
+ console.log();
2574
+ }
2575
+
2576
+ async function leaderboardCommand(args) {
2577
+ const tabArg = args.find((a, i) => args[i - 1] === '--tab') || 'overall';
2578
+ const showAll = args.includes('--all');
2579
+ const limit = showAll ? 200 : 20;
2580
+
2581
+ if (!quietMode && !jsonMode) {
2582
+ banner();
2583
+ process.stdout.write(` ${c.dim}Loading leaderboard...${c.reset}`);
2584
+ }
2585
+
2586
+ let data;
2587
+ try {
2588
+ const url = `${REGISTRY_URL}/api/leaderboard?tab=${encodeURIComponent(tabArg)}&limit=${limit}`;
2589
+ const res = await fetch(url, { signal: AbortSignal.timeout(15_000) });
2590
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
2591
+ data = await res.json();
2592
+ } catch (err) {
2593
+ if (!jsonMode) {
2594
+ process.stdout.write('\r\x1b[2K');
2595
+ console.log(` ${c.red}Failed to load leaderboard: ${err.message}${c.reset}`);
2596
+ }
2597
+ process.exitCode = 1;
2598
+ return;
2599
+ }
2600
+
2601
+ if (jsonMode) {
2602
+ console.log(JSON.stringify(data, null, 2));
2603
+ return;
2604
+ }
2605
+
2606
+ process.stdout.write('\r\x1b[2K');
2607
+ const creds = loadCredentials();
2608
+ console.log(` ${c.bold}Leaderboard${c.reset} ${c.dim}${tabArg}${c.reset}`);
2609
+ console.log();
2610
+
2611
+ if (!Array.isArray(data) || data.length === 0) {
2612
+ console.log(` ${c.dim}No data available${c.reset}`);
2613
+ return;
2614
+ }
2615
+
2616
+ const maxPts = data[0]?.total_points || 1;
2617
+ const medals = ['🥇', '🥈', '🥉'];
2618
+ const barW = 24;
2619
+
2620
+ for (let i = 0; i < data.length; i++) {
2621
+ const entry = data[i];
2622
+ const name = (entry.agent_name || '').slice(0, 20);
2623
+ const isMe = creds && entry.agent_name === creds.agent_name;
2624
+ const prefix = i < 3 ? ` ${medals[i]} ` : ` #${String(i + 1).padStart(2)} `;
2625
+ const nameStr = isMe ? `${c.green}${c.bold}${name}${c.reset}` : name;
2626
+ const bar = renderBar(entry.total_points || 0, maxPts, barW);
2627
+ const pts = `${fmtNum(entry.total_points || 0)} pts`;
2628
+ const audits = `${fmtNum(entry.total_reports || 0)} audits`;
2629
+ console.log(`${prefix}${padRight(nameStr, 22)} ${bar} ${padLeft(pts, 12)} ${padLeft(audits, 10)}`);
2630
+ if (i === 2 && data.length > 3) {
2631
+ console.log(` ${c.dim}${'─'.repeat(74)}${c.reset}`);
2632
+ }
2633
+ }
2634
+ console.log();
2635
+ }
2636
+
2637
+ async function benchmarkCommand(args) {
2638
+ if (!quietMode && !jsonMode) {
2639
+ banner();
2640
+ process.stdout.write(` ${c.dim}Loading benchmark data...${c.reset}`);
2641
+ }
2642
+
2643
+ let data;
2644
+ try {
2645
+ const res = await fetch(`${REGISTRY_URL}/api/benchmark`, { signal: AbortSignal.timeout(15_000) });
2646
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
2647
+ data = await res.json();
2648
+ } catch (err) {
2649
+ if (!jsonMode) {
2650
+ process.stdout.write('\r\x1b[2K');
2651
+ console.log(` ${c.red}Failed to load benchmark: ${err.message}${c.reset}`);
2652
+ }
2653
+ process.exitCode = 1;
2654
+ return;
2655
+ }
2656
+
2657
+ if (jsonMode) {
2658
+ console.log(JSON.stringify(data, null, 2));
2659
+ return;
2660
+ }
2661
+
2662
+ process.stdout.write('\r\x1b[2K');
2663
+ const width = process.stdout.columns || 80;
2664
+ const benchLines = renderBenchmarkTab({ benchmark: data }, width);
2665
+ console.log(` ${c.bold}Model Benchmark${c.reset} ${c.dim}— LLM Audit Performance${c.reset}`);
2666
+ console.log();
2667
+ for (const line of benchLines) console.log(line);
2668
+ console.log();
2669
+ }
2670
+
2671
+ async function dashboardCommand() {
2672
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
2673
+ console.log(`${c.red}Error: dashboard requires an interactive terminal${c.reset}`);
2674
+ 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}`);
2675
+ process.exitCode = 1;
2676
+ return;
2677
+ }
2678
+
2679
+ const tabs = [
2680
+ { key: '1', label: 'Overview' },
2681
+ { key: '2', label: 'Leaderboard' },
2682
+ { key: '3', label: 'Benchmark' },
2683
+ { key: '4', label: 'Activity' },
2684
+ { key: '5', label: 'Search' },
2685
+ ];
2686
+ let activeTab = 0;
2687
+ let scrollOffset = 0;
2688
+ let running = true;
2689
+
2690
+ // Search tab state
2691
+ let searchQuery = '';
2692
+ let searchResults = null;
2693
+ let searchLoading = false;
2694
+ let searchError = null;
2695
+
2696
+ // Enter alt screen
2697
+ process.stdout.write(term.altScreenOn + term.hideCursor);
2698
+
2699
+ // Loading screen
2700
+ process.stdout.write(term.clearScreen);
2701
+ const bannerLines = dashboardBanner();
2702
+ for (const line of bannerLines) process.stdout.write(line + '\n');
2703
+ process.stdout.write(`\n ${c.dim}Loading data...${c.reset}\n`);
2704
+
2705
+ // Fetch data
2706
+ const data = await fetchDashboardData();
2707
+
2708
+ async function doSearch() {
2709
+ if (!searchQuery.trim()) return;
2710
+ searchLoading = true;
2711
+ searchError = null;
2712
+ render();
2713
+ try {
2714
+ const res = await fetch(`${REGISTRY_URL}/api/lookup?hash=${encodeURIComponent(searchQuery.trim())}`, {
2715
+ signal: AbortSignal.timeout(15_000),
2716
+ });
2717
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
2718
+ searchResults = await res.json();
2719
+ } catch (err) {
2720
+ searchError = `Search failed: ${err.message}`;
2721
+ searchResults = null;
2722
+ }
2723
+ searchLoading = false;
2724
+ if (running) render();
2725
+ }
2726
+
2727
+ function render() {
2728
+ const cols = process.stdout.columns || 80;
2729
+ const rows = process.stdout.rows || 24;
2730
+ let output = term.clearScreen;
2731
+
2732
+ // Banner
2733
+ const bLines = dashboardBanner();
2734
+ for (const line of bLines) output += line + '\n';
2735
+ output += '\n';
2736
+
2737
+ // Tab bar
2738
+ let tabBar = ' ';
2739
+ for (let i = 0; i < tabs.length; i++) {
2740
+ const tab = tabs[i];
2741
+ if (i === activeTab) {
2742
+ tabBar += `${c.bold}${c.cyan}${term.underline} [${tab.key}] ${tab.label} ${term.noUnderline}${c.reset}`;
2743
+ } else {
2744
+ tabBar += `${c.dim} [${tab.key}] ${tab.label} ${c.reset}`;
2745
+ }
2746
+ if (i < tabs.length - 1) tabBar += `${c.dim}\u2500${c.reset}`;
2747
+ }
2748
+ output += tabBar + '\n\n';
2749
+
2750
+ // Tab content
2751
+ let contentLines = [];
2752
+ switch (activeTab) {
2753
+ case 0:
2754
+ contentLines = renderOverviewTab(data, cols);
2755
+ break;
2756
+ case 1:
2757
+ contentLines = renderLeaderboardTab(data, cols);
2758
+ break;
2759
+ case 2:
2760
+ contentLines = renderBenchmarkTab(data, cols);
2761
+ break;
2762
+ case 3:
2763
+ contentLines = renderActivityTab(data, cols);
2764
+ break;
2765
+ case 4:
2766
+ contentLines = renderSearchTab({ query: searchQuery, results: searchResults, loading: searchLoading, error: searchError }, cols);
2767
+ break;
2768
+ }
2769
+
2770
+ // Apply scroll
2771
+ const availableRows = rows - 10; // header + tab bar + footer
2772
+ const maxScroll = Math.max(0, contentLines.length - availableRows);
2773
+ scrollOffset = Math.min(scrollOffset, maxScroll);
2774
+ scrollOffset = Math.max(0, scrollOffset);
2775
+
2776
+ const visible = contentLines.slice(scrollOffset, scrollOffset + availableRows);
2777
+ for (const line of visible) output += line + '\n';
2778
+
2779
+ // Footer
2780
+ output += '\n';
2781
+ const scrollInfo = contentLines.length > availableRows ? ` ${c.dim}[${scrollOffset + 1}-${Math.min(scrollOffset + availableRows, contentLines.length)}/${contentLines.length}]${c.reset}` : '';
2782
+ const isSearchTab = activeTab === 4;
2783
+ const footerHint = isSearchTab
2784
+ ? `${c.dim}\u2190\u2192 tab Enter=search Esc=clear q quit${c.reset}`
2785
+ : `${c.dim}\u2190\u2192 tab \u2191\u2193 scroll 1-5 jump q quit${c.reset}`;
2786
+ output += ` ${footerHint}${scrollInfo}\n`;
2787
+
2788
+ process.stdout.write(output);
2789
+ }
2790
+
2791
+ function cleanup() {
2792
+ if (!running) return;
2793
+ running = false;
2794
+ process.stdout.write(term.showCursor + term.altScreenOff);
2795
+ try { process.stdin.setRawMode(false); } catch {}
2796
+ process.stdin.pause();
2797
+ process.stdin.removeListener('data', onKeypress);
2798
+ process.stdout.removeListener('resize', render);
2799
+ }
2800
+
2801
+ function onKeypress(key) {
2802
+ if (!running) return;
2803
+ const isSearchTab = activeTab === 4;
2804
+
2805
+ // Ctrl+C always quits
2806
+ if (key === '\x03') {
2807
+ cleanup();
2808
+ return;
2809
+ }
2810
+
2811
+ // Search tab: route printable keys to search input
2812
+ if (isSearchTab) {
2813
+ // Escape: clear search
2814
+ if (key === '\x1b' && key.length === 1) {
2815
+ searchQuery = '';
2816
+ searchResults = null;
2817
+ searchError = null;
2818
+ scrollOffset = 0;
2819
+ render();
2820
+ return;
2821
+ }
2822
+
2823
+ // Enter: trigger search
2824
+ if (key === '\r' || key === '\n') {
2825
+ doSearch();
2826
+ return;
2827
+ }
2828
+
2829
+ // Backspace / Delete
2830
+ if (key === '\x7f' || key === '\b') {
2831
+ if (searchQuery.length > 0) {
2832
+ searchQuery = searchQuery.slice(0, -1);
2833
+ render();
2834
+ }
2835
+ return;
2836
+ }
2837
+
2838
+ // Tab: switch to next tab
2839
+ if (key === '\t') {
2840
+ activeTab = (activeTab + 1) % tabs.length;
2841
+ scrollOffset = 0;
2842
+ render();
2843
+ return;
2844
+ }
2845
+
2846
+ // Arrow keys still navigate
2847
+ if (key === '\x1b[C') { activeTab = (activeTab + 1) % tabs.length; scrollOffset = 0; render(); return; }
2848
+ if (key === '\x1b[D') { activeTab = (activeTab - 1 + tabs.length) % tabs.length; scrollOffset = 0; render(); return; }
2849
+ if (key === '\x1b[A') { scrollOffset = Math.max(0, scrollOffset - 1); render(); return; }
2850
+ if (key === '\x1b[B') { scrollOffset++; render(); return; }
2851
+
2852
+ // q quits only if search buffer is empty
2853
+ if (key === 'q' && searchQuery.length === 0) {
2854
+ cleanup();
2855
+ return;
2856
+ }
2857
+
2858
+ // Printable ASCII → append to search query
2859
+ if (key.length === 1 && key.charCodeAt(0) >= 32 && key.charCodeAt(0) <= 126) {
2860
+ searchQuery += key;
2861
+ render();
2862
+ return;
2863
+ }
2864
+ return;
2865
+ }
2866
+
2867
+ // Non-search tabs: normal keypress handling
2868
+ // q = quit
2869
+ if (key === 'q') {
2870
+ cleanup();
2871
+ return;
2872
+ }
2873
+
2874
+ // Tab navigation
2875
+ if (key === '\x1b[C' || key === '\t') {
2876
+ activeTab = (activeTab + 1) % tabs.length;
2877
+ scrollOffset = 0;
2878
+ render();
2879
+ return;
2880
+ }
2881
+ if (key === '\x1b[D') {
2882
+ activeTab = (activeTab - 1 + tabs.length) % tabs.length;
2883
+ scrollOffset = 0;
2884
+ render();
2885
+ return;
2886
+ }
2887
+
2888
+ // Number keys 1-5
2889
+ if (key >= '1' && key <= '5') {
2890
+ const idx = parseInt(key, 10) - 1;
2891
+ if (idx < tabs.length) { activeTab = idx; scrollOffset = 0; render(); }
2892
+ return;
2893
+ }
2894
+
2895
+ // Scroll
2896
+ if (key === '\x1b[A' || key === 'k') { scrollOffset = Math.max(0, scrollOffset - 1); render(); return; }
2897
+ if (key === '\x1b[B' || key === 'j') { scrollOffset++; render(); return; }
2898
+ if (key === '\x1b[5~') { scrollOffset = Math.max(0, scrollOffset - 10); render(); return; }
2899
+ if (key === '\x1b[6~') { scrollOffset += 10; render(); return; }
2900
+ }
2901
+
2902
+ // Setup input
2903
+ process.stdin.setRawMode(true);
2904
+ process.stdin.resume();
2905
+ process.stdin.setEncoding('utf8');
2906
+ process.stdin.on('data', onKeypress);
2907
+ process.stdout.on('resize', render);
2908
+
2909
+ // Graceful cleanup
2910
+ process.on('exit', cleanup);
2911
+ process.on('SIGINT', () => { cleanup(); process.exit(0); });
2912
+ process.on('SIGTERM', () => { cleanup(); process.exit(0); });
2913
+
2914
+ // Initial render
2915
+ render();
2916
+
2917
+ // Keep alive until user quits
2918
+ await new Promise(resolve => {
2919
+ const check = setInterval(() => {
2920
+ if (!running) { clearInterval(check); resolve(); }
2921
+ }, 100);
2922
+ });
2923
+ }
2924
+
1943
2925
  // ── Main ────────────────────────────────────────────────
1944
2926
 
1945
2927
  async function main() {
@@ -2054,9 +3036,10 @@ async function main() {
2054
3036
  setup: [
2055
3037
  `${c.bold}agentaudit setup${c.reset}`,
2056
3038
  ``,
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).`,
3039
+ `Log in to agentaudit.dev — create an account or enter an existing API key.`,
3040
+ `This enables uploading audit reports to the public registry.`,
3041
+ ``,
3042
+ `${c.bold}Note:${c.reset} This is NOT for LLM/provider configuration. Use ${c.cyan}agentaudit model${c.reset} for that.`,
2060
3043
  ``,
2061
3044
  `${c.bold}Examples:${c.reset}`,
2062
3045
  ` agentaudit setup`,
@@ -2073,15 +3056,107 @@ async function main() {
2073
3056
  ` agentaudit status`,
2074
3057
  ` agentaudit status --json`,
2075
3058
  ],
3059
+ dashboard: [
3060
+ `${c.bold}agentaudit dashboard${c.reset}`,
3061
+ ``,
3062
+ `Interactive full-screen dashboard with stats, leaderboard, and benchmark.`,
3063
+ `Uses alternate screen buffer — your scrollback stays intact.`,
3064
+ ``,
3065
+ `${c.bold}Navigation:${c.reset}`,
3066
+ ` \u2190\u2192 / Tab Switch between tabs`,
3067
+ ` 1-5 Jump to tab by number`,
3068
+ ` \u2191\u2193 / j/k Scroll content`,
3069
+ ` PgUp/PgDn Scroll fast`,
3070
+ ` q / Ctrl+C Quit`,
3071
+ ``,
3072
+ `${c.bold}Tabs:${c.reset}`,
3073
+ ` [1] Overview Your profile + registry stats`,
3074
+ ` [2] Leaderboard Top contributors ranking`,
3075
+ ` [3] Benchmark LLM model performance comparison`,
3076
+ ` [4] Activity Your recent audits & findings`,
3077
+ ` [5] Search Search packages (interactive input)`,
3078
+ ``,
3079
+ `${c.bold}Aliases:${c.reset} dashboard, dash`,
3080
+ ],
3081
+ dash: null, // alias → dashboard
3082
+ leaderboard: [
3083
+ `${c.bold}agentaudit leaderboard${c.reset} [options]`,
3084
+ ``,
3085
+ `Show top contributors ranking. Pipe-friendly output (no alt-screen).`,
3086
+ ``,
3087
+ `${c.bold}Options:${c.reset}`,
3088
+ ` --tab <name> Category: overall (default), monthly, reviewers, verifiers, streaks`,
3089
+ ` --all Show all entries (default: top 20)`,
3090
+ ` --json Machine-readable JSON output`,
3091
+ ``,
3092
+ `${c.bold}Aliases:${c.reset} leaderboard, lb`,
3093
+ ``,
3094
+ `${c.bold}Examples:${c.reset}`,
3095
+ ` agentaudit leaderboard`,
3096
+ ` agentaudit leaderboard --tab monthly`,
3097
+ ` agentaudit leaderboard --all --json`,
3098
+ ],
3099
+ lb: null, // alias → leaderboard
3100
+ benchmark: [
3101
+ `${c.bold}agentaudit benchmark${c.reset} [options]`,
3102
+ ``,
3103
+ `Compare LLM model performance across security audits.`,
3104
+ `Shows detection rates, severity breakdown, and vulnerability landscape.`,
3105
+ ``,
3106
+ `${c.bold}Options:${c.reset}`,
3107
+ ` --json Machine-readable JSON output`,
3108
+ ``,
3109
+ `${c.bold}Aliases:${c.reset} benchmark, bench`,
3110
+ ``,
3111
+ `${c.bold}Examples:${c.reset}`,
3112
+ ` agentaudit benchmark`,
3113
+ ` agentaudit benchmark --json`,
3114
+ ],
3115
+ bench: null, // alias → benchmark
3116
+ activity: [
3117
+ `${c.bold}agentaudit activity${c.reset} [options]`,
3118
+ ``,
3119
+ `Show your recent audits and findings from the AgentAudit registry.`,
3120
+ `Requires being logged in (run ${c.cyan}agentaudit setup${c.reset} first).`,
3121
+ ``,
3122
+ `${c.bold}Options:${c.reset}`,
3123
+ ` --json Machine-readable JSON output`,
3124
+ ``,
3125
+ `${c.bold}Aliases:${c.reset} activity, my`,
3126
+ ``,
3127
+ `${c.bold}Examples:${c.reset}`,
3128
+ ` agentaudit activity`,
3129
+ ` agentaudit activity --json`,
3130
+ ` agentaudit my`,
3131
+ ],
3132
+ my: null, // alias → activity
3133
+ search: [
3134
+ `${c.bold}agentaudit search${c.reset} <query> [options]`,
3135
+ ``,
3136
+ `Search for packages in the AgentAudit registry by name, ASF-ID, or hash.`,
3137
+ ``,
3138
+ `${c.bold}Options:${c.reset}`,
3139
+ ` --json Machine-readable JSON output`,
3140
+ ``,
3141
+ `${c.bold}Aliases:${c.reset} search, find`,
3142
+ ``,
3143
+ `${c.bold}Examples:${c.reset}`,
3144
+ ` agentaudit search fastmcp`,
3145
+ ` agentaudit search mcp-server`,
3146
+ ` agentaudit search ASF-2025-0001`,
3147
+ ` agentaudit search fastmcp --json`,
3148
+ ` agentaudit find fastmcp`,
3149
+ ],
3150
+ find: null, // alias → search
2076
3151
  };
2077
3152
 
2078
3153
  // Show subcommand help: `agentaudit help <cmd>` or `agentaudit <cmd> --help`
2079
3154
  function showSubcommandHelp(cmd) {
2080
3155
  let helpLines = subcommandHelp[cmd];
2081
- if (helpLines === null && subcommandHelp[cmd] === null) {
3156
+ if (helpLines === null) {
2082
3157
  // alias redirect
2083
- const alias = cmd === 'check' ? 'lookup' : cmd;
2084
- helpLines = subcommandHelp[alias];
3158
+ const aliases = { check: 'lookup', dash: 'dashboard', lb: 'leaderboard', bench: 'benchmark', my: 'activity', find: 'search' };
3159
+ helpLines = subcommandHelp[aliases[cmd] || cmd];
2085
3160
  }
2086
3161
  if (!helpLines) {
2087
3162
  console.log(` ${c.red}No help available for "${cmd}"${c.reset}`);
@@ -2124,9 +3199,16 @@ async function main() {
2124
3199
  console.log(` ${c.cyan}audit${c.reset} <url> [url...] Deep LLM-powered security audit (~30s)`);
2125
3200
  console.log(` ${c.cyan}lookup${c.reset} <name> Look up package in registry`);
2126
3201
  console.log();
3202
+ console.log(` ${c.bold}COMMUNITY${c.reset}`);
3203
+ console.log(` ${c.cyan}dashboard${c.reset} Interactive dashboard (full-screen)`);
3204
+ console.log(` ${c.cyan}leaderboard${c.reset} Top contributors ranking`);
3205
+ console.log(` ${c.cyan}benchmark${c.reset} LLM model performance comparison`);
3206
+ console.log(` ${c.cyan}activity${c.reset} Your recent audits & findings`);
3207
+ console.log(` ${c.cyan}search${c.reset} <query> Search packages in registry`);
3208
+ console.log();
2127
3209
  console.log(` ${c.bold}CONFIGURATION${c.reset}`);
2128
3210
  console.log(` ${c.cyan}model${c.reset} Configure LLM provider + model`);
2129
- console.log(` ${c.cyan}setup${c.reset} Create account / enter API key`);
3211
+ console.log(` ${c.cyan}setup${c.reset} Log in to agentaudit.dev (for report uploads)`);
2130
3212
  console.log(` ${c.cyan}status${c.reset} Show current config + auth status`);
2131
3213
  console.log();
2132
3214
  console.log(` ${c.bold}FLAGS${c.reset}`);
@@ -2154,9 +3236,31 @@ async function main() {
2154
3236
  // Default no-arg → discover
2155
3237
  const command = args.length === 0 ? 'discover' : args[0];
2156
3238
  const targets = args.slice(1);
2157
-
3239
+
3240
+ // Dashboard/Leaderboard/Benchmark — before banner() since dashboard uses alt-screen
3241
+ if (command === 'dashboard' || command === 'dash') {
3242
+ await dashboardCommand();
3243
+ return;
3244
+ }
3245
+ if (command === 'leaderboard' || command === 'lb') {
3246
+ await leaderboardCommand(targets);
3247
+ return;
3248
+ }
3249
+ if (command === 'benchmark' || command === 'bench') {
3250
+ await benchmarkCommand(targets);
3251
+ return;
3252
+ }
3253
+ if (command === 'activity' || command === 'my') {
3254
+ await activityCommand(targets);
3255
+ return;
3256
+ }
3257
+ if (command === 'search' || command === 'find') {
3258
+ await searchCommand(targets);
3259
+ return;
3260
+ }
3261
+
2158
3262
  banner();
2159
-
3263
+
2160
3264
  if (command === 'setup') {
2161
3265
  await setupCommand();
2162
3266
  return;
@@ -2218,6 +3322,39 @@ async function main() {
2218
3322
  }
2219
3323
  console.log();
2220
3324
 
3325
+ // Personal stats (from registry, silently skip on error)
3326
+ if (creds?.agent_name && creds.agent_name !== 'env') {
3327
+ try {
3328
+ const [agentRes, lbRes] = await Promise.all([
3329
+ fetch(`${REGISTRY_URL}/api/agents/${encodeURIComponent(creds.agent_name)}`, {
3330
+ headers: { 'Authorization': `Bearer ${creds.api_key}` },
3331
+ signal: AbortSignal.timeout(10_000),
3332
+ }).then(r => r.ok ? r.json() : null),
3333
+ fetch(`${REGISTRY_URL}/api/leaderboard?limit=100`, { signal: AbortSignal.timeout(10_000) }).then(r => r.ok ? r.json() : null),
3334
+ ]);
3335
+ if (agentRes) {
3336
+ let rank = null;
3337
+ if (Array.isArray(lbRes)) {
3338
+ const idx = lbRes.findIndex(e => e.agent_name === creds.agent_name);
3339
+ if (idx >= 0) rank = idx + 1;
3340
+ }
3341
+ // Update profile cache
3342
+ saveProfileCache({
3343
+ agent_name: creds.agent_name,
3344
+ rank,
3345
+ total_points: agentRes.total_points || 0,
3346
+ total_reports: agentRes.total_reports || 0,
3347
+ });
3348
+ console.log(` ${c.bold}Profile${c.reset}`);
3349
+ console.log(` Rank ${c.bold}${rank ? `#${rank} of ${lbRes.length}` : '-'}${c.reset}`);
3350
+ console.log(` Points ${c.bold}${fmtNum(agentRes.total_points || 0)}${c.reset}`);
3351
+ console.log(` Audits ${c.bold}${fmtNum(agentRes.total_reports || 0)}${c.reset}`);
3352
+ console.log(` Findings ${c.bold}${fmtNum(agentRes.total_findings_submitted || 0)}${c.reset} ${c.dim}(${fmtNum(agentRes.total_findings_confirmed || 0)} confirmed)${c.reset}`);
3353
+ console.log();
3354
+ }
3355
+ } catch {}
3356
+ }
3357
+
2221
3358
  // JSON mode
2222
3359
  if (jsonMode) {
2223
3360
  const status = {