agentaudit 3.10.3 → 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 +513 -32
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -30,6 +30,7 @@ and supply chain attacks. Powered by regex static analysis and deep LLM audits.
30
30
  - [What It Detects](#-what-it-detects)
31
31
  - [How the 3-Pass Audit Works](#-how-the-3-pass-audit-works)
32
32
  - [CI/CD Integration](#-cicd-integration)
33
+ - [Dashboard & Community](#-dashboard--community)
33
34
  - [Configuration](#-configuration)
34
35
  - [Requirements](#-requirements)
35
36
  - [FAQ](#-faq)
@@ -76,8 +77,7 @@ agentaudit lookup fastmcp
76
77
 
77
78
  **Example output:**
78
79
  ```
79
- AgentAudit v3.9.8
80
- Security scanner for AI packages
80
+ AgentAudit v3.10.4 │ my-scanner #3 · 280pts · 19 audits
81
81
 
82
82
  Discovering MCP servers in your AI editors...
83
83
 
@@ -93,6 +93,8 @@ agentaudit lookup fastmcp
93
93
  Looking for general package scanning? Try `pip audit` or `npm audit`.
94
94
  ```
95
95
 
96
+ > **Enhanced banner:** When logged in, the banner shows your agent name, rank, points, and audit count. Run `agentaudit setup` to create an account.
97
+
96
98
  ### Option B: MCP Server in your AI editor
97
99
 
98
100
  Add AgentAudit as an MCP server — your AI agent can then discover, scan, and audit packages using its own LLM. **No extra API key needed.**
@@ -197,6 +199,8 @@ Then ask your agent: *"Check which MCP servers I have installed and audit any un
197
199
 
198
200
  ## 📋 Commands Reference
199
201
 
202
+ ### Scan & Audit
203
+
200
204
  | Command | Description | Example |
201
205
  |---------|-------------|---------|
202
206
  | `agentaudit` | Discover MCP servers (default, same as `discover`) | `agentaudit` |
@@ -207,18 +211,36 @@ Then ask your agent: *"Check which MCP servers I have installed and audit any un
207
211
  | `agentaudit scan <url> --deep` | Deep audit (same as `audit`) | `agentaudit scan https://github.com/owner/repo --deep` |
208
212
  | `agentaudit audit <url>` | Deep LLM-powered 3-pass audit (~30s) | `agentaudit audit https://github.com/owner/repo` |
209
213
  | `agentaudit lookup <name>` | Look up package in trust registry | `agentaudit lookup fastmcp` |
210
- | `agentaudit check <name\|url>` | Lookup + auto-audit if not found | `agentaudit check https://github.com/owner/repo` |
211
- | `agentaudit status` | Check API keys + active LLM provider | `agentaudit status` |
212
- | `agentaudit setup` | Register agent + configure API key | `agentaudit setup` |
214
+
215
+ ### Community
216
+
217
+ | Command | Alias | Description |
218
+ |---------|-------|-------------|
219
+ | `agentaudit dashboard` | `dash` | Interactive full-screen TUI with 5 tabs (Overview, Leaderboard, Benchmark, Activity, Search) |
220
+ | `agentaudit leaderboard` | `lb` | Top contributors ranking (pipe-friendly) |
221
+ | `agentaudit benchmark` | `bench` | LLM model audit performance comparison |
222
+ | `agentaudit activity` | `my` | Your recent audits & findings |
223
+ | `agentaudit search <query>` | `find` | Search packages in the registry by name, ASF-ID, or hash |
224
+
225
+ ### Configuration
226
+
227
+ | Command | Alias | Description |
228
+ |---------|-------|-------------|
229
+ | `agentaudit model` | — | Interactive LLM provider + model configuration |
230
+ | `agentaudit setup` | — | Register agent + configure API key for registry uploads |
231
+ | `agentaudit status` | `whoami` | Show current config, API keys, and personal stats |
213
232
 
214
233
  ### Global Flags
215
234
 
216
235
  | Flag | Description |
217
236
  |------|-------------|
218
237
  | `--json` | Output machine-readable JSON to stdout |
219
- | `--quiet` / `-q` | Suppress banner and decorative output (show findings only) |
238
+ | `--quiet` / `-q` | Suppress banner and decorative output |
220
239
  | `--no-color` | Disable ANSI colors (also respects `NO_COLOR` env var) |
221
- | `--provider <name>` | Force LLM provider (`anthropic`, `openai`, `openrouter`, `ollama`, `custom`) |
240
+ | `--model <name>` | Override LLM model for this run |
241
+ | `--no-upload` | Skip uploading report to registry |
242
+ | `--export` | Export audit payload as markdown |
243
+ | `--debug` | Show raw LLM response on parse errors |
222
244
  | `--help` / `-h` | Show help text |
223
245
  | `-v` / `--version` | Show version |
224
246
 
@@ -418,6 +440,41 @@ agentaudit lookup fastmcp --json
418
440
 
419
441
  ---
420
442
 
443
+ ## 📊 Dashboard & Community
444
+
445
+ AgentAudit includes a full-screen interactive dashboard and standalone community commands.
446
+
447
+ ### Interactive Dashboard
448
+
449
+ ```bash
450
+ agentaudit dashboard # or: agentaudit dash
451
+ ```
452
+
453
+ 5-tab TUI with keyboard navigation (←→ tabs, ↑↓ scroll, 1-5 jump, q quit):
454
+
455
+ | Tab | Content |
456
+ |-----|---------|
457
+ | **[1] Overview** | Your profile (rank, points, audits, severity breakdown) + registry stats |
458
+ | **[2] Leaderboard** | Top contributors with medal rankings and bar charts |
459
+ | **[3] Benchmark** | LLM model audit performance comparison |
460
+ | **[4] Activity** | Your recent audits and findings |
461
+ | **[5] Search** | Interactive package search (type to search, Enter to submit) |
462
+
463
+ ### Standalone Commands
464
+
465
+ All community commands work without the dashboard (pipe-friendly, supports `--json`):
466
+
467
+ ```bash
468
+ agentaudit leaderboard # Top contributors
469
+ agentaudit leaderboard --tab monthly --json # Monthly rankings as JSON
470
+ agentaudit benchmark # Model comparison
471
+ agentaudit activity # Your recent audits & findings
472
+ agentaudit search fastmcp # Search registry by name/ASF-ID
473
+ agentaudit search fastmcp --json # Machine-readable search results
474
+ ```
475
+
476
+ ---
477
+
421
478
  ## ⚙️ Configuration
422
479
 
423
480
  ### Credentials
@@ -430,23 +487,34 @@ Run `agentaudit setup` to configure interactively, or set via environment:
430
487
  export AGENTAUDIT_API_KEY=asf_your_key_here
431
488
  ```
432
489
 
433
- ### Environment Variables
490
+ ### LLM Providers (13 supported)
491
+
492
+ AgentAudit supports 13 LLM providers for deep audits. Set one API key — the CLI auto-detects it. Use `agentaudit model` to choose provider + model interactively, or `agentaudit status` to check your setup.
493
+
494
+ | Variable | Provider | Default Model |
495
+ |----------|----------|---------------|
496
+ | `ANTHROPIC_API_KEY` | Anthropic (Claude) | `claude-sonnet-4-20250514` |
497
+ | `GEMINI_API_KEY` | Google (Gemini) | `gemini-2.5-flash` |
498
+ | `OPENAI_API_KEY` | OpenAI (GPT-4o) | `gpt-4o` |
499
+ | `DEEPSEEK_API_KEY` | DeepSeek | `deepseek-chat` |
500
+ | `MISTRAL_API_KEY` | Mistral | `mistral-large-latest` |
501
+ | `GROQ_API_KEY` | Groq | `llama-3.3-70b-versatile` |
502
+ | `XAI_API_KEY` | xAI (Grok) | `grok-3` |
503
+ | `TOGETHER_API_KEY` | Together AI | `Llama-3.3-70B-Instruct-Turbo` |
504
+ | `FIREWORKS_API_KEY` | Fireworks AI | `llama-v3p3-70b-instruct` |
505
+ | `CEREBRAS_API_KEY` | Cerebras | `llama-3.3-70b` |
506
+ | `ZAI_API_KEY` | Zhipu AI (GLM) | `glm-4.7` |
507
+ | `OPENROUTER_API_KEY` | OpenRouter | `anthropic/claude-sonnet-4` |
508
+
509
+ ### Other Environment Variables
434
510
 
435
511
  | Variable | Description |
436
512
  |----------|-------------|
437
- | `AGENTAUDIT_API_KEY` | API key for registry access |
438
- | `ANTHROPIC_API_KEY` | Anthropic API key for deep audits (Claude) -- recommended |
439
- | `OPENAI_API_KEY` | OpenAI API key for deep audits (GPT-4o) |
440
- | `OPENROUTER_API_KEY` | OpenRouter API key (access 200+ models) |
441
- | `OPENROUTER_MODEL` | Model to use via OpenRouter (default: `anthropic/claude-sonnet-4`) |
442
- | `OLLAMA_MODEL` | Ollama model name for local audits (e.g. `llama3.1`, `qwen2.5-coder`) |
443
- | `OLLAMA_HOST` | Ollama server URL (default: `http://localhost:11434`) |
444
- | `LLM_API_URL` | Any OpenAI-compatible API endpoint (e.g. LM Studio, vLLM, Together, Groq) |
445
- | `LLM_API_KEY` | API key for custom endpoint (optional if no auth needed) |
446
- | `LLM_MODEL` | Model name for custom endpoint |
513
+ | `AGENTAUDIT_API_KEY` | API key for registry uploads (or use `agentaudit setup`) |
514
+ | `AGENTAUDIT_MODEL` | Override LLM model (same as `--model` flag) |
447
515
  | `NO_COLOR` | Disable ANSI colors ([no-color.org](https://no-color.org)) |
448
516
 
449
- > **Provider priority:** Anthropic > OpenAI > OpenRouter > Custom > Ollama. Override with `--provider=ollama` etc.
517
+ > **Provider priority:** Set `preferred_provider` via `agentaudit model`, or the CLI picks the first available key. Override per-run with `--model <name>`.
450
518
 
451
519
  ---
452
520
 
@@ -477,49 +545,26 @@ Or use without installing: `npx agentaudit`
477
545
 
478
546
  ### Setting up your LLM key for deep audits
479
547
 
480
- The `audit` command supports **any LLM provider**. Set one of these environment variables:
481
-
482
- ```bash
483
- # Linux / macOS
484
- export ANTHROPIC_API_KEY=sk-ant-... # Recommended (Claude Sonnet)
485
- export OPENAI_API_KEY=sk-... # Alternative (GPT-4o)
486
- export OPENROUTER_API_KEY=sk-or-... # 200+ models via OpenRouter
487
-
488
- # Windows (PowerShell)
489
- $env:ANTHROPIC_API_KEY = "sk-ant-..."
490
- $env:OPENAI_API_KEY = "sk-..."
491
- $env:OPENROUTER_API_KEY = "sk-or-..."
492
-
493
- # Windows (CMD)
494
- set ANTHROPIC_API_KEY=sk-ant-...
495
- set OPENAI_API_KEY=sk-...
496
- set OPENROUTER_API_KEY=sk-or-...
497
- ```
498
-
499
- **Provider priority:** Anthropic > OpenAI > OpenRouter > Custom > Ollama. Override with `--provider=<name>`.
500
-
501
- **OpenRouter model selection:** By default uses `anthropic/claude-sonnet-4`. Override with:
502
- ```bash
503
- export OPENROUTER_MODEL=google/gemini-2.5-pro # or any model on openrouter.ai
504
- ```
548
+ The `audit` command supports **13 LLM providers**. Set one API key and AgentAudit auto-detects it:
505
549
 
506
- **Local with Ollama (free, no API key):**
507
550
  ```bash
508
- export OLLAMA_MODEL=llama3.1 # or qwen2.5-coder, deepseek-r1, etc.
509
- agentaudit audit https://github.com/owner/repo
551
+ # Set any one of these (Anthropic recommended)
552
+ export ANTHROPIC_API_KEY=sk-ant-...
553
+ export OPENAI_API_KEY=sk-...
554
+ export GEMINI_API_KEY=...
555
+ export DEEPSEEK_API_KEY=...
556
+ # ... or any of the 13 supported providers (see Configuration section)
510
557
  ```
511
- > Note: Local models produce lower quality audits than Claude/GPT-4o. Use for quick checks, not production security audits.
512
558
 
513
- **Any OpenAI-compatible API:**
559
+ **Interactive setup:**
514
560
  ```bash
515
- export LLM_API_URL=http://localhost:1234/v1 # LM Studio, vLLM, etc.
516
- export LLM_MODEL=my-model
517
- agentaudit audit https://github.com/owner/repo
561
+ agentaudit model # 2-step menu: pick provider → pick model
562
+ agentaudit status # check which keys are set + current config
518
563
  ```
519
564
 
520
- **Check your setup:**
565
+ **Override per-run:**
521
566
  ```bash
522
- agentaudit status # validates all configured API keys
567
+ agentaudit audit https://github.com/owner/repo --model gpt-4o
523
568
  ```
524
569
 
525
570
  **Troubleshooting:** If you see `API error: Incorrect API key`, double-check your key is valid and has credits. Use `--debug` to see the full API response.
package/cli.mjs CHANGED
@@ -12,6 +12,8 @@
12
12
  * dashboard Interactive full-screen dashboard
13
13
  * leaderboard Top contributors ranking
14
14
  * benchmark LLM model performance comparison
15
+ * activity Your recent audits & findings
16
+ * search <query> Search packages in registry
15
17
  * model [name|reset] Configure LLM provider + model
16
18
  * setup Log in to agentaudit.dev (for report uploads)
17
19
  * status Show current config + auth status
@@ -145,6 +147,7 @@ const xdgConfig = process.env.XDG_CONFIG_HOME || path.join(home, '.config');
145
147
  const USER_CRED_DIR = path.join(xdgConfig, 'agentaudit');
146
148
  const USER_CRED_FILE = path.join(USER_CRED_DIR, 'credentials.json');
147
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');
148
151
 
149
152
  function loadCredentials() {
150
153
  for (const f of [SKILL_CRED_FILE, USER_CRED_FILE]) {
@@ -227,6 +230,29 @@ function saveCredentials(data) {
227
230
  } catch {}
228
231
  }
229
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
+
230
256
  function askQuestion(question) {
231
257
  const rl = createInterface({ input: process.stdin, output: process.stdout });
232
258
  return new Promise(resolve => rl.question(question, answer => { rl.close(); resolve(answer.trim()); }));
@@ -527,8 +553,17 @@ function getVersion() {
527
553
  function banner() {
528
554
  if (quietMode || jsonMode) return;
529
555
  console.log();
530
- console.log(` ${c.bold}${c.cyan}AgentAudit${c.reset} ${c.dim}v${getVersion()}${c.reset}`);
531
- 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
+ }
532
567
  console.log();
533
568
  }
534
569
 
@@ -2078,6 +2113,20 @@ async function fetchDashboardData() {
2078
2113
  fetches.push(Promise.resolve(null));
2079
2114
  }
2080
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
+ }
2081
2130
  return { stats, leaderboard, benchmark, agent, creds };
2082
2131
  }
2083
2132
 
@@ -2173,7 +2222,7 @@ function renderLeaderboardTab(data, width, opts = {}) {
2173
2222
  const pts = padLeft(`${fmtNum(entry.total_points || 0)} pts`, 12);
2174
2223
  const audits = padLeft(`${fmtNum(entry.total_reports || 0)} audits`, 12);
2175
2224
  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}`);
2225
+ lines.push(`${prefix}${padRight(nameStr, maxNameW)} ${bar} ${pts} ${audits}${extra}`);
2177
2226
 
2178
2227
  // Separator after top 3
2179
2228
  if (i === 2 && leaderboard.length > 3) {
@@ -2204,20 +2253,27 @@ function renderBenchmarkTab(data, width) {
2204
2253
  lines.push('');
2205
2254
 
2206
2255
  // 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`;
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`;
2209
2258
  lines.push(hdr);
2210
- lines.push(` ${c.dim}${'─'.repeat(Math.min(width - 4, 80))}${c.reset}`);
2259
+ lines.push(` ${c.dim}${'─'.repeat(Math.min(width - 4, 86))}${c.reset}`);
2211
2260
 
2212
2261
  for (const m of benchmark.models) {
2213
2262
  const name = (m.audit_model || 'unknown').slice(0, nameW - 2);
2214
- const audits = padLeft(fmtNum(m.total_audits), 6);
2263
+ const audits = padLeft(fmtNum(m.total_audits), 5);
2215
2264
  const riskVal = parseFloat(m.avg_risk_score) || 0;
2216
2265
  const riskColor = riskVal <= 20 ? c.green : riskVal <= 40 ? c.yellow : c.red;
2217
2266
  const risk = `${riskColor}${padLeft(String(Math.round(riskVal)), 3)}${c.reset}`;
2218
2267
  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}`);
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}`);
2221
2277
  }
2222
2278
 
2223
2279
  // Vulnerability landscape
@@ -2249,6 +2305,274 @@ function renderBenchmarkTab(data, width) {
2249
2305
  return lines;
2250
2306
  }
2251
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
+
2252
2576
  async function leaderboardCommand(args) {
2253
2577
  const tabArg = args.find((a, i) => args[i - 1] === '--tab') || 'overall';
2254
2578
  const showAll = args.includes('--all');
@@ -2302,7 +2626,7 @@ async function leaderboardCommand(args) {
2302
2626
  const bar = renderBar(entry.total_points || 0, maxPts, barW);
2303
2627
  const pts = `${fmtNum(entry.total_points || 0)} pts`;
2304
2628
  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)}`);
2629
+ console.log(`${prefix}${padRight(nameStr, 22)} ${bar} ${padLeft(pts, 12)} ${padLeft(audits, 10)}`);
2306
2630
  if (i === 2 && data.length > 3) {
2307
2631
  console.log(` ${c.dim}${'─'.repeat(74)}${c.reset}`);
2308
2632
  }
@@ -2356,11 +2680,19 @@ async function dashboardCommand() {
2356
2680
  { key: '1', label: 'Overview' },
2357
2681
  { key: '2', label: 'Leaderboard' },
2358
2682
  { key: '3', label: 'Benchmark' },
2683
+ { key: '4', label: 'Activity' },
2684
+ { key: '5', label: 'Search' },
2359
2685
  ];
2360
2686
  let activeTab = 0;
2361
2687
  let scrollOffset = 0;
2362
2688
  let running = true;
2363
2689
 
2690
+ // Search tab state
2691
+ let searchQuery = '';
2692
+ let searchResults = null;
2693
+ let searchLoading = false;
2694
+ let searchError = null;
2695
+
2364
2696
  // Enter alt screen
2365
2697
  process.stdout.write(term.altScreenOn + term.hideCursor);
2366
2698
 
@@ -2373,6 +2705,25 @@ async function dashboardCommand() {
2373
2705
  // Fetch data
2374
2706
  const data = await fetchDashboardData();
2375
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
+
2376
2727
  function render() {
2377
2728
  const cols = process.stdout.columns || 80;
2378
2729
  const rows = process.stdout.rows || 24;
@@ -2392,7 +2743,7 @@ async function dashboardCommand() {
2392
2743
  } else {
2393
2744
  tabBar += `${c.dim} [${tab.key}] ${tab.label} ${c.reset}`;
2394
2745
  }
2395
- if (i < tabs.length - 1) tabBar += `${c.dim}──${c.reset}`;
2746
+ if (i < tabs.length - 1) tabBar += `${c.dim}\u2500${c.reset}`;
2396
2747
  }
2397
2748
  output += tabBar + '\n\n';
2398
2749
 
@@ -2408,6 +2759,12 @@ async function dashboardCommand() {
2408
2759
  case 2:
2409
2760
  contentLines = renderBenchmarkTab(data, cols);
2410
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;
2411
2768
  }
2412
2769
 
2413
2770
  // Apply scroll
@@ -2422,7 +2779,11 @@ async function dashboardCommand() {
2422
2779
  // Footer
2423
2780
  output += '\n';
2424
2781
  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`;
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`;
2426
2787
 
2427
2788
  process.stdout.write(output);
2428
2789
  }
@@ -2439,37 +2800,103 @@ async function dashboardCommand() {
2439
2800
 
2440
2801
  function onKeypress(key) {
2441
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
+ }
2442
2866
 
2443
- // q or Ctrl+C = quit
2444
- if (key === 'q' || key === '\x03') {
2867
+ // Non-search tabs: normal keypress handling
2868
+ // q = quit
2869
+ if (key === 'q') {
2445
2870
  cleanup();
2446
2871
  return;
2447
2872
  }
2448
2873
 
2449
2874
  // Tab navigation
2450
- if (key === '\x1b[C' || key === '\t') { // Right arrow or Tab
2875
+ if (key === '\x1b[C' || key === '\t') {
2451
2876
  activeTab = (activeTab + 1) % tabs.length;
2452
2877
  scrollOffset = 0;
2453
2878
  render();
2454
2879
  return;
2455
2880
  }
2456
- if (key === '\x1b[D') { // Left arrow
2881
+ if (key === '\x1b[D') {
2457
2882
  activeTab = (activeTab - 1 + tabs.length) % tabs.length;
2458
2883
  scrollOffset = 0;
2459
2884
  render();
2460
2885
  return;
2461
2886
  }
2462
2887
 
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; }
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
+ }
2467
2894
 
2468
2895
  // 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
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; }
2473
2900
  }
2474
2901
 
2475
2902
  // Setup input
@@ -2636,9 +3063,9 @@ async function main() {
2636
3063
  `Uses alternate screen buffer — your scrollback stays intact.`,
2637
3064
  ``,
2638
3065
  `${c.bold}Navigation:${c.reset}`,
2639
- ` ←→ / Tab Switch between tabs`,
2640
- ` 1-3 Jump to tab by number`,
2641
- ` ↑↓ / j/k Scroll content`,
3066
+ ` \u2190\u2192 / Tab Switch between tabs`,
3067
+ ` 1-5 Jump to tab by number`,
3068
+ ` \u2191\u2193 / j/k Scroll content`,
2642
3069
  ` PgUp/PgDn Scroll fast`,
2643
3070
  ` q / Ctrl+C Quit`,
2644
3071
  ``,
@@ -2646,6 +3073,8 @@ async function main() {
2646
3073
  ` [1] Overview Your profile + registry stats`,
2647
3074
  ` [2] Leaderboard Top contributors ranking`,
2648
3075
  ` [3] Benchmark LLM model performance comparison`,
3076
+ ` [4] Activity Your recent audits & findings`,
3077
+ ` [5] Search Search packages (interactive input)`,
2649
3078
  ``,
2650
3079
  `${c.bold}Aliases:${c.reset} dashboard, dash`,
2651
3080
  ],
@@ -2684,6 +3113,41 @@ async function main() {
2684
3113
  ` agentaudit benchmark --json`,
2685
3114
  ],
2686
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
2687
3151
  };
2688
3152
 
2689
3153
  // Show subcommand help: `agentaudit help <cmd>` or `agentaudit <cmd> --help`
@@ -2691,7 +3155,7 @@ async function main() {
2691
3155
  let helpLines = subcommandHelp[cmd];
2692
3156
  if (helpLines === null) {
2693
3157
  // alias redirect
2694
- const aliases = { check: 'lookup', dash: 'dashboard', lb: 'leaderboard', bench: 'benchmark' };
3158
+ const aliases = { check: 'lookup', dash: 'dashboard', lb: 'leaderboard', bench: 'benchmark', my: 'activity', find: 'search' };
2695
3159
  helpLines = subcommandHelp[aliases[cmd] || cmd];
2696
3160
  }
2697
3161
  if (!helpLines) {
@@ -2739,6 +3203,8 @@ async function main() {
2739
3203
  console.log(` ${c.cyan}dashboard${c.reset} Interactive dashboard (full-screen)`);
2740
3204
  console.log(` ${c.cyan}leaderboard${c.reset} Top contributors ranking`);
2741
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`);
2742
3208
  console.log();
2743
3209
  console.log(` ${c.bold}CONFIGURATION${c.reset}`);
2744
3210
  console.log(` ${c.cyan}model${c.reset} Configure LLM provider + model`);
@@ -2784,6 +3250,14 @@ async function main() {
2784
3250
  await benchmarkCommand(targets);
2785
3251
  return;
2786
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
+ }
2787
3261
 
2788
3262
  banner();
2789
3263
 
@@ -2859,13 +3333,20 @@ async function main() {
2859
3333
  fetch(`${REGISTRY_URL}/api/leaderboard?limit=100`, { signal: AbortSignal.timeout(10_000) }).then(r => r.ok ? r.json() : null),
2860
3334
  ]);
2861
3335
  if (agentRes) {
2862
- console.log(` ${c.bold}Profile${c.reset}`);
2863
- let rank = '-';
3336
+ let rank = null;
2864
3337
  if (Array.isArray(lbRes)) {
2865
3338
  const idx = lbRes.findIndex(e => e.agent_name === creds.agent_name);
2866
- if (idx >= 0) rank = `#${idx + 1} of ${lbRes.length}`;
3339
+ if (idx >= 0) rank = idx + 1;
2867
3340
  }
2868
- console.log(` Rank ${c.bold}${rank}${c.reset}`);
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}`);
2869
3350
  console.log(` Points ${c.bold}${fmtNum(agentRes.total_points || 0)}${c.reset}`);
2870
3351
  console.log(` Audits ${c.bold}${fmtNum(agentRes.total_reports || 0)}${c.reset}`);
2871
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}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentaudit",
3
- "version": "3.10.3",
3
+ "version": "3.10.4",
4
4
  "description": "Security scanner for AI packages — MCP server + CLI",
5
5
  "type": "module",
6
6
  "bin": {