agentaudit 3.12.5 → 3.12.7

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 +186 -11
  2. package/package.json +1 -1
package/cli.mjs CHANGED
@@ -15,7 +15,8 @@
15
15
  * activity Your recent audits & findings
16
16
  * search <query> Search packages in registry
17
17
  * model [name|reset] Configure LLM provider + model
18
- * setup Log in to agentaudit.dev (for report uploads)
18
+ * login Sign in with GitHub (opens browser, auto-creates API key)
19
+ * setup Manual login — paste an API key from agentaudit.dev
19
20
  * status Show current config + auth status
20
21
  * profile Your profile — rank, points, audit stats
21
22
  * help [command] Show help
@@ -579,6 +580,117 @@ async function setupCommand() {
579
580
  console.log();
580
581
  }
581
582
 
583
+ // ── Login via GitHub Device Flow ─────────────────────────
584
+
585
+ async function loginCommand() {
586
+ console.log(` ${c.bold}AgentAudit Login${c.reset}`);
587
+ console.log(` ${c.dim}Sign in with GitHub to upload audit reports${c.reset}`);
588
+ console.log();
589
+
590
+ const existing = loadCredentials();
591
+ if (existing) {
592
+ console.log(` ${icons.safe} Already logged in as ${c.bold}${existing.agent_name}${c.reset}`);
593
+ console.log(` ${c.dim}Key: ${existing.api_key.slice(0, 12)}...${c.reset}`);
594
+ console.log();
595
+ const answer = await askQuestion(` Re-authenticate? ${c.dim}(y/N)${c.reset} `);
596
+ if (answer.toLowerCase() !== 'y') {
597
+ console.log(` ${c.dim}Keeping existing login.${c.reset}`);
598
+ return;
599
+ }
600
+ console.log();
601
+ }
602
+
603
+ // Step 1: Start device flow
604
+ process.stdout.write(` Starting login flow...`);
605
+ let deviceData;
606
+ try {
607
+ const res = await fetch(`${REGISTRY_URL}/api/auth/device`, {
608
+ method: 'POST',
609
+ headers: { 'Content-Type': 'application/json' },
610
+ signal: AbortSignal.timeout(10_000),
611
+ });
612
+ deviceData = await res.json();
613
+ if (!res.ok || !deviceData.device_code) {
614
+ console.log(` ${c.red}failed${c.reset}`);
615
+ console.log(` ${c.red}${deviceData.error || 'Could not start login flow'}${c.reset}`);
616
+ console.log(` ${c.dim}Fallback: run ${c.cyan}agentaudit setup${c.dim} to paste an API key manually${c.reset}`);
617
+ return;
618
+ }
619
+ console.log(` ${c.green}ok${c.reset}`);
620
+ } catch (err) {
621
+ console.log(` ${c.red}failed${c.reset}`);
622
+ console.log(` ${c.red}Could not reach ${REGISTRY_URL}${c.reset}`);
623
+ console.log(` ${c.dim}Fallback: run ${c.cyan}agentaudit setup${c.dim} to paste an API key manually${c.reset}`);
624
+ return;
625
+ }
626
+
627
+ // Step 2: Open browser
628
+ const verifyUrl = deviceData.verification_url;
629
+ console.log();
630
+ console.log(` ${c.bold}Open this URL in your browser:${c.reset}`);
631
+ console.log(` ${c.cyan}${verifyUrl}${c.reset}`);
632
+ console.log();
633
+
634
+ // Try to auto-open browser
635
+ try {
636
+ const openCmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
637
+ const { exec } = await import('child_process');
638
+ exec(`${openCmd} "${verifyUrl}"`);
639
+ console.log(` ${c.dim}(Browser should open automatically)${c.reset}`);
640
+ } catch {}
641
+
642
+ // Step 3: Poll for authorization
643
+ console.log(` ${c.dim}Waiting for GitHub authorization...${c.reset}`);
644
+ console.log();
645
+
646
+ const interval = (deviceData.interval || 5) * 1000;
647
+ const maxAttempts = Math.ceil((deviceData.expires_in || 900) / (interval / 1000));
648
+
649
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
650
+ await new Promise(r => setTimeout(r, interval));
651
+
652
+ try {
653
+ const res = await fetch(`${REGISTRY_URL}/api/auth/device?device_code=${deviceData.device_code}`, {
654
+ signal: AbortSignal.timeout(10_000),
655
+ });
656
+ const data = await res.json();
657
+
658
+ if (res.ok && data.api_key) {
659
+ // Success!
660
+ saveCredentials({ api_key: data.api_key, agent_name: data.agent_name });
661
+ console.log(` ${c.green}${icons.safe} Logged in as ${c.bold}${data.agent_name}${c.reset}`);
662
+ console.log(` ${c.dim}Key saved to: ${USER_CRED_FILE}${c.reset}`);
663
+ console.log();
664
+ console.log(` ${c.bold}Ready!${c.reset} You can now:`);
665
+ console.log(` ${c.dim}•${c.reset} Audit packages: ${c.cyan}agentaudit audit <repo-url>${c.reset}`);
666
+ console.log(` ${c.dim}•${c.reset} Quick scan: ${c.cyan}agentaudit scan <repo-url>${c.reset}`);
667
+ console.log(` ${c.dim}•${c.reset} Check registry: ${c.cyan}agentaudit check <name>${c.reset}`);
668
+ console.log();
669
+ return;
670
+ }
671
+
672
+ if (data.error === 'authorization_pending') {
673
+ process.stdout.write(`\r ${c.dim}Waiting... (${attempt + 1}/${maxAttempts})${c.reset} `);
674
+ continue;
675
+ }
676
+
677
+ if (data.error === 'expired_token') {
678
+ console.log(`\n ${c.red}Login expired. Run ${c.cyan}agentaudit login${c.red} again.${c.reset}`);
679
+ return;
680
+ }
681
+
682
+ // Unknown error
683
+ console.log(`\n ${c.red}${data.error || 'Unknown error'}${c.reset}`);
684
+ return;
685
+ } catch {
686
+ // Network error during poll — continue trying
687
+ continue;
688
+ }
689
+ }
690
+
691
+ console.log(`\n ${c.red}Login timed out. Run ${c.cyan}agentaudit login${c.red} again.${c.reset}`);
692
+ }
693
+
582
694
  // ── Helpers ──────────────────────────────────────────────
583
695
 
584
696
  function validateGitUrl(url) {
@@ -2587,10 +2699,43 @@ function loadAuditPrompt() {
2587
2699
  return null;
2588
2700
  }
2589
2701
 
2702
+ // Known context window sizes (input tokens) for common models
2703
+ const MODEL_CONTEXT_LIMITS = {
2704
+ 'claude-sonnet-4': 200000, 'claude-opus-4': 200000, 'claude-haiku-4': 200000,
2705
+ 'claude-3.5-sonnet': 200000, 'claude-3-haiku': 200000,
2706
+ 'gpt-4o': 128000, 'gpt-4o-mini': 128000, 'gpt-4-turbo': 128000, 'gpt-4': 8192,
2707
+ 'gemini-2.5-flash': 1048576, 'gemini-2.5-pro': 1048576, 'gemini-2.0-flash': 1048576,
2708
+ 'deepseek-chat': 64000, 'deepseek-reasoner': 64000,
2709
+ 'mistral-large': 128000, 'mistral-small': 32000,
2710
+ };
2711
+
2712
+ function estimateTokens(text) { return Math.ceil(text.length / 3.5); }
2713
+
2714
+ function checkContextLimit(model, systemPrompt, userMessage) {
2715
+ const modelKey = Object.keys(MODEL_CONTEXT_LIMITS).find(k => model.toLowerCase().includes(k.toLowerCase()));
2716
+ if (!modelKey) return null; // unknown model, skip check
2717
+ const limit = MODEL_CONTEXT_LIMITS[modelKey];
2718
+ const estimated = estimateTokens(systemPrompt) + estimateTokens(userMessage);
2719
+ if (estimated > limit * 0.9) {
2720
+ return { estimated, limit, pct: Math.round(estimated / limit * 100) };
2721
+ }
2722
+ return null;
2723
+ }
2724
+
2590
2725
  async function callLlm(llmConfig, systemPrompt, userMessage) {
2591
2726
  const apiKey = process.env[llmConfig.key];
2592
2727
  if (!apiKey) return { error: `Missing API key: ${llmConfig.key}` };
2593
2728
  const start = Date.now();
2729
+
2730
+ // Context window warning
2731
+ const ctxCheck = checkContextLimit(llmConfig.model, systemPrompt, userMessage);
2732
+ if (ctxCheck) {
2733
+ console.log(` ${c.yellow}⚠ Input ~${Math.round(ctxCheck.estimated/1000)}k tokens (${ctxCheck.pct}% of ${Math.round(ctxCheck.limit/1000)}k context window)${c.reset}`);
2734
+ if (ctxCheck.pct > 100) {
2735
+ return { error: `Input too large (~${Math.round(ctxCheck.estimated/1000)}k tokens) for ${llmConfig.model} (${Math.round(ctxCheck.limit/1000)}k context limit). Try a smaller package or a model with a larger context window.` };
2736
+ }
2737
+ }
2738
+
2594
2739
  let _text = '';
2595
2740
  try {
2596
2741
  let data;
@@ -2607,14 +2752,19 @@ async function callLlm(llmConfig, systemPrompt, userMessage) {
2607
2752
  return { error: friendly?.text || data.error.message || JSON.stringify(data.error), hint: friendly?.hint, duration: Date.now() - start };
2608
2753
  }
2609
2754
  _text = data.content?.[0]?.text || '';
2755
+ if (data.stop_reason === 'max_tokens') {
2756
+ console.log(` ${c.red}✗ Output truncated — model hit max_tokens limit (${data.usage?.output_tokens || '?'} tokens). Results may be incomplete.${c.reset}`);
2757
+ console.log(` ${c.dim} Hint: Try a model with higher output capacity, or scan a smaller package.${c.reset}`);
2758
+ }
2610
2759
  const report = extractJSON(_text);
2611
2760
  if (report) {
2612
2761
  report.audit_model = data.model || llmConfig.model;
2613
2762
  report.audit_provider = llmConfig.provider;
2614
2763
  if (data.id) report.provider_msg_id = data.id;
2615
2764
  if (data.usage) { report.input_tokens = data.usage.input_tokens; report.output_tokens = data.usage.output_tokens; }
2765
+ if (data.stop_reason === 'max_tokens') report.output_truncated = true;
2616
2766
  }
2617
- return { report, text: _text, duration: Date.now() - start };
2767
+ return { report, text: _text, duration: Date.now() - start, truncated: data.stop_reason === 'max_tokens' };
2618
2768
  } else if (llmConfig.type === 'gemini') {
2619
2769
  const res = await fetch(`${llmConfig.url}/${llmConfig.model}:generateContent?key=${apiKey}`, {
2620
2770
  method: 'POST',
@@ -2632,13 +2782,19 @@ async function callLlm(llmConfig, systemPrompt, userMessage) {
2632
2782
  return { error: friendly?.text || data.error.message || JSON.stringify(data.error), hint: friendly?.hint, duration: Date.now() - start };
2633
2783
  }
2634
2784
  _text = data.candidates?.[0]?.content?.parts?.[0]?.text || '';
2785
+ const geminiFinish = data.candidates?.[0]?.finishReason;
2786
+ if (geminiFinish === 'MAX_TOKENS') {
2787
+ console.log(` ${c.red}✗ Output truncated — model hit maxOutputTokens limit (${data.usageMetadata?.candidatesTokenCount || '?'} tokens). Results may be incomplete.${c.reset}`);
2788
+ console.log(` ${c.dim} Hint: Try a model with higher output capacity, or scan a smaller package.${c.reset}`);
2789
+ }
2635
2790
  const report = extractJSON(_text);
2636
2791
  if (report) {
2637
2792
  report.audit_model = data.modelVersion || llmConfig.model;
2638
2793
  report.audit_provider = llmConfig.provider;
2639
2794
  if (data.usageMetadata) { report.input_tokens = data.usageMetadata.promptTokenCount; report.output_tokens = data.usageMetadata.candidatesTokenCount; }
2795
+ if (geminiFinish === 'MAX_TOKENS') report.output_truncated = true;
2640
2796
  }
2641
- return { report, text: _text, duration: Date.now() - start };
2797
+ return { report, text: _text, duration: Date.now() - start, truncated: geminiFinish === 'MAX_TOKENS' };
2642
2798
  } else {
2643
2799
  const headers = { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' };
2644
2800
  if (llmConfig.provider === 'openrouter') { headers['HTTP-Referer'] = 'https://agentaudit.dev'; headers['X-Title'] = 'AgentAudit CLI'; }
@@ -2654,6 +2810,11 @@ async function callLlm(llmConfig, systemPrompt, userMessage) {
2654
2810
  return { error: friendly?.text || data.error.message || JSON.stringify(data.error), hint: friendly?.hint, duration: Date.now() - start };
2655
2811
  }
2656
2812
  _text = data.choices?.[0]?.message?.content || '';
2813
+ const oaiFinish = data.choices?.[0]?.finish_reason;
2814
+ if (oaiFinish === 'length') {
2815
+ console.log(` ${c.red}✗ Output truncated — model hit max_tokens limit (${data.usage?.completion_tokens || '?'} tokens). Results may be incomplete.${c.reset}`);
2816
+ console.log(` ${c.dim} Hint: Try a model with higher output capacity, or scan a smaller package.${c.reset}`);
2817
+ }
2657
2818
  const report = extractJSON(_text);
2658
2819
  if (report) {
2659
2820
  report.audit_model = data.model || llmConfig.model;
@@ -2661,12 +2822,13 @@ async function callLlm(llmConfig, systemPrompt, userMessage) {
2661
2822
  if (data.id) report.provider_msg_id = data.id;
2662
2823
  if (data.system_fingerprint) report.provider_fingerprint = data.system_fingerprint;
2663
2824
  if (data.usage) { report.input_tokens = data.usage.prompt_tokens; report.output_tokens = data.usage.completion_tokens; }
2825
+ if (oaiFinish === 'length') report.output_truncated = true;
2664
2826
  }
2665
- return { report, text: _text, duration: Date.now() - start };
2827
+ return { report, text: _text, duration: Date.now() - start, truncated: oaiFinish === 'length' };
2666
2828
  }
2667
2829
  } catch (err) {
2668
2830
  const dur = Date.now() - start;
2669
- if (err.name === 'TimeoutError' || err.message?.includes('timeout')) return { error: 'Request timed out (120s)', hint: 'Try again or use a faster model', duration: dur };
2831
+ if (err.name === 'TimeoutError' || err.message?.includes('timeout')) return { error: 'Request timed out (180s)', hint: 'Try again or use a faster model', duration: dur };
2670
2832
  if (err.code === 'ENOTFOUND' || err.code === 'ECONNREFUSED' || err.message?.includes('fetch failed')) return { error: `Network error: could not reach ${llmConfig.provider}`, hint: 'Check your internet connection', duration: dur };
2671
2833
  return { error: err.message, duration: dur };
2672
2834
  }
@@ -3076,7 +3238,7 @@ async function auditRepo(url) {
3076
3238
  for (const { report } of reports) await uploadReport(report, creds);
3077
3239
  console.log(` ${c.dim}Reports: ${REGISTRY_URL}/packages/${slug}${c.reset}`);
3078
3240
  } else if (!noUpload && !creds) {
3079
- console.log(` ${c.dim}Run ${c.cyan}agentaudit setup${c.dim} to upload reports to agentaudit.dev${c.reset}`);
3241
+ console.log(` ${c.dim}Run ${c.cyan}agentaudit login${c.dim} to upload reports to agentaudit.dev${c.reset}`);
3080
3242
  }
3081
3243
 
3082
3244
  console.log();
@@ -3139,6 +3301,13 @@ async function auditRepo(url) {
3139
3301
 
3140
3302
  console.log(` ${c.green}done${c.reset} ${c.dim}(${elapsed(start)})${c.reset}`);
3141
3303
 
3304
+ if (llmResult.truncated) {
3305
+ console.log();
3306
+ console.log(` ${c.yellow}⚠ WARNING: The model's output was truncated (hit token limit).${c.reset}`);
3307
+ console.log(` ${c.yellow} Some findings may be missing from this scan.${c.reset}`);
3308
+ console.log(` ${c.dim} Tip: Try a model with more output capacity or scan a smaller package.${c.reset}`);
3309
+ }
3310
+
3142
3311
  const report = llmResult.report;
3143
3312
  if (!report) {
3144
3313
  console.log(` ${c.red}Could not parse LLM response as JSON${c.reset}`);
@@ -3208,11 +3377,11 @@ async function auditRepo(url) {
3208
3377
  console.log(` ${c.dim}Report: ${REGISTRY_URL}/packages/${slug}${c.reset}`);
3209
3378
  } else {
3210
3379
  console.log(` ${c.red}invalid key${c.reset}`);
3211
- console.log(` ${c.dim}Run ${c.cyan}agentaudit setup${c.dim} to configure.${c.reset}`);
3380
+ console.log(` ${c.dim}Run ${c.cyan}agentaudit login${c.dim} to sign in.${c.reset}`);
3212
3381
  }
3213
3382
  }
3214
3383
  } else {
3215
- console.log(` ${c.dim}Run ${c.cyan}agentaudit setup${c.dim} to configure your API key and upload reports${c.reset}`);
3384
+ console.log(` ${c.dim}Run ${c.cyan}agentaudit login${c.dim} to sign in and upload reports${c.reset}`);
3216
3385
  }
3217
3386
 
3218
3387
  console.log();
@@ -3313,7 +3482,7 @@ function renderOverviewTab(data, width) {
3313
3482
  profileLines.push(sevParts.join(' ') || `${c.dim}no findings yet${c.reset}`);
3314
3483
  } else {
3315
3484
  profileLines.push(`${c.dim}Not logged in${c.reset}`);
3316
- profileLines.push(`${c.dim}Run ${c.cyan}agentaudit setup${c.dim} to create account${c.reset}`);
3485
+ profileLines.push(`${c.dim}Run ${c.cyan}agentaudit login${c.dim} to sign in${c.reset}`);
3317
3486
  }
3318
3487
 
3319
3488
  // Registry box
@@ -3534,7 +3703,7 @@ async function activityCommand(args) {
3534
3703
  } else {
3535
3704
  banner();
3536
3705
  console.log(` ${c.yellow}Not logged in${c.reset}`);
3537
- console.log(` ${c.dim}Run ${c.cyan}agentaudit setup${c.dim} to create an account${c.reset}`);
3706
+ console.log(` ${c.dim}Run ${c.cyan}agentaudit login${c.dim} to sign in${c.reset}`);
3538
3707
  }
3539
3708
  return;
3540
3709
  }
@@ -4410,7 +4579,8 @@ async function main() {
4410
4579
  console.log();
4411
4580
  console.log(` ${c.bold}CONFIGURATION${c.reset}`);
4412
4581
  console.log(` ${c.cyan}model${c.reset} Configure LLM provider + model`);
4413
- console.log(` ${c.cyan}setup${c.reset} Log in to agentaudit.dev (for report uploads)`);
4582
+ console.log(` ${c.cyan}login${c.reset} Sign in with GitHub (opens browser)`);
4583
+ console.log(` ${c.cyan}setup${c.reset} Manual login — paste API key`);
4414
4584
  console.log(` ${c.cyan}status${c.reset} Show current config + auth status`);
4415
4585
  console.log(` ${c.cyan}profile${c.reset} Your profile — rank, points, audit stats`);
4416
4586
  console.log();
@@ -4569,6 +4739,11 @@ async function main() {
4569
4739
  return;
4570
4740
  }
4571
4741
 
4742
+ if (command === 'login') {
4743
+ await loginCommand();
4744
+ return;
4745
+ }
4746
+
4572
4747
  if (command === 'status' || command === 'whoami') {
4573
4748
  // ── Status / diagnostic overview ──
4574
4749
  const config = loadLlmConfig();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentaudit",
3
- "version": "3.12.5",
3
+ "version": "3.12.7",
4
4
  "description": "Security scanner for AI packages — MCP server + CLI",
5
5
  "type": "module",
6
6
  "bin": {