agentaudit 3.10.0 → 3.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/cli.mjs +249 -110
  2. package/package.json +1 -1
package/cli.mjs CHANGED
@@ -10,7 +10,7 @@
10
10
  * agentaudit lookup <name> Look up package in registry
11
11
  * agentaudit setup Register + configure API key
12
12
  *
13
- * Global flags: --json, --quiet, --no-color
13
+ * Global flags: --json, --quiet, --no-color, --no-upload
14
14
  */
15
15
 
16
16
  import fs from 'fs';
@@ -49,6 +49,51 @@ const LLM_PROVIDERS = [
49
49
  { key: 'OPENROUTER_API_KEY', name: 'OpenRouter', provider: 'openrouter', type: 'openai', model: 'anthropic/claude-sonnet-4', url: 'https://openrouter.ai/api/v1/chat/completions' },
50
50
  ];
51
51
 
52
+ // ── Provider-specific model choices (for interactive menu) ──
53
+ const PROVIDER_MODELS = {
54
+ anthropic: [
55
+ { label: 'claude-sonnet-4-20250514', sublabel: 'fast + smart (default)', value: 'claude-sonnet-4-20250514' },
56
+ { label: 'claude-opus-4-20250514', sublabel: 'most capable', value: 'claude-opus-4-20250514' },
57
+ ],
58
+ openai: [
59
+ { label: 'gpt-4o', sublabel: 'fast multimodal (default)', value: 'gpt-4o' },
60
+ { label: 'gpt-4.1', sublabel: 'latest', value: 'gpt-4.1' },
61
+ ],
62
+ google: [
63
+ { label: 'gemini-2.5-flash', sublabel: 'fast + cheap (default)', value: 'gemini-2.5-flash' },
64
+ { label: 'gemini-2.5-pro', sublabel: 'most capable', value: 'gemini-2.5-pro' },
65
+ ],
66
+ deepseek: [
67
+ { label: 'deepseek-chat', sublabel: 'cost-effective (default)', value: 'deepseek-chat' },
68
+ ],
69
+ mistral: [
70
+ { label: 'mistral-large-latest', sublabel: 'EU-hosted (default)', value: 'mistral-large-latest' },
71
+ ],
72
+ groq: [
73
+ { label: 'llama-3.3-70b-versatile', sublabel: 'ultra-fast (default)', value: 'llama-3.3-70b-versatile' },
74
+ ],
75
+ xai: [
76
+ { label: 'grok-3', sublabel: 'real-time knowledge (default)', value: 'grok-3' },
77
+ ],
78
+ together: [
79
+ { label: 'meta-llama/Llama-3.3-70B-Instruct-Turbo', sublabel: 'open source (default)', value: 'meta-llama/Llama-3.3-70B-Instruct-Turbo' },
80
+ ],
81
+ fireworks: [
82
+ { label: 'accounts/fireworks/models/llama-v3p3-70b-instruct', sublabel: 'open source (default)', value: 'accounts/fireworks/models/llama-v3p3-70b-instruct' },
83
+ ],
84
+ cerebras: [
85
+ { label: 'llama-3.3-70b', sublabel: 'fast inference (default)', value: 'llama-3.3-70b' },
86
+ ],
87
+ zhipu: [
88
+ { label: 'glm-4.7', sublabel: 'Chinese language (default)', value: 'glm-4.7' },
89
+ ],
90
+ openrouter: [
91
+ { label: 'anthropic/claude-sonnet-4', sublabel: 'default', value: 'anthropic/claude-sonnet-4' },
92
+ { label: 'qwen/qwen3-coder', sublabel: 'code specialist', value: 'qwen/qwen3-coder' },
93
+ { label: 'meta-llama/Llama-3.3-70B', sublabel: 'open source', value: 'meta-llama/Llama-3.3-70B' },
94
+ ],
95
+ };
96
+
52
97
  // ── ANSI Colors (respects NO_COLOR and --no-color) ───────
53
98
 
54
99
  const noColor = !!(process.env.NO_COLOR || process.argv.includes('--no-color'));
@@ -114,20 +159,23 @@ function loadLlmConfig() {
114
159
  if (fs.existsSync(f)) {
115
160
  try {
116
161
  const data = JSON.parse(fs.readFileSync(f, 'utf8'));
117
- if (data.llm_model) return { llm_model: data.llm_model };
162
+ if (data.llm_model || data.preferred_provider) {
163
+ return { llm_model: data.llm_model || null, preferred_provider: data.preferred_provider || null };
164
+ }
118
165
  } catch {}
119
166
  }
120
167
  }
121
168
  return null;
122
169
  }
123
170
 
124
- function saveLlmConfig(model) {
171
+ function saveLlmConfig(model, provider) {
125
172
  // Merge into existing credentials
126
173
  let existing = {};
127
174
  if (fs.existsSync(USER_CRED_FILE)) {
128
175
  try { existing = JSON.parse(fs.readFileSync(USER_CRED_FILE, 'utf8')); } catch {}
129
176
  }
130
- existing.llm_model = model;
177
+ if (model !== undefined) existing.llm_model = model;
178
+ if (provider !== undefined) existing.preferred_provider = provider;
131
179
  const json = JSON.stringify(existing, null, 2);
132
180
  fs.mkdirSync(USER_CRED_DIR, { recursive: true });
133
181
  fs.writeFileSync(USER_CRED_FILE, json, { mode: 0o600 });
@@ -136,12 +184,32 @@ function saveLlmConfig(model) {
136
184
  if (fs.existsSync(SKILL_CRED_FILE)) {
137
185
  try { skillExisting = JSON.parse(fs.readFileSync(SKILL_CRED_FILE, 'utf8')); } catch {}
138
186
  }
139
- skillExisting.llm_model = model;
187
+ if (model !== undefined) skillExisting.llm_model = model;
188
+ if (provider !== undefined) skillExisting.preferred_provider = provider;
140
189
  fs.mkdirSync(path.dirname(SKILL_CRED_FILE), { recursive: true });
141
190
  fs.writeFileSync(SKILL_CRED_FILE, JSON.stringify(skillExisting, null, 2), { mode: 0o600 });
142
191
  } catch {}
143
192
  }
144
193
 
194
+ function resolveProvider() {
195
+ const config = loadLlmConfig();
196
+ const preferred = config?.preferred_provider;
197
+
198
+ if (preferred) {
199
+ // Find provider by name, check if any of their keys is set
200
+ const match = LLM_PROVIDERS.find(p => p.provider === preferred && process.env[p.key]);
201
+ if (match) return match;
202
+ // Key missing for preferred provider — warn + fallback
203
+ const providerInfo = LLM_PROVIDERS.find(p => p.provider === preferred);
204
+ if (providerInfo && !quietMode) {
205
+ console.log(` ${c.yellow}Preferred provider "${providerInfo.name}" missing key (${providerInfo.key}), falling back...${c.reset}`);
206
+ }
207
+ }
208
+
209
+ // Fallback: first match wins
210
+ return LLM_PROVIDERS.find(p => process.env[p.key]) || null;
211
+ }
212
+
145
213
  function saveCredentials(data) {
146
214
  const json = JSON.stringify(data, null, 2);
147
215
  fs.mkdirSync(USER_CRED_DIR, { recursive: true });
@@ -398,31 +466,17 @@ async function setupCommand() {
398
466
 
399
467
  console.log();
400
468
 
401
- // ── LLM model configuration ──
469
+ // ── LLM configuration hint ──
402
470
  const llmConfig = loadLlmConfig();
403
- console.log(` ${c.bold}LLM Model Configuration${c.reset}`);
404
- console.log(` ${c.dim}Used for deep audits. Requires an LLM API key in your environment.${c.reset}`);
405
- console.log();
406
- if (llmConfig) {
407
- console.log(` ${icons.safe} Current model: ${c.bold}${llmConfig.llm_model}${c.reset}`);
408
- console.log();
409
- }
410
- console.log(` ${c.dim}Examples:${c.reset}`);
411
- console.log(` ${c.dim}claude-sonnet-4-20250514${c.reset} ${c.dim}(Anthropic)${c.reset}`);
412
- console.log(` ${c.dim}gpt-4o${c.reset} ${c.dim}(OpenAI)${c.reset}`);
413
- console.log(` ${c.dim}gemini-2.5-flash${c.reset} ${c.dim}(Google)${c.reset}`);
414
- console.log(` ${c.dim}qwen/qwen3.5-coder-32b${c.reset} ${c.dim}(OpenRouter)${c.reset}`);
415
- console.log(` ${c.dim}deepseek-chat${c.reset} ${c.dim}(DeepSeek)${c.reset}`);
416
- console.log();
417
- const modelAnswer = await askQuestion(` Model ${c.dim}(Enter to keep default, or type model name)${c.reset}: `);
418
- if (modelAnswer) {
419
- saveLlmConfig(modelAnswer);
420
- console.log(` ${icons.safe} Model set to ${c.bold}${modelAnswer}${c.reset}`);
421
- } else if (llmConfig) {
422
- console.log(` ${c.dim}Keeping: ${llmConfig.llm_model}${c.reset}`);
423
- } else {
424
- console.log(` ${c.dim}No custom model — will use provider default${c.reset}`);
425
- }
471
+ console.log(` ${c.bold}LLM Configuration${c.reset}`);
472
+ if (llmConfig?.llm_model || llmConfig?.preferred_provider) {
473
+ const parts = [];
474
+ if (llmConfig.preferred_provider) parts.push(llmConfig.preferred_provider);
475
+ if (llmConfig.llm_model) parts.push(llmConfig.llm_model);
476
+ console.log(` ${icons.safe} Current: ${c.bold}${parts.join(' → ')}${c.reset}`);
477
+ }
478
+ console.log(` ${c.dim}Run ${c.cyan}agentaudit model${c.dim} to configure your LLM provider and model.${c.reset}`);
479
+ console.log(` ${c.dim}Deep audits require an LLM API key in your environment.${c.reset}`);
426
480
  console.log();
427
481
 
428
482
  console.log(` ${c.bold}Ready!${c.reset} You can now:`);
@@ -1513,8 +1567,8 @@ async function auditRepo(url) {
1513
1567
  console.log(` ${c.green}done${c.reset}`);
1514
1568
 
1515
1569
  // Step 4: LLM Analysis
1516
- // Detect provider from environment variables (first match wins)
1517
- const activeLlm = LLM_PROVIDERS.find(p => process.env[p.key]);
1570
+ // Resolve provider: preferred_provider from config first match fallback
1571
+ const activeLlm = resolveProvider();
1518
1572
  const llmApiKey = activeLlm ? process.env[activeLlm.key] : null;
1519
1573
  const activeProvider = activeLlm ? activeLlm.name : null;
1520
1574
 
@@ -1529,34 +1583,16 @@ async function auditRepo(url) {
1529
1583
  }
1530
1584
 
1531
1585
  if (!activeLlm) {
1532
- // No LLM API key — clear explanation
1586
+ // No LLM API key — compact explanation
1533
1587
  console.log();
1534
1588
  console.log(` ${c.yellow}No LLM API key found.${c.reset} The ${c.bold}audit${c.reset} command needs an LLM to analyze code.`);
1535
1589
  console.log();
1536
- console.log(` ${c.bold}Option 1: Set an API key${c.reset} (any one of these):`);
1537
- console.log(` ${c.cyan}ANTHROPIC_API_KEY${c.reset} Anthropic Claude`);
1538
- console.log(` ${c.cyan}OPENAI_API_KEY${c.reset} OpenAI GPT-4o`);
1539
- console.log(` ${c.cyan}GEMINI_API_KEY${c.reset} Google Gemini`);
1540
- console.log(` ${c.cyan}DEEPSEEK_API_KEY${c.reset} DeepSeek`);
1541
- console.log(` ${c.cyan}MISTRAL_API_KEY${c.reset} Mistral`);
1542
- console.log(` ${c.cyan}XAI_API_KEY${c.reset} xAI Grok`);
1543
- console.log(` ${c.cyan}GROQ_API_KEY${c.reset} Groq`);
1544
- console.log(` ${c.cyan}TOGETHER_API_KEY${c.reset} Together AI`);
1545
- console.log(` ${c.cyan}FIREWORKS_API_KEY${c.reset} Fireworks AI`);
1546
- console.log(` ${c.cyan}CEREBRAS_API_KEY${c.reset} Cerebras`);
1547
- console.log(` ${c.cyan}ZAI_API_KEY${c.reset} Zhipu AI (GLM)`);
1548
- console.log(` ${c.cyan}OPENROUTER_API_KEY${c.reset} OpenRouter (any model)`);
1549
- console.log();
1550
- console.log(` ${c.dim}export ANTHROPIC_API_KEY=sk-ant-...${c.reset}`);
1551
- console.log(` ${c.dim}export OPENAI_API_KEY=sk-...${c.reset}`);
1590
+ console.log(` ${c.bold}Set an API key${c.reset} (e.g. ${c.cyan}export OPENROUTER_API_KEY=sk-or-...${c.reset})`);
1591
+ console.log(` ${c.dim}Run "agentaudit model" to configure provider + model interactively${c.reset}`);
1552
1592
  console.log();
1553
- console.log(` ${c.bold}Option 2: Export for manual review${c.reset}`);
1554
- console.log(` ${c.cyan}agentaudit audit ${url} --export${c.reset}`);
1555
- console.log(` ${c.dim}Creates a markdown file you can paste into any LLM${c.reset}`);
1556
- console.log();
1557
- console.log(` ${c.bold}Option 3: Use MCP in Claude/Cursor/Windsurf (no API key needed)${c.reset}`);
1558
- console.log(` ${c.dim}Add AgentAudit as MCP server — your editor's agent runs the audit using its own LLM.${c.reset}`);
1559
- console.log(` ${c.dim}Config: { "mcpServers": { "agentaudit": { "command": "npx", "args": ["-y", "agentaudit"] } } }${c.reset}`);
1593
+ console.log(` ${c.bold}Or export for manual review:${c.reset} ${c.cyan}agentaudit audit ${url} --export${c.reset}`);
1594
+ console.log(` ${c.bold}Or use as MCP server${c.reset} in Cursor/Claude ${c.dim}(no extra API key needed)${c.reset}`);
1595
+ console.log(` ${c.dim}{ "agentaudit": { "command": "npx", "args": ["-y", "agentaudit"] } }${c.reset}`);
1560
1596
  console.log();
1561
1597
 
1562
1598
  // Check if --export flag
@@ -1793,9 +1829,12 @@ async function auditRepo(url) {
1793
1829
  console.log();
1794
1830
  }
1795
1831
 
1796
- // Upload to registry
1797
- const creds = loadCredentials();
1798
- if (creds) {
1832
+ // Upload to registry (skip with --no-upload)
1833
+ const noUpload = process.argv.includes('--no-upload');
1834
+ let creds = loadCredentials();
1835
+ if (noUpload) {
1836
+ // Skip silently
1837
+ } else if (creds) {
1799
1838
  process.stdout.write(` Uploading report to registry...`);
1800
1839
  try {
1801
1840
  const res = await fetch(`${REGISTRY_URL}/api/reports`, {
@@ -1817,8 +1856,51 @@ async function auditRepo(url) {
1817
1856
  } catch (err) {
1818
1857
  console.log(` ${c.yellow}failed${c.reset}`);
1819
1858
  }
1859
+ } else if (process.stdin.isTTY) {
1860
+ // No credentials — offer inline registration
1861
+ console.log();
1862
+ console.log(` ${c.bold}Share this audit with the community?${c.reset}`);
1863
+ console.log(` ${c.dim}Uploading helps others assess package security. Account is free.${c.reset}`);
1864
+ console.log();
1865
+ console.log(` ${c.bold}1)${c.reset} Create account + upload ${c.dim}(free)${c.reset}`);
1866
+ console.log(` ${c.bold}2)${c.reset} Skip`);
1867
+ console.log();
1868
+ const uploadChoice = await askQuestion(` Choice ${c.dim}(1/2)${c.reset}: `);
1869
+ if (uploadChoice === '1') {
1870
+ const name = await askQuestion(` Agent name ${c.dim}(e.g. my-scanner, claude-desktop)${c.reset}: `);
1871
+ if (!name || !/^[a-zA-Z0-9._-]{2,64}$/.test(name)) {
1872
+ console.log(` ${c.red}Invalid name. Use 2-64 chars: letters, numbers, dash, underscore, dot.${c.reset}`);
1873
+ } else {
1874
+ try {
1875
+ process.stdout.write(` Registering ${c.bold}${name}${c.reset}...`);
1876
+ const regData = await registerAgent(name);
1877
+ saveCredentials({ api_key: regData.api_key, agent_name: regData.agent_name });
1878
+ console.log(` ${c.green}done!${c.reset}`);
1879
+ creds = { api_key: regData.api_key, agent_name: regData.agent_name };
1880
+ process.stdout.write(` Uploading report...`);
1881
+ const res = await fetch(`${REGISTRY_URL}/api/reports`, {
1882
+ method: 'POST',
1883
+ headers: {
1884
+ 'Authorization': `Bearer ${creds.api_key}`,
1885
+ 'Content-Type': 'application/json',
1886
+ },
1887
+ body: JSON.stringify(report),
1888
+ signal: AbortSignal.timeout(15_000),
1889
+ });
1890
+ if (res.ok) {
1891
+ console.log(` ${c.green}done${c.reset}`);
1892
+ console.log(` ${c.dim}Report: ${REGISTRY_URL}/skills/${slug}${c.reset}`);
1893
+ } else {
1894
+ console.log(` ${c.yellow}failed (HTTP ${res.status})${c.reset}`);
1895
+ }
1896
+ } catch (err) {
1897
+ console.log(` ${c.red}failed${c.reset}`);
1898
+ console.log(` ${c.dim}${err.message}${c.reset}`);
1899
+ }
1900
+ }
1901
+ }
1820
1902
  } else {
1821
- console.log(` ${c.dim}Run ${c.cyan}agentaudit setup${c.dim} to upload reports to the registry${c.reset}`);
1903
+ console.log(` ${c.dim}Run ${c.cyan}agentaudit setup${c.dim} to create an account and upload reports${c.reset}`);
1822
1904
  }
1823
1905
 
1824
1906
  console.log();
@@ -1871,7 +1953,7 @@ async function main() {
1871
1953
  // --no-color already handled at top level for `c` object
1872
1954
 
1873
1955
  // Strip global flags from args (including --model <value>)
1874
- const globalFlags = new Set(['--json', '--quiet', '-q', '--no-color']);
1956
+ const globalFlags = new Set(['--json', '--quiet', '-q', '--no-color', '--no-upload']);
1875
1957
  let args = rawArgs.filter(a => !globalFlags.has(a));
1876
1958
  // Remove --model <value> pair
1877
1959
  const modelIdx = args.indexOf('--model');
@@ -1894,14 +1976,16 @@ async function main() {
1894
1976
  console.log(` ${c.cyan}agentaudit scan${c.reset} <url> ${c.dim}--deep${c.reset} Deep audit (same as audit)`);
1895
1977
  console.log(` ${c.cyan}agentaudit audit${c.reset} <url> [url...] Deep LLM-powered security audit`);
1896
1978
  console.log(` ${c.cyan}agentaudit lookup${c.reset} <name> Look up package in registry`);
1897
- console.log(` ${c.cyan}agentaudit model${c.reset} [name|reset] View or set default LLM model`);
1898
- console.log(` ${c.cyan}agentaudit setup${c.reset} Register + configure API key`);
1979
+ console.log(` ${c.cyan}agentaudit model${c.reset} Configure LLM provider + model interactively`);
1980
+ console.log(` ${c.cyan}agentaudit model${c.reset} <name> Set model directly (e.g. gpt-4o)`);
1981
+ console.log(` ${c.cyan}agentaudit setup${c.reset} Create account / enter API key (for report uploads)`);
1899
1982
  console.log();
1900
1983
  console.log(` ${c.bold}Global flags:${c.reset}`);
1901
1984
  console.log(` ${c.dim}--json Output JSON to stdout (machine-readable)${c.reset}`);
1902
1985
  console.log(` ${c.dim}--quiet Suppress banner and tree visualization${c.reset}`);
1903
1986
  console.log(` ${c.dim}--no-color Disable ANSI colors (also: NO_COLOR env)${c.reset}`);
1904
- console.log(` ${c.dim}--model <name> Override LLM model (e.g. qwen/qwen3.5-coder-32b)${c.reset}`);
1987
+ console.log(` ${c.dim}--model <name> Override LLM model for this run${c.reset}`);
1988
+ console.log(` ${c.dim}--no-upload Skip uploading report to registry${c.reset}`);
1905
1989
  console.log();
1906
1990
  console.log(` ${c.bold}Quick Scan${c.reset} vs ${c.bold}Deep Audit${c.reset}:`);
1907
1991
  console.log(` ${c.dim}scan = fast regex-based static analysis (~2s)${c.reset}`);
@@ -1917,14 +2001,11 @@ async function main() {
1917
2001
  console.log(` agentaudit audit https://github.com/owner/repo`);
1918
2002
  console.log(` agentaudit lookup fastmcp --json`);
1919
2003
  console.log();
1920
- console.log(` ${c.bold}For deep audits,${c.reset} set any LLM API key:`);
1921
- console.log(` ${c.dim}ANTHROPIC_API_KEY, OPENAI_API_KEY, GEMINI_API_KEY, DEEPSEEK_API_KEY,${c.reset}`);
1922
- console.log(` ${c.dim}MISTRAL_API_KEY, XAI_API_KEY, GROQ_API_KEY, TOGETHER_API_KEY,${c.reset}`);
1923
- console.log(` ${c.dim}FIREWORKS_API_KEY, CEREBRAS_API_KEY, ZAI_API_KEY, or OPENROUTER_API_KEY${c.reset}`);
2004
+ console.log(` ${c.bold}For deep audits,${c.reset} set an LLM API key (e.g. ${c.cyan}export OPENROUTER_API_KEY=sk-or-...${c.reset})`);
2005
+ console.log(` ${c.dim}Run "agentaudit model" to configure provider + model interactively${c.reset}`);
1924
2006
  console.log();
1925
2007
  console.log(` ${c.bold}Or use as MCP server${c.reset} in Cursor/Claude ${c.dim}(no extra API key needed):${c.reset}`);
1926
- console.log(` ${c.dim}Add to your MCP config:${c.reset}`);
1927
- console.log(` ${c.dim}{ "agentaudit": { "command": "npx", "args": ["-y", "agentaudit"] } }${c.reset}`);
2008
+ console.log(` ${c.dim}{ "agentaudit": { "command": "npx", "args": ["-y", "agentaudit"] } }${c.reset}`);
1928
2009
  console.log();
1929
2010
  process.exitCode = 0; return;
1930
2011
  }
@@ -1942,23 +2023,27 @@ async function main() {
1942
2023
 
1943
2024
  if (command === 'model') {
1944
2025
  const newModel = targets.filter(t => !t.startsWith('--'))[0];
1945
- const current = loadLlmConfig()?.llm_model;
2026
+ const config = loadLlmConfig();
2027
+ const current = config?.llm_model;
2028
+ const currentProvider = config?.preferred_provider;
1946
2029
 
1947
- // Direct set: agentaudit model <name>
2030
+ // Direct set: agentaudit model reset
1948
2031
  if (newModel === 'reset' || newModel === 'clear') {
1949
2032
  for (const f of [USER_CRED_FILE, SKILL_CRED_FILE]) {
1950
2033
  if (fs.existsSync(f)) {
1951
2034
  try {
1952
2035
  const data = JSON.parse(fs.readFileSync(f, 'utf8'));
1953
2036
  delete data.llm_model;
2037
+ delete data.preferred_provider;
1954
2038
  fs.writeFileSync(f, JSON.stringify(data, null, 2), { mode: 0o600 });
1955
2039
  } catch {}
1956
2040
  }
1957
2041
  }
1958
- console.log(` ${icons.safe} Model reset to provider default`);
2042
+ console.log(` ${icons.safe} Model + provider reset to defaults`);
1959
2043
  return;
1960
2044
  }
1961
2045
 
2046
+ // Direct set: agentaudit model <name> (sets model only, keeps provider)
1962
2047
  if (newModel) {
1963
2048
  saveLlmConfig(newModel);
1964
2049
  console.log(` ${icons.safe} Model set to ${c.bold}${newModel}${c.reset}`);
@@ -1966,82 +2051,136 @@ async function main() {
1966
2051
  return;
1967
2052
  }
1968
2053
 
1969
- // No argument — interactive menu
1970
- const activeLlm = LLM_PROVIDERS.find(p => process.env[p.key]);
1971
- console.log(` ${c.bold}LLM Model Configuration${c.reset}`);
2054
+ // ── No argument — interactive two-step menu ──
2055
+
2056
+ // Build deduplicated provider list
2057
+ const seen = new Set();
2058
+ const providerList = [];
2059
+ for (const p of LLM_PROVIDERS) {
2060
+ if (seen.has(p.provider)) continue;
2061
+ seen.add(p.provider);
2062
+ // Check if ANY key for this provider is set
2063
+ const keys = LLM_PROVIDERS.filter(x => x.provider === p.provider);
2064
+ const hasKey = keys.some(x => process.env[x.key]);
2065
+ const keyName = p.key;
2066
+ providerList.push({
2067
+ label: p.name,
2068
+ sublabel: hasKey
2069
+ ? `${keyName} ${c.green}✔${c.reset}`
2070
+ : `${keyName} ${c.red}✗${c.reset} ${c.dim}set: export ${keyName}=...${c.reset}`,
2071
+ value: p.provider,
2072
+ hasKey,
2073
+ keyName,
2074
+ });
2075
+ }
2076
+
2077
+ // Show status
2078
+ const resolved = resolveProvider();
2079
+ console.log(` ${c.bold}LLM Configuration${c.reset}`);
1972
2080
  console.log();
1973
- if (current) {
1974
- console.log(` Current: ${c.bold}${c.cyan}${current}${c.reset}`);
2081
+ if (currentProvider && current) {
2082
+ const provInfo = LLM_PROVIDERS.find(p => p.provider === currentProvider);
2083
+ console.log(` Active: ${c.bold}${c.cyan}${provInfo?.name || currentProvider} → ${current}${c.reset}`);
2084
+ } else if (current) {
2085
+ console.log(` Active: ${c.bold}${c.cyan}${resolved?.name || 'auto'} → ${current}${c.reset}`);
2086
+ } else if (resolved) {
2087
+ console.log(` Active: ${c.dim}${resolved.name} → ${resolved.model} (defaults)${c.reset}`);
1975
2088
  } else {
1976
- console.log(` Current: ${c.dim}(provider default${activeLlm ? `: ${activeLlm.model}` : ''})${c.reset}`);
1977
- }
1978
- if (activeLlm) {
1979
- console.log(` Provider: ${c.dim}${activeLlm.name} (${activeLlm.key})${c.reset}`);
2089
+ console.log(` Active: ${c.yellow}no provider configured${c.reset}`);
1980
2090
  }
1981
2091
  console.log();
1982
2092
 
1983
- // Build menu items — deduplicate providers, show popular models
2093
+ // Step A: Provider selection
2094
+ const providerChoices = [
2095
+ ...providerList,
2096
+ { label: '(keep current)', sublabel: '', value: '__keep__', hasKey: true },
2097
+ ];
2098
+
2099
+ const selectedProvider = await singleSelect(providerChoices, {
2100
+ title: 'Choose provider',
2101
+ hint: '↑↓ move Enter select Esc cancel',
2102
+ });
2103
+
2104
+ if (selectedProvider === null) {
2105
+ console.log(` ${c.dim}Cancelled${c.reset}`);
2106
+ return;
2107
+ }
2108
+
2109
+ if (selectedProvider === '__keep__') {
2110
+ console.log(` ${c.dim}Keeping current config${c.reset}`);
2111
+ return;
2112
+ }
2113
+
2114
+ // Check if provider has key set
2115
+ const chosenEntry = providerList.find(p => p.value === selectedProvider);
2116
+ if (chosenEntry && !chosenEntry.hasKey) {
2117
+ console.log();
2118
+ console.log(` ${c.yellow}${chosenEntry.keyName} is not set.${c.reset}`);
2119
+ console.log(` ${c.dim}Run: export ${chosenEntry.keyName}=your-key-here${c.reset}`);
2120
+ console.log(` ${c.dim}Then run "agentaudit model" again.${c.reset}`);
2121
+ return;
2122
+ }
2123
+
2124
+ // Step B: Model selection for chosen provider
2125
+ console.log();
2126
+ const providerModels = PROVIDER_MODELS[selectedProvider] || [];
1984
2127
  const modelChoices = [
1985
- { label: 'claude-sonnet-4-20250514', sublabel: 'Anthropic — fast + smart', value: 'claude-sonnet-4-20250514' },
1986
- { label: 'claude-opus-4-20250514', sublabel: 'Anthropic — most capable', value: 'claude-opus-4-20250514' },
1987
- { label: 'gpt-4o', sublabel: 'OpenAI — fast multimodal', value: 'gpt-4o' },
1988
- { label: 'gpt-4.1', sublabel: 'OpenAI — latest', value: 'gpt-4.1' },
1989
- { label: 'gemini-2.5-flash', sublabel: 'Google — fast + cheap', value: 'gemini-2.5-flash' },
1990
- { label: 'gemini-2.5-pro', sublabel: 'Google — most capable', value: 'gemini-2.5-pro' },
1991
- { label: 'deepseek-chat', sublabel: 'DeepSeek — cost-effective', value: 'deepseek-chat' },
1992
- { label: 'qwen/qwen3-coder', sublabel: 'OpenRouter — code specialist', value: 'qwen/qwen3-coder' },
1993
- { label: 'mistral-large-latest', sublabel: 'Mistral — EU-hosted', value: 'mistral-large-latest' },
1994
- { label: 'grok-3', sublabel: 'xAI — real-time knowledge', value: 'grok-3' },
1995
- { label: 'meta-llama/Llama-3.3-70B-Instruct-Turbo', sublabel: 'Together AI — open source', value: 'meta-llama/Llama-3.3-70B-Instruct-Turbo' },
1996
- { label: 'llama-3.3-70b-versatile', sublabel: 'Groq — ultra-fast inference', value: 'llama-3.3-70b-versatile' },
1997
- { label: '(reset to provider default)', sublabel: '', value: '__reset__' },
1998
- { label: '(enter custom model name)', sublabel: '', value: '__custom__' },
2128
+ ...providerModels,
2129
+ { label: '(enter custom model name)', sublabel: '', value: '__custom__' },
2130
+ { label: '(reset to provider default)', sublabel: '', value: '__reset__' },
1999
2131
  ];
2000
2132
 
2001
- const selected = await singleSelect(modelChoices, {
2002
- title: 'Choose default model',
2003
- hint: '↑↓=move Enter=select Esc=cancel',
2133
+ const providerName = LLM_PROVIDERS.find(p => p.provider === selectedProvider)?.name || selectedProvider;
2134
+ const selectedModel = await singleSelect(modelChoices, {
2135
+ title: `Choose model for ${providerName}`,
2136
+ hint: '↑↓ move Enter select Esc cancel',
2004
2137
  });
2005
2138
 
2006
- if (selected === null) {
2139
+ if (selectedModel === null) {
2007
2140
  console.log(` ${c.dim}Cancelled${c.reset}`);
2008
2141
  return;
2009
2142
  }
2010
2143
 
2011
- if (selected === '__reset__') {
2144
+ if (selectedModel === '__reset__') {
2145
+ // Save provider, clear model (use provider default)
2146
+ saveLlmConfig(undefined, selectedProvider);
2147
+ // Also clear llm_model from both files
2012
2148
  for (const f of [USER_CRED_FILE, SKILL_CRED_FILE]) {
2013
2149
  if (fs.existsSync(f)) {
2014
2150
  try {
2015
2151
  const data = JSON.parse(fs.readFileSync(f, 'utf8'));
2016
2152
  delete data.llm_model;
2153
+ data.preferred_provider = selectedProvider;
2017
2154
  fs.writeFileSync(f, JSON.stringify(data, null, 2), { mode: 0o600 });
2018
2155
  } catch {}
2019
2156
  }
2020
2157
  }
2158
+ const defaultModel = LLM_PROVIDERS.find(p => p.provider === selectedProvider)?.model;
2021
2159
  console.log();
2022
- console.log(` ${icons.safe} Model reset to provider default`);
2160
+ console.log(` ${icons.safe} Provider: ${c.bold}${providerName}${c.reset}, model: ${c.dim}${defaultModel} (default)${c.reset}`);
2023
2161
  return;
2024
2162
  }
2025
2163
 
2026
- if (selected === '__custom__') {
2164
+ if (selectedModel === '__custom__') {
2027
2165
  console.log();
2028
2166
  const custom = await askQuestion(` Model name: `);
2029
2167
  if (!custom) {
2030
2168
  console.log(` ${c.dim}Cancelled${c.reset}`);
2031
2169
  return;
2032
2170
  }
2033
- saveLlmConfig(custom);
2034
- console.log(` ${icons.safe} Model set to ${c.bold}${custom}${c.reset}`);
2035
- if (current) console.log(` ${c.dim}Was: ${current}${c.reset}`);
2171
+ saveLlmConfig(custom, selectedProvider);
2172
+ console.log(` ${icons.safe} Provider: ${c.bold}${providerName}${c.reset}, model: ${c.bold}${custom}${c.reset}`);
2173
+ if (current) console.log(` ${c.dim}Was: ${currentProvider || 'auto'} → ${current}${c.reset}`);
2036
2174
  return;
2037
2175
  }
2038
2176
 
2039
- saveLlmConfig(selected);
2177
+ // Normal model selection
2178
+ saveLlmConfig(selectedModel, selectedProvider);
2040
2179
  console.log();
2041
- console.log(` ${icons.safe} Model set to ${c.bold}${selected}${c.reset}`);
2042
- if (current) console.log(` ${c.dim}Was: ${current}${c.reset}`);
2180
+ console.log(` ${icons.safe} Provider: ${c.bold}${providerName}${c.reset}, model: ${c.bold}${selectedModel}${c.reset}`);
2181
+ if (current) console.log(` ${c.dim}Was: ${currentProvider || 'auto'} → ${current}${c.reset}`);
2043
2182
  console.log();
2044
- console.log(` ${c.dim}Tip: agentaudit model <name> to set directly, or agentaudit model reset${c.reset}`);
2183
+ console.log(` ${c.dim}Tip: agentaudit model <name> to set model directly, or agentaudit model reset${c.reset}`);
2045
2184
  return;
2046
2185
  }
2047
2186
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentaudit",
3
- "version": "3.10.0",
3
+ "version": "3.10.1",
4
4
  "description": "Security scanner for AI packages — MCP server + CLI",
5
5
  "type": "module",
6
6
  "bin": {