agentaudit 3.9.18 → 3.9.20

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 +32 -4
  2. package/cli.mjs +222 -107
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -208,6 +208,7 @@ Then ask your agent: *"Check which MCP servers I have installed and audit any un
208
208
  | `agentaudit audit <url>` | Deep LLM-powered 3-pass audit (~30s) | `agentaudit audit https://github.com/owner/repo` |
209
209
  | `agentaudit lookup <name>` | Look up package in trust registry | `agentaudit lookup fastmcp` |
210
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` |
211
212
  | `agentaudit setup` | Register agent + configure API key | `agentaudit setup` |
212
213
 
213
214
  ### Global Flags
@@ -217,6 +218,7 @@ Then ask your agent: *"Check which MCP servers I have installed and audit any un
217
218
  | `--json` | Output machine-readable JSON to stdout |
218
219
  | `--quiet` / `-q` | Suppress banner and decorative output (show findings only) |
219
220
  | `--no-color` | Disable ANSI colors (also respects `NO_COLOR` env var) |
221
+ | `--provider <name>` | Force LLM provider (`anthropic`, `openai`, `openrouter`, `ollama`, `custom`) |
220
222
  | `--help` / `-h` | Show help text |
221
223
  | `-v` / `--version` | Show version |
222
224
 
@@ -433,12 +435,19 @@ export AGENTAUDIT_API_KEY=asf_your_key_here
433
435
  | Variable | Description |
434
436
  |----------|-------------|
435
437
  | `AGENTAUDIT_API_KEY` | API key for registry access |
436
- | `ANTHROPIC_API_KEY` | Anthropic API key for deep audits (Claude) |
438
+ | `ANTHROPIC_API_KEY` | Anthropic API key for deep audits (Claude) -- recommended |
437
439
  | `OPENAI_API_KEY` | OpenAI API key for deep audits (GPT-4o) |
438
440
  | `OPENROUTER_API_KEY` | OpenRouter API key (access 200+ models) |
439
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 |
440
447
  | `NO_COLOR` | Disable ANSI colors ([no-color.org](https://no-color.org)) |
441
448
 
449
+ > **Provider priority:** Anthropic > OpenAI > OpenRouter > Custom > Ollama. Override with `--provider=ollama` etc.
450
+
442
451
  ---
443
452
 
444
453
  ## 📦 Requirements
@@ -468,7 +477,7 @@ Or use without installing: `npx agentaudit`
468
477
 
469
478
  ### Setting up your LLM key for deep audits
470
479
 
471
- The `audit` command supports **three LLM providers**. Set one of these environment variables:
480
+ The `audit` command supports **any LLM provider**. Set one of these environment variables:
472
481
 
473
482
  ```bash
474
483
  # Linux / macOS
@@ -487,13 +496,32 @@ set OPENAI_API_KEY=sk-...
487
496
  set OPENROUTER_API_KEY=sk-or-...
488
497
  ```
489
498
 
490
- **Provider priority:** Anthropic > OpenAI > OpenRouter. The active provider is shown during the audit.
499
+ **Provider priority:** Anthropic > OpenAI > OpenRouter > Custom > Ollama. Override with `--provider=<name>`.
491
500
 
492
- **OpenRouter model selection:** By default, OpenRouter uses `anthropic/claude-sonnet-4`. Override with:
501
+ **OpenRouter model selection:** By default uses `anthropic/claude-sonnet-4`. Override with:
493
502
  ```bash
494
503
  export OPENROUTER_MODEL=google/gemini-2.5-pro # or any model on openrouter.ai
495
504
  ```
496
505
 
506
+ **Local with Ollama (free, no API key):**
507
+ ```bash
508
+ export OLLAMA_MODEL=llama3.1 # or qwen2.5-coder, deepseek-r1, etc.
509
+ agentaudit audit https://github.com/owner/repo
510
+ ```
511
+ > Note: Local models produce lower quality audits than Claude/GPT-4o. Use for quick checks, not production security audits.
512
+
513
+ **Any OpenAI-compatible API:**
514
+ ```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
518
+ ```
519
+
520
+ **Check your setup:**
521
+ ```bash
522
+ agentaudit status # validates all configured API keys
523
+ ```
524
+
497
525
  **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.
498
526
 
499
527
  ### What data is sent externally?
package/cli.mjs CHANGED
@@ -1,18 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * AgentAudit CLI — Security scanner for AI packages
3
+ * AgentAudit CLI — Security scanner for AI tools
4
4
  *
5
- * Usage:
6
- * agentaudit Discover local MCP servers
7
- * agentaudit discover [--quick|--deep] Find MCP servers in AI editors
8
- * agentaudit scan <repo-url> [--deep] Quick scan (or deep audit with --deep)
9
- * agentaudit audit <repo-url> Deep LLM-powered security audit
10
- * agentaudit lookup <name> Look up package in registry
11
- * agentaudit check <name|url> Lookup + auto-audit if not found
12
- * agentaudit status Check configured API keys + providers
13
- * agentaudit setup Register + configure API key
5
+ * Scan & Audit: scan <url>, audit <url>, discover
6
+ * Registry: check <name|url>, lookup <name>
7
+ * Setup: status, setup, config
14
8
  *
15
- * Global flags: --json, --quiet, --no-color
9
+ * Global flags: --json, --quiet, --no-color, --provider, --debug, --export
16
10
  */
17
11
 
18
12
  import fs from 'fs';
@@ -27,23 +21,38 @@ const SKILL_DIR = path.resolve(__dirname);
27
21
  const REGISTRY_URL = 'https://agentaudit.dev';
28
22
 
29
23
  // ── Provider resolution ────
30
- function resolveProvider(preferred, keys) {
24
+ function resolveProvider(flagOverride, keys) {
25
+ const orModel = process.env.OPENROUTER_MODEL || 'anthropic/claude-sonnet-4';
26
+ const ollamaModel = process.env.OLLAMA_MODEL || 'llama3.1';
27
+ const ollamaHost = process.env.OLLAMA_HOST || 'http://localhost:11434';
28
+ const customUrl = process.env.LLM_API_URL;
29
+ const customKey = process.env.LLM_API_KEY;
30
+ const customModel = process.env.LLM_MODEL || 'default';
31
+
31
32
  const providers = {
32
33
  anthropic: keys.anthropicKey ? { id: 'anthropic', label: 'Anthropic (Claude)', key: keys.anthropicKey } : null,
33
34
  openai: keys.openaiKey ? { id: 'openai', label: 'OpenAI (GPT-4o)', key: keys.openaiKey } : null,
34
- openrouter: keys.openrouterKey ? { id: 'openrouter', label: `OpenRouter (${process.env.OPENROUTER_MODEL || 'anthropic/claude-sonnet-4'})`, key: keys.openrouterKey } : null,
35
+ openrouter: keys.openrouterKey ? { id: 'openrouter', label: `OpenRouter (${orModel})`, key: keys.openrouterKey } : null,
36
+ ollama: process.env.OLLAMA_MODEL || process.env.OLLAMA_HOST ? { id: 'ollama', label: `Ollama (${ollamaModel})`, key: null, host: ollamaHost, model: ollamaModel } : null,
37
+ custom: customUrl ? { id: 'custom', label: `Custom (${customModel})`, key: customKey, url: customUrl, model: customModel } : null,
35
38
  };
36
39
  // Aliases
37
- const aliases = { claude: 'anthropic', gpt: 'openai', 'gpt-4o': 'openai', 'gpt4': 'openai', or: 'openrouter' };
40
+ const aliases = { claude: 'anthropic', gpt: 'openai', 'gpt-4o': 'openai', 'gpt4': 'openai', or: 'openrouter', local: 'ollama' };
41
+
42
+ // Priority: --provider flag > AGENTAUDIT_PROVIDER env > config file > auto-detect
43
+ const preferred = flagOverride
44
+ || process.env.AGENTAUDIT_PROVIDER?.toLowerCase()
45
+ || loadConfig()?.preferred_provider
46
+ || null;
38
47
 
39
48
  if (preferred) {
40
49
  const resolved = aliases[preferred] || preferred;
41
50
  const p = providers[resolved];
42
- if (!p) return null; // requested provider not available
51
+ if (!p) return null;
43
52
  return p;
44
53
  }
45
- // Auto-detect: Anthropic > OpenAI > OpenRouter
46
- return providers.anthropic || providers.openai || providers.openrouter || null;
54
+ // Auto-detect priority: Anthropic > OpenAI > OpenRouter > Custom > Ollama (local last — usually weaker)
55
+ return providers.anthropic || providers.openai || providers.openrouter || providers.custom || providers.ollama || null;
47
56
  }
48
57
 
49
58
  // ── Global flags (set in main before command routing) ────
@@ -120,6 +129,24 @@ function saveCredentials(data) {
120
129
  } catch {}
121
130
  }
122
131
 
132
+ const USER_CONFIG_FILE = path.join(USER_CRED_DIR, 'config.json');
133
+
134
+ function loadConfig() {
135
+ try {
136
+ if (fs.existsSync(USER_CONFIG_FILE)) {
137
+ return JSON.parse(fs.readFileSync(USER_CONFIG_FILE, 'utf8'));
138
+ }
139
+ } catch {}
140
+ return {};
141
+ }
142
+
143
+ function saveConfig(data) {
144
+ const existing = loadConfig();
145
+ const merged = { ...existing, ...data };
146
+ fs.mkdirSync(USER_CRED_DIR, { recursive: true });
147
+ fs.writeFileSync(USER_CONFIG_FILE, JSON.stringify(merged, null, 2), { mode: 0o600 });
148
+ }
149
+
123
150
  function askQuestion(question) {
124
151
  const rl = createInterface({ input: process.stdin, output: process.stdout });
125
152
  return new Promise(resolve => rl.question(question, answer => { rl.close(); resolve(answer.trim()); }));
@@ -303,6 +330,28 @@ async function setupCommand() {
303
330
  console.log();
304
331
  }
305
332
 
333
+ // ── Structured error output ─────────────────────────────
334
+
335
+ function emitError(code, message, hint, exitCode = 2) {
336
+ if (jsonMode) {
337
+ process.stderr.write(JSON.stringify({ error: true, code, message, hint: hint || undefined, exitCode }) + '\n');
338
+ }
339
+ process.exitCode = exitCode;
340
+ }
341
+
342
+ // ── Levenshtein distance for typo correction ────────────
343
+
344
+ function levenshtein(a, b) {
345
+ const m = a.length, n = b.length;
346
+ const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
347
+ for (let i = 0; i <= m; i++) dp[i][0] = i;
348
+ for (let j = 0; j <= n; j++) dp[0][j] = j;
349
+ for (let i = 1; i <= m; i++)
350
+ for (let j = 1; j <= n; j++)
351
+ dp[i][j] = Math.min(dp[i-1][j] + 1, dp[i][j-1] + 1, dp[i-1][j-1] + (a[i-1] !== b[j-1] ? 1 : 0));
352
+ return dp[m][n];
353
+ }
354
+
306
355
  // ── Helpers ──────────────────────────────────────────────
307
356
 
308
357
  function getVersion() {
@@ -315,8 +364,8 @@ function getVersion() {
315
364
  function banner() {
316
365
  if (quietMode || jsonMode) return;
317
366
  console.log();
318
- console.log(` ${c.bold}${c.cyan}AgentAudit${c.reset} ${c.dim}v${getVersion()}${c.reset}`);
319
- console.log(` ${c.dim}Security scanner for AI packages${c.reset}`);
367
+ console.log(` 🛡 ${c.bold}${c.cyan}AgentAudit${c.reset} ${c.dim}v${getVersion()}${c.reset}`);
368
+ console.log(` ${c.dim}Security scanner for AI tools${c.reset}`);
320
369
  console.log();
321
370
  }
322
371
 
@@ -1344,10 +1393,14 @@ async function auditRepo(url) {
1344
1393
  if (!resolvedProvider) {
1345
1394
  // No LLM API key — clear explanation
1346
1395
  console.log();
1347
- console.log(` ${c.yellow}No LLM API key found.${c.reset} The ${c.bold}audit${c.reset} command needs an LLM to analyze code.`);
1396
+ console.log(` ${c.yellow}No LLM provider configured.${c.reset} The ${c.bold}audit${c.reset} command needs an LLM to analyze code.`);
1348
1397
  console.log();
1349
- console.log(` ${c.bold}Option 1: Set an API key${c.reset}`);
1350
- console.log(` Supported keys: ${c.cyan}ANTHROPIC_API_KEY${c.reset}, ${c.cyan}OPENAI_API_KEY${c.reset}, or ${c.cyan}OPENROUTER_API_KEY${c.reset}`);
1398
+ console.log(` ${c.bold}Option 1: Set an API key${c.reset} ${c.dim}(any one of these)${c.reset}`);
1399
+ console.log(` ${c.cyan}ANTHROPIC_API_KEY${c.reset} Anthropic Claude ${c.dim}(recommended)${c.reset}`);
1400
+ console.log(` ${c.cyan}OPENAI_API_KEY${c.reset} OpenAI GPT-4o`);
1401
+ console.log(` ${c.cyan}OPENROUTER_API_KEY${c.reset} OpenRouter ${c.dim}(200+ models)${c.reset}`);
1402
+ console.log(` ${c.cyan}OLLAMA_MODEL${c.reset} Ollama ${c.dim}(local, free, set model name)${c.reset}`);
1403
+ console.log(` ${c.cyan}LLM_API_URL${c.reset} Any OpenAI-compatible API ${c.dim}(+ LLM_API_KEY, LLM_MODEL)${c.reset}`);
1351
1404
  console.log();
1352
1405
  console.log(` ${c.dim}# Linux / macOS:${c.reset}`);
1353
1406
  console.log(` ${c.dim}export ANTHROPIC_API_KEY=sk-ant-...${c.reset}`);
@@ -1451,19 +1504,33 @@ async function auditRepo(url) {
1451
1504
  _lastLlmText = text;
1452
1505
  report = extractJSON(text);
1453
1506
  } else {
1454
- // OpenAI or OpenRouter (both use OpenAI-compatible API)
1455
- const isOpenRouter = resolvedProvider.id === 'openrouter';
1456
- const apiUrl = isOpenRouter ? 'https://openrouter.ai/api/v1/chat/completions' : 'https://api.openai.com/v1/chat/completions';
1457
- const modelName = isOpenRouter ? (process.env.OPENROUTER_MODEL || 'anthropic/claude-sonnet-4') : 'gpt-4o';
1458
- const extraHeaders = isOpenRouter ? { 'HTTP-Referer': 'https://agentaudit.dev', 'X-Title': 'AgentAudit' } : {};
1507
+ // OpenAI, OpenRouter, Ollama, or Custom (all use OpenAI-compatible chat completions API)
1508
+ let apiUrl, modelName, authHeaders;
1509
+ switch (resolvedProvider.id) {
1510
+ case 'openrouter':
1511
+ apiUrl = 'https://openrouter.ai/api/v1/chat/completions';
1512
+ modelName = process.env.OPENROUTER_MODEL || 'anthropic/claude-sonnet-4';
1513
+ authHeaders = { 'Authorization': `Bearer ${resolvedProvider.key}`, 'HTTP-Referer': 'https://agentaudit.dev', 'X-Title': 'AgentAudit' };
1514
+ break;
1515
+ case 'ollama':
1516
+ apiUrl = `${resolvedProvider.host}/v1/chat/completions`;
1517
+ modelName = resolvedProvider.model;
1518
+ authHeaders = {};
1519
+ break;
1520
+ case 'custom':
1521
+ apiUrl = resolvedProvider.url.endsWith('/chat/completions') ? resolvedProvider.url : `${resolvedProvider.url.replace(/\/$/, '')}/chat/completions`;
1522
+ modelName = resolvedProvider.model;
1523
+ authHeaders = resolvedProvider.key ? { 'Authorization': `Bearer ${resolvedProvider.key}` } : {};
1524
+ break;
1525
+ default: // openai
1526
+ apiUrl = 'https://api.openai.com/v1/chat/completions';
1527
+ modelName = 'gpt-4o';
1528
+ authHeaders = { 'Authorization': `Bearer ${resolvedProvider.key}` };
1529
+ }
1459
1530
 
1460
1531
  const res = await fetch(apiUrl, {
1461
1532
  method: 'POST',
1462
- headers: {
1463
- 'Authorization': `Bearer ${resolvedProvider.key}`,
1464
- 'Content-Type': 'application/json',
1465
- ...extraHeaders,
1466
- },
1533
+ headers: { 'Content-Type': 'application/json', ...authHeaders },
1467
1534
  body: JSON.stringify({
1468
1535
  model: modelName,
1469
1536
  max_tokens: 8192,
@@ -1472,7 +1539,7 @@ async function auditRepo(url) {
1472
1539
  { role: 'user', content: userMessage },
1473
1540
  ],
1474
1541
  }),
1475
- signal: AbortSignal.timeout(120_000),
1542
+ signal: AbortSignal.timeout(resolvedProvider.id === 'ollama' ? 300_000 : 120_000), // Ollama: 5min (local can be slow)
1476
1543
  });
1477
1544
  const data = await res.json();
1478
1545
  if (data.error) {
@@ -1579,8 +1646,11 @@ async function checkPackage(name) {
1579
1646
  console.log();
1580
1647
  return await auditRepo(name);
1581
1648
  }
1582
- console.log(` ${c.yellow}Not found${c.reset} — package "${name}" hasn't been audited yet.`);
1583
- console.log(` ${c.dim}Run: agentaudit audit <repo-url> for a deep LLM audit${c.reset}`);
1649
+ console.log(` ${c.yellow}Not found${c.reset} — "${name}" hasn't been audited yet.`);
1650
+ console.log();
1651
+ console.log(` ${c.dim}Next steps:${c.reset}`);
1652
+ console.log(` ${c.cyan}agentaudit audit <repo-url>${c.reset} ${c.dim}Run a deep audit yourself${c.reset}`);
1653
+ console.log(` ${c.cyan}agentaudit scan <repo-url>${c.reset} ${c.dim}Quick static check (no API key)${c.reset}`);
1584
1654
  }
1585
1655
  return null;
1586
1656
  }
@@ -1672,57 +1742,41 @@ async function main() {
1672
1742
 
1673
1743
  if (args[0] === '--help' || args[0] === '-h') {
1674
1744
  banner();
1675
- console.log(` ${c.bold}Commands:${c.reset}`);
1676
- console.log();
1677
- console.log(` ${c.cyan}agentaudit${c.reset} Discover MCP servers (same as discover)`);
1678
- console.log(` ${c.cyan}agentaudit discover${c.reset} Find MCP servers in your AI editors (Cursor, Claude, VS Code, Windsurf)`);
1679
- console.log(` ${c.cyan}agentaudit discover --quick${c.reset} Discover + auto-scan all servers`);
1680
- console.log(` ${c.cyan}agentaudit discover --deep${c.reset} Discover + select servers to deep-audit`);
1681
- console.log(` ${c.cyan}agentaudit scan${c.reset} <url> [url...] Quick static scan (regex, local)`);
1682
- console.log(` ${c.cyan}agentaudit scan${c.reset} <url> ${c.dim}--deep${c.reset} Deep audit (same as audit)`);
1683
- console.log(` ${c.cyan}agentaudit audit${c.reset} <url> [url...] Deep LLM-powered security audit`);
1684
- console.log(` ${c.cyan}agentaudit lookup${c.reset} <name> Look up package in registry`);
1685
- console.log(` ${c.cyan}agentaudit check${c.reset} <name|url> Lookup + auto-audit if not found`);
1686
- console.log(` ${c.cyan}agentaudit status${c.reset} Check API keys + active provider`);
1687
- console.log(` ${c.cyan}agentaudit setup${c.reset} Register + configure API key`);
1745
+ console.log(` ${c.bold}USAGE${c.reset}`);
1746
+ console.log(` ${c.cyan}agentaudit${c.reset} <command> [options]`);
1688
1747
  console.log();
1689
- console.log(` ${c.bold}Global flags:${c.reset}`);
1690
- console.log(` ${c.dim}--json Output JSON to stdout (machine-readable)${c.reset}`);
1691
- console.log(` ${c.dim}--quiet Suppress banner and tree visualization${c.reset}`);
1692
- console.log(` ${c.dim}--no-color Disable ANSI colors (also: NO_COLOR env)${c.reset}`);
1693
- console.log(` ${c.dim}--provider Force LLM provider (anthropic, openai, openrouter)${c.reset}`);
1748
+ console.log(` ${c.bold}SCAN & AUDIT${c.reset}`);
1749
+ console.log(` ${c.cyan}scan${c.reset} <url> [url...] Quick static analysis ${c.dim}(~2s, no API key)${c.reset}`);
1750
+ console.log(` ${c.cyan}audit${c.reset} <url> [url...] Deep LLM security audit ${c.dim}(~30s)${c.reset}`);
1751
+ console.log(` ${c.cyan}discover${c.reset} Find MCP servers in your editors`);
1694
1752
  console.log();
1695
- console.log(` ${c.bold}Quick Scan${c.reset} vs ${c.bold}Deep Audit${c.reset}:`);
1696
- console.log(` ${c.dim}scan = fast regex-based static analysis (~2s)${c.reset}`);
1697
- console.log(` ${c.dim}audit = deep LLM analysis with 3-pass methodology (~30s)${c.reset}`);
1753
+ console.log(` ${c.bold}REGISTRY${c.reset}`);
1754
+ console.log(` ${c.cyan}check${c.reset} <name|url> Look up or auto-audit package`);
1755
+ console.log(` ${c.cyan}lookup${c.reset} <name> Look up package in registry`);
1698
1756
  console.log();
1699
- console.log(` ${c.bold}Exit codes:${c.reset}`);
1700
- console.log(` ${c.dim}0 = clean / success 1 = findings detected 2 = error${c.reset}`);
1757
+ console.log(` ${c.bold}SETUP${c.reset}`);
1758
+ console.log(` ${c.cyan}status${c.reset} Check providers & API keys`);
1759
+ console.log(` ${c.cyan}setup${c.reset} Register & configure`);
1760
+ console.log(` ${c.cyan}config set${c.reset} <key> <value> Set default provider/options`);
1701
1761
  console.log();
1702
- console.log(` ${c.bold}Examples:${c.reset}`);
1703
- console.log(` agentaudit`);
1704
- console.log(` agentaudit discover --quick`);
1705
- console.log(` agentaudit scan https://github.com/owner/repo`);
1706
- console.log(` agentaudit audit https://github.com/owner/repo`);
1707
- console.log(` agentaudit lookup fastmcp --json`);
1762
+ console.log(` ${c.bold}OPTIONS${c.reset}`);
1763
+ console.log(` ${c.dim}--json Machine-readable JSON output${c.reset}`);
1764
+ console.log(` ${c.dim}--quiet Suppress banner${c.reset}`);
1765
+ console.log(` ${c.dim}--no-color Disable colors ${c.reset}${c.dim}(also: NO_COLOR=1)${c.reset}`);
1766
+ console.log(` ${c.dim}--provider <p> Force provider ${c.reset}${c.dim}(anthropic|openai|openrouter|ollama|custom)${c.reset}`);
1767
+ console.log(` ${c.dim}--export Export audit payload to markdown${c.reset}`);
1768
+ console.log(` ${c.dim}--debug Show raw LLM response on errors${c.reset}`);
1708
1769
  console.log();
1709
- console.log(` ${c.bold}For deep audits,${c.reset} set an LLM API key (any one):`);
1710
- if (process.platform === 'win32') {
1711
- console.log(` ${c.dim}PowerShell: $env:ANTHROPIC_API_KEY = "sk-ant-..."${c.reset}`);
1712
- console.log(` ${c.dim} $env:OPENAI_API_KEY = "sk-..."${c.reset}`);
1713
- console.log(` ${c.dim} $env:OPENROUTER_API_KEY = "sk-or-..."${c.reset}`);
1714
- console.log(` ${c.dim}CMD: set ANTHROPIC_API_KEY=sk-ant-...${c.reset}`);
1715
- console.log(` ${c.dim} set OPENAI_API_KEY=sk-...${c.reset}`);
1716
- console.log(` ${c.dim} set OPENROUTER_API_KEY=sk-or-...${c.reset}`);
1717
- } else {
1718
- console.log(` ${c.dim}export ANTHROPIC_API_KEY=sk-ant-...${c.reset}`);
1719
- console.log(` ${c.dim}export OPENAI_API_KEY=sk-...${c.reset}`);
1720
- console.log(` ${c.dim}export OPENROUTER_API_KEY=sk-or-...${c.reset} ${c.dim}(200+ models, set OPENROUTER_MODEL to pick)${c.reset}`);
1721
- }
1770
+ console.log(` ${c.bold}EXAMPLES${c.reset}`);
1771
+ console.log(` ${c.dim}$${c.reset} agentaudit scan https://github.com/owner/repo`);
1772
+ console.log(` ${c.dim}$${c.reset} agentaudit audit https://github.com/owner/repo`);
1773
+ console.log(` ${c.dim}$${c.reset} agentaudit check fastmcp`);
1774
+ console.log(` ${c.dim}$${c.reset} agentaudit status`);
1722
1775
  console.log();
1723
- console.log(` ${c.bold}Or use as MCP server${c.reset} in Cursor/Claude ${c.dim}(no extra API key needed):${c.reset}`);
1724
- console.log(` ${c.dim}Add to your MCP config:${c.reset}`);
1725
- console.log(` ${c.dim}{ "agentaudit": { "command": "npx", "args": ["-y", "agentaudit"] } }${c.reset}`);
1776
+ console.log(` ${c.bold}PROVIDERS${c.reset} ${c.dim}(set any one for deep audits)${c.reset}`);
1777
+ console.log(` ${c.dim}ANTHROPIC_API_KEY · OPENAI_API_KEY · OPENROUTER_API_KEY · OLLAMA_MODEL · LLM_API_URL${c.reset}`);
1778
+ console.log(` ${c.dim}Set default: AGENTAUDIT_PROVIDER=openai or agentaudit config set provider openai${c.reset}`);
1779
+ console.log(` ${c.dim}Run ${c.cyan}agentaudit status${c.dim} to check configuration.${c.reset}`);
1726
1780
  console.log();
1727
1781
  process.exitCode = 0; return;
1728
1782
  }
@@ -1746,10 +1800,16 @@ async function main() {
1746
1800
  openaiKey: process.env.OPENAI_API_KEY,
1747
1801
  openrouterKey: process.env.OPENROUTER_API_KEY,
1748
1802
  };
1803
+ const ollamaHost = process.env.OLLAMA_HOST || 'http://localhost:11434';
1804
+ const ollamaModel = process.env.OLLAMA_MODEL;
1805
+ const customUrl = process.env.LLM_API_URL;
1806
+
1749
1807
  const checks = [
1750
1808
  { name: 'Anthropic', env: 'ANTHROPIC_API_KEY', key: keys.anthropicKey, testUrl: 'https://api.anthropic.com/v1/messages', testHeaders: (k) => ({ 'x-api-key': k, 'anthropic-version': '2023-06-01', 'content-type': 'application/json' }), testBody: JSON.stringify({ model: 'claude-sonnet-4-20250514', max_tokens: 1, messages: [{ role: 'user', content: 'hi' }] }) },
1751
- { name: 'OpenAI', env: 'OPENAI_API_KEY', key: keys.openaiKey, testUrl: 'https://api.openai.com/v1/models', testHeaders: (k) => ({ 'Authorization': `Bearer ${k}` }), testBody: null },
1752
- { name: 'OpenRouter', env: 'OPENROUTER_API_KEY', key: keys.openrouterKey, testUrl: 'https://openrouter.ai/api/v1/models', testHeaders: (k) => ({ 'Authorization': `Bearer ${k}` }), testBody: null },
1809
+ { name: 'OpenAI', env: 'OPENAI_API_KEY', key: keys.openaiKey, testUrl: 'https://api.openai.com/v1/chat/completions', testHeaders: (k) => ({ 'Authorization': `Bearer ${k}`, 'Content-Type': 'application/json' }), testBody: JSON.stringify({ model: 'gpt-4o-mini', max_tokens: 1, messages: [{ role: 'user', content: 'hi' }] }) },
1810
+ { name: 'OpenRouter', env: 'OPENROUTER_API_KEY', key: keys.openrouterKey, testUrl: 'https://openrouter.ai/api/v1/chat/completions', testHeaders: (k) => ({ 'Authorization': `Bearer ${k}`, 'Content-Type': 'application/json', 'HTTP-Referer': 'https://agentaudit.dev', 'X-Title': 'AgentAudit' }), testBody: JSON.stringify({ model: 'openai/gpt-4o-mini', max_tokens: 1, messages: [{ role: 'user', content: 'hi' }] }) },
1811
+ { name: 'Ollama', env: 'OLLAMA_MODEL', key: ollamaModel, testUrl: `${ollamaHost}/api/tags`, testHeaders: () => ({}), testBody: null },
1812
+ { name: 'Custom', env: 'LLM_API_URL', key: customUrl, testUrl: customUrl ? `${customUrl.replace(/\/$/, '')}/models` : null, testHeaders: (k) => process.env.LLM_API_KEY ? ({ 'Authorization': `Bearer ${process.env.LLM_API_KEY}` }) : {}, testBody: null },
1753
1813
  ];
1754
1814
 
1755
1815
  for (const p of checks) {
@@ -1770,8 +1830,25 @@ async function main() {
1770
1830
  process.stdout.write(`\r ${c.green}●${c.reset} ${p.name.padEnd(12)} ${c.dim}${masked}${c.reset} ${c.green}valid ✓${c.reset} \n`);
1771
1831
  } else {
1772
1832
  const body = await res.json().catch(() => ({}));
1773
- const errMsg = body?.error?.message || `HTTP ${res.status}`;
1774
- process.stdout.write(`\r ${c.red}●${c.reset} ${p.name.padEnd(12)} ${c.dim}${masked}${c.reset} ${c.red}invalid ✗${c.reset} ${c.dim}(${errMsg})${c.reset} \n`);
1833
+ const rawMsg = body?.error?.message || body?.message || `HTTP ${res.status}`;
1834
+ // Detect specific error types for clearer messages
1835
+ const lcMsg = rawMsg.toLowerCase();
1836
+ let errMsg = rawMsg;
1837
+ let hint = '';
1838
+ if (lcMsg.includes('credit') || lcMsg.includes('balance') || lcMsg.includes('quota') || lcMsg.includes('billing') || lcMsg.includes('exceeded') || lcMsg.includes('insufficient')) {
1839
+ errMsg = 'no credits';
1840
+ if (p.name === 'Anthropic') hint = `\n ${c.dim}└─ Add credits: console.anthropic.com/settings/plans${c.reset}`;
1841
+ else if (p.name === 'OpenAI') hint = `\n ${c.dim}└─ Check usage: platform.openai.com/usage${c.reset}`;
1842
+ else if (p.name === 'OpenRouter') hint = `\n ${c.dim}└─ Check balance: openrouter.ai/credits${c.reset}`;
1843
+ } else if (res.status === 401 || lcMsg.includes('invalid') || lcMsg.includes('unauthorized') || lcMsg.includes('authentication')) {
1844
+ errMsg = 'invalid key';
1845
+ if (p.name === 'Anthropic') hint = `\n ${c.dim}└─ Check key: console.anthropic.com/settings/keys${c.reset}`;
1846
+ else if (p.name === 'OpenAI') hint = `\n ${c.dim}└─ Check key: platform.openai.com/api-keys${c.reset}`;
1847
+ } else if (res.status === 429) {
1848
+ errMsg = 'rate limited';
1849
+ hint = `\n ${c.dim}└─ Try again in a moment${c.reset}`;
1850
+ }
1851
+ process.stdout.write(`\r ${c.red}●${c.reset} ${p.name.padEnd(12)} ${c.dim}${masked}${c.reset} ${c.red}✖ ${errMsg}${c.reset}${hint} \n`);
1775
1852
  }
1776
1853
  } catch (e) {
1777
1854
  process.stdout.write(`\r ${c.red}●${c.reset} ${p.name.padEnd(12)} ${c.dim}${masked}${c.reset} ${c.red}error ✗${c.reset} ${c.dim}(${e.message})${c.reset} \n`);
@@ -1781,10 +1858,13 @@ async function main() {
1781
1858
  const resolved = resolveProvider(null, keys);
1782
1859
  console.log();
1783
1860
  if (resolved) {
1784
- console.log(` ${c.bold}Active provider:${c.reset} ${resolved.label}`);
1785
- console.log(` ${c.dim}Override with: --provider=openai | --provider=openrouter | --provider=anthropic${c.reset}`);
1861
+ console.log(` ${c.bold}Active:${c.reset} ${c.green}${resolved.label}${c.reset}`);
1862
+ console.log(` ${c.dim}Override: --provider=<name> or AGENTAUDIT_PROVIDER=<name>${c.reset}`);
1863
+ console.log(` ${c.dim}Set default: agentaudit config set provider <name>${c.reset}`);
1786
1864
  } else {
1787
- console.log(` ${c.yellow}No LLM provider configured.${c.reset} Set one of the API keys above for deep audits.`);
1865
+ console.log(` ${c.yellow}No working LLM provider.${c.reset} Deep audits require one.`);
1866
+ console.log(` ${c.dim}Set a key: export ANTHROPIC_API_KEY=sk-ant-...${c.reset}`);
1867
+ console.log(` ${c.dim}Or scan without LLM: agentaudit scan <url>${c.reset}`);
1788
1868
  }
1789
1869
 
1790
1870
  // AgentAudit registry key
@@ -1808,10 +1888,47 @@ async function main() {
1808
1888
  return;
1809
1889
  }
1810
1890
 
1891
+ if (command === 'config') {
1892
+ const subCmd = targets[0];
1893
+ if (subCmd === 'set' && targets[1] === 'provider' && targets[2]) {
1894
+ const validProviders = ['anthropic', 'openai', 'openrouter', 'ollama', 'custom', 'claude', 'gpt'];
1895
+ const val = targets[2].toLowerCase();
1896
+ if (!validProviders.includes(val)) {
1897
+ console.log(` ${c.red}✖ Unknown provider: ${val}${c.reset}`);
1898
+ console.log(` ${c.dim}Valid: anthropic, openai, openrouter, ollama, custom${c.reset}`);
1899
+ process.exitCode = 2; return;
1900
+ }
1901
+ saveConfig({ preferred_provider: val });
1902
+ console.log(` ${c.green}✔${c.reset} Default provider set to: ${c.bold}${val}${c.reset}`);
1903
+ console.log(` ${c.dim}Override per-command: --provider=<name>${c.reset}`);
1904
+ console.log(` ${c.dim}Or env: AGENTAUDIT_PROVIDER=<name>${c.reset}`);
1905
+ } else if (subCmd === 'get' || !subCmd) {
1906
+ const cfg = loadConfig();
1907
+ console.log(` ${c.bold}Config:${c.reset} ${USER_CONFIG_FILE}`);
1908
+ if (Object.keys(cfg).length === 0) {
1909
+ console.log(` ${c.dim}(empty — using defaults)${c.reset}`);
1910
+ } else {
1911
+ for (const [k, v] of Object.entries(cfg)) {
1912
+ console.log(` ${c.dim}${k}:${c.reset} ${v}`);
1913
+ }
1914
+ }
1915
+ } else if (subCmd === 'reset') {
1916
+ try { fs.unlinkSync(USER_CONFIG_FILE); } catch {}
1917
+ console.log(` ${c.green}✔${c.reset} Config reset to defaults.`);
1918
+ } else {
1919
+ console.log(` ${c.red}✖ Unknown config command${c.reset}`);
1920
+ console.log(` ${c.dim}Usage: agentaudit config set provider <name>${c.reset}`);
1921
+ console.log(` ${c.dim} agentaudit config get${c.reset}`);
1922
+ console.log(` ${c.dim} agentaudit config reset${c.reset}`);
1923
+ }
1924
+ return;
1925
+ }
1926
+
1811
1927
  if (command === 'lookup' || command === 'check') {
1812
1928
  const names = targets.filter(t => !t.startsWith('--'));
1813
1929
  if (names.length === 0) {
1814
- console.log(` ${c.red}Error: package name required${c.reset}`);
1930
+ console.log(` ${c.red}✖ Package name or URL required${c.reset}`);
1931
+ console.log(` ${c.dim}Usage: agentaudit check <name|url>${c.reset}`);
1815
1932
  process.exitCode = 2;
1816
1933
  return;
1817
1934
  }
@@ -1824,31 +1941,18 @@ async function main() {
1824
1941
  console.log(JSON.stringify(results.length === 1 ? (results[0] || { error: 'not_found' }) : results, null, 2));
1825
1942
  }
1826
1943
  process.exitCode = 0; return;
1827
- return;
1828
1944
  }
1829
1945
 
1830
1946
  if (command === 'scan') {
1831
- const deepFlag = targets.includes('--deep');
1832
1947
  const urls = targets.filter(t => !t.startsWith('--'));
1833
1948
  if (urls.length === 0) {
1834
- console.log(` ${c.red}Error: at least one repository URL required${c.reset}`);
1835
- console.log(` ${c.dim}Tip: use ${c.cyan}agentaudit discover${c.dim} to find & check locally installed MCP servers${c.reset}`);
1836
- console.log(` ${c.dim}Tip: use ${c.cyan}agentaudit audit <url>${c.dim} for a deep LLM-powered audit${c.reset}`);
1949
+ console.log(` ${c.red}✖ Repository URL required${c.reset}`);
1950
+ console.log(` ${c.dim}Usage: agentaudit scan <url>${c.reset}`);
1951
+ console.log(` ${c.dim}Or discover local servers: ${c.cyan}agentaudit discover${c.reset}`);
1837
1952
  process.exitCode = 2;
1838
1953
  return;
1839
1954
  }
1840
1955
 
1841
- // --deep redirects to audit flow
1842
- if (deepFlag) {
1843
- let hasFindings = false;
1844
- for (const url of urls) {
1845
- const report = await auditRepo(url);
1846
- if (report?.findings?.length > 0) hasFindings = true;
1847
- }
1848
- process.exitCode = hasFindings ? 1 : 0;
1849
- return;
1850
- }
1851
-
1852
1956
  const results = [];
1853
1957
  let hadErrors = false;
1854
1958
  for (const url of urls) {
@@ -1885,7 +1989,8 @@ async function main() {
1885
1989
  if (command === 'audit') {
1886
1990
  const urls = targets.filter(t => !t.startsWith('--'));
1887
1991
  if (urls.length === 0) {
1888
- console.log(` ${c.red}Error: at least one repository URL required${c.reset}`);
1992
+ console.log(` ${c.red}✖ Repository URL required${c.reset}`);
1993
+ console.log(` ${c.dim}Usage: agentaudit audit <url>${c.reset}`);
1889
1994
  process.exitCode = 2;
1890
1995
  return;
1891
1996
  }
@@ -1899,8 +2004,18 @@ async function main() {
1899
2004
  return;
1900
2005
  }
1901
2006
 
1902
- console.log(` ${c.red}Unknown command: ${command}${c.reset}`);
1903
- console.log(` ${c.dim}Run agentaudit --help for usage${c.reset}`);
2007
+ // Typo correction via Levenshtein distance
2008
+ const knownCommands = ['discover', 'scan', 'audit', 'check', 'lookup', 'status', 'setup', 'config'];
2009
+ const suggestion = knownCommands
2010
+ .map(cmd => ({ cmd, dist: levenshtein(command, cmd) }))
2011
+ .filter(x => x.dist <= 3)
2012
+ .sort((a, b) => a.dist - b.dist)[0];
2013
+
2014
+ console.log(` ${c.red}✖ Unknown command: ${command}${c.reset}`);
2015
+ if (suggestion) {
2016
+ console.log(` ${c.dim}Did you mean: ${c.cyan}agentaudit ${suggestion.cmd}${c.reset}${c.dim}?${c.reset}`);
2017
+ }
2018
+ console.log(` ${c.dim}Run ${c.cyan}agentaudit --help${c.dim} for usage${c.reset}`);
1904
2019
  process.exitCode = 2;
1905
2020
  }
1906
2021
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentaudit",
3
- "version": "3.9.18",
3
+ "version": "3.9.20",
4
4
  "description": "Security scanner for AI packages — MCP server + CLI",
5
5
  "type": "module",
6
6
  "bin": {