@oculum/cli 1.0.0 → 1.0.2

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/dist/index.js +315 -86
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -17678,7 +17678,7 @@ var require_ai_fingerprinting = __commonJS({
17678
17678
  return vulnerabilities;
17679
17679
  }
17680
17680
  const anyUsageByContext = categorizeAnyUsage(lines, filePath);
17681
- const priorityAny = anyUsageByContext.filter((usage) => usage.context === "api_boundary" || usage.context === "database_layer" || usage.context === "auth_handler");
17681
+ const priorityAny = anyUsageByContext.filter((usage2) => usage2.context === "api_boundary" || usage2.context === "database_layer" || usage2.context === "auth_handler");
17682
17682
  const cappedAny = priorityAny.slice(0, 5);
17683
17683
  if (cappedAny.length === 0) {
17684
17684
  return vulnerabilities;
@@ -17708,21 +17708,21 @@ var require_ai_fingerprinting = __commonJS({
17708
17708
  layer: 2
17709
17709
  });
17710
17710
  } else {
17711
- for (const usage of cappedAny) {
17711
+ for (const usage2 of cappedAny) {
17712
17712
  const contextNames = {
17713
17713
  "api_boundary": "API request/response handler",
17714
17714
  "database_layer": "Database query",
17715
17715
  "auth_handler": "Authentication logic"
17716
17716
  };
17717
17717
  vulnerabilities.push({
17718
- id: `ai-fingerprint-any-${filePath}-${usage.lineNumber}`,
17718
+ id: `ai-fingerprint-any-${filePath}-${usage2.lineNumber}`,
17719
17719
  filePath,
17720
- lineNumber: usage.lineNumber,
17721
- lineContent: usage.lineContent,
17720
+ lineNumber: usage2.lineNumber,
17721
+ lineContent: usage2.lineContent,
17722
17722
  severity: "low",
17723
17723
  category: "ai_pattern",
17724
- title: `[AI Pattern] TypeScript 'any' in ${contextNames[usage.context] || usage.context}`,
17725
- description: `Using 'any' type at a security boundary bypasses type checking and can lead to type confusion vulnerabilities. This is especially risky in ${contextNames[usage.context] || usage.context}.`,
17724
+ title: `[AI Pattern] TypeScript 'any' in ${contextNames[usage2.context] || usage2.context}`,
17725
+ description: `Using 'any' type at a security boundary bypasses type checking and can lead to type confusion vulnerabilities. This is especially risky in ${contextNames[usage2.context] || usage2.context}.`,
17726
17726
  suggestedFix: 'Replace "any" with an explicit type. Use typed request schemas, ORM models, or interface definitions.',
17727
17727
  confidence: "medium",
17728
17728
  layer: 2
@@ -29411,11 +29411,11 @@ var require_AbstractChatCompletionRunner = __commonJS({
29411
29411
  prompt_tokens: 0,
29412
29412
  total_tokens: 0
29413
29413
  };
29414
- for (const { usage } of this._chatCompletions) {
29415
- if (usage) {
29416
- total.completion_tokens += usage.completion_tokens;
29417
- total.prompt_tokens += usage.prompt_tokens;
29418
- total.total_tokens += usage.total_tokens;
29414
+ for (const { usage: usage2 } of this._chatCompletions) {
29415
+ if (usage2) {
29416
+ total.completion_tokens += usage2.completion_tokens;
29417
+ total.prompt_tokens += usage2.prompt_tokens;
29418
+ total.total_tokens += usage2.total_tokens;
29419
29419
  }
29420
29420
  }
29421
29421
  return total;
@@ -37001,11 +37001,11 @@ For each candidate finding, return:
37001
37001
  max_completion_tokens: 4096
37002
37002
  }));
37003
37003
  statsLock.apiCalls++;
37004
- const usage = response.usage;
37005
- if (usage) {
37006
- const promptTokens = usage.prompt_tokens || 0;
37007
- const completionTokens = usage.completion_tokens || 0;
37008
- const cachedTokens = usage.prompt_tokens_details?.cached_tokens || 0;
37004
+ const usage2 = response.usage;
37005
+ if (usage2) {
37006
+ const promptTokens = usage2.prompt_tokens || 0;
37007
+ const completionTokens = usage2.completion_tokens || 0;
37008
+ const cachedTokens = usage2.prompt_tokens_details?.cached_tokens || 0;
37009
37009
  const freshInputTokens = promptTokens - cachedTokens;
37010
37010
  statsLock.estimatedInputTokens += freshInputTokens;
37011
37011
  statsLock.estimatedOutputTokens += completionTokens;
@@ -37226,19 +37226,19 @@ For each candidate finding, return:
37226
37226
  }));
37227
37227
  stats.apiCalls++;
37228
37228
  totalApiBatches++;
37229
- const usage = response.usage;
37230
- if (usage) {
37229
+ const usage2 = response.usage;
37230
+ if (usage2) {
37231
37231
  console.log(`[DEBUG] Batch ${batchNum} - Full API Response Usage:`);
37232
- console.log(JSON.stringify(usage, null, 2));
37232
+ console.log(JSON.stringify(usage2, null, 2));
37233
37233
  console.log(`[DEBUG] Breakdown:`);
37234
- console.log(` - input_tokens: ${usage.input_tokens || 0}`);
37235
- console.log(` - output_tokens: ${usage.output_tokens || 0}`);
37236
- console.log(` - cache_creation_input_tokens: ${usage.cache_creation_input_tokens || 0}`);
37237
- console.log(` - cache_read_input_tokens: ${usage.cache_read_input_tokens || 0}`);
37238
- stats.estimatedInputTokens += usage.input_tokens || 0;
37239
- stats.estimatedOutputTokens += usage.output_tokens || 0;
37240
- const cacheCreation = usage.cache_creation_input_tokens || 0;
37241
- const cacheRead = usage.cache_read_input_tokens || 0;
37234
+ console.log(` - input_tokens: ${usage2.input_tokens || 0}`);
37235
+ console.log(` - output_tokens: ${usage2.output_tokens || 0}`);
37236
+ console.log(` - cache_creation_input_tokens: ${usage2.cache_creation_input_tokens || 0}`);
37237
+ console.log(` - cache_read_input_tokens: ${usage2.cache_read_input_tokens || 0}`);
37238
+ stats.estimatedInputTokens += usage2.input_tokens || 0;
37239
+ stats.estimatedOutputTokens += usage2.output_tokens || 0;
37240
+ const cacheCreation = usage2.cache_creation_input_tokens || 0;
37241
+ const cacheRead = usage2.cache_read_input_tokens || 0;
37242
37242
  stats.cacheCreationTokens += cacheCreation;
37243
37243
  stats.cacheReadTokens += cacheRead;
37244
37244
  }
@@ -42827,6 +42827,27 @@ async function pollForLogin(deviceCode) {
42827
42827
  tier: result.tier
42828
42828
  };
42829
42829
  }
42830
+ async function getUsage(apiKey) {
42831
+ const baseUrl = getApiBaseUrl();
42832
+ const url = `${baseUrl}/v1/usage`;
42833
+ try {
42834
+ const response = await fetch(url, {
42835
+ method: "GET",
42836
+ headers: {
42837
+ "Authorization": `Bearer ${apiKey}`,
42838
+ "X-Oculum-Client": "cli",
42839
+ "X-Oculum-Version": "1.0.0"
42840
+ }
42841
+ });
42842
+ const result = await response.json();
42843
+ return result;
42844
+ } catch (error) {
42845
+ return {
42846
+ success: false,
42847
+ error: "Failed to fetch usage data. Please check your internet connection."
42848
+ };
42849
+ }
42850
+ }
42830
42851
 
42831
42852
  // src/utils/errors.ts
42832
42853
  function enhanceError(error) {
@@ -42892,14 +42913,18 @@ function enhanceAPIError(error) {
42892
42913
  ]
42893
42914
  };
42894
42915
  case 429:
42916
+ const rateLimitInfo = error.reason === "quota_exceeded" ? "You've reached your monthly scan quota." : "Too many requests in a short period.";
42917
+ const rateLimitSuggestion = error.reason === "quota_exceeded" ? "Your quota resets at the start of next month. Upgrade to Pro for higher limits." : "Wait a moment and try again. Consider using --depth cheap for unlimited local scans.";
42895
42918
  return {
42896
- message: "Rate limit exceeded",
42897
- suggestion: "Wait a moment and try again. Upgrade to Pro for higher limits.",
42919
+ message: rateLimitInfo,
42920
+ suggestion: rateLimitSuggestion,
42898
42921
  category: "server",
42899
42922
  recoveryActions: [
42900
- { label: "Retry in 30 seconds", action: "retry" },
42901
- { label: "View pricing", action: "upgrade" }
42902
- ]
42923
+ { label: "Use free local scan", command: "oculum scan . --depth cheap", action: "fallback" },
42924
+ { label: "View usage & upgrade", action: "upgrade" },
42925
+ { label: "Retry in 30 seconds", action: "retry" }
42926
+ ],
42927
+ details: error.reason === "quota_exceeded" ? "Check your usage at https://oculum.dev/dashboard/usage" : void 0
42903
42928
  };
42904
42929
  case 500:
42905
42930
  case 502:
@@ -43443,11 +43468,17 @@ async function runScanOnce(targetPath, options) {
43443
43468
  const noColor = options.color === false;
43444
43469
  if ((options.depth === "validated" || options.depth === "deep") && !isAuthenticated()) {
43445
43470
  if (!options.quiet) {
43446
- console.log(source_default.yellow("\nNote: validated and deep scans require authentication."));
43447
- console.log(
43448
- source_default.dim("Run `oculum login` to authenticate, or use `--depth cheap` for free local scans.\n")
43449
- );
43450
- console.log(source_default.dim("Falling back to cheap scan...\n"));
43471
+ console.log("");
43472
+ console.log(source_default.yellow("\u26A0\uFE0F AI-powered scans require authentication"));
43473
+ console.log("");
43474
+ console.log(source_default.dim(" You requested: ") + source_default.white(options.depth) + source_default.dim(" scan"));
43475
+ console.log(source_default.dim(" This requires: ") + source_default.white("Pro subscription"));
43476
+ console.log("");
43477
+ console.log(source_default.dim(" Options:"));
43478
+ console.log(source_default.cyan(" \u2022 oculum login") + source_default.dim(" - Authenticate to unlock Pro features"));
43479
+ console.log(source_default.cyan(" \u2022 --depth cheap") + source_default.dim(" - Use free local pattern matching"));
43480
+ console.log("");
43481
+ console.log(source_default.dim(" Continuing with ") + source_default.green("cheap") + source_default.dim(" scan (free)...\n"));
43451
43482
  }
43452
43483
  options.depth = "cheap";
43453
43484
  }
@@ -43469,19 +43500,24 @@ async function runScanOnce(targetPath, options) {
43469
43500
  const onProgress = (progress) => {
43470
43501
  switch (progress.status) {
43471
43502
  case "layer1":
43472
- spinner.text = `Layer 1: Pattern matching... (${progress.vulnerabilitiesFound} findings)`;
43503
+ spinner.text = `Scanning patterns... ${source_default.dim(`(${progress.vulnerabilitiesFound} potential issues)`)}`;
43473
43504
  break;
43474
43505
  case "layer2":
43475
- spinner.text = `Layer 2: Structural analysis... (${progress.vulnerabilitiesFound} findings)`;
43506
+ spinner.text = `Analyzing code structure... ${source_default.dim(`(${progress.vulnerabilitiesFound} findings)`)}`;
43476
43507
  break;
43477
43508
  case "validating":
43478
- spinner.text = `AI validation... (${progress.vulnerabilitiesFound} candidates)`;
43509
+ spinner.text = `AI validating findings... ${source_default.dim(`(${progress.vulnerabilitiesFound} candidates)`)}`;
43479
43510
  break;
43480
43511
  case "layer3":
43481
- spinner.text = `Layer 3: AI semantic analysis...`;
43512
+ spinner.text = `Deep AI analysis in progress...`;
43482
43513
  break;
43483
43514
  case "complete":
43484
- spinner.succeed(`Scan complete: ${progress.vulnerabilitiesFound} issues found`);
43515
+ const issueText = progress.vulnerabilitiesFound === 1 ? "issue" : "issues";
43516
+ if (progress.vulnerabilitiesFound === 0) {
43517
+ spinner.succeed(source_default.green("Scan complete: No security issues found! \u2713"));
43518
+ } else {
43519
+ spinner.succeed(`Scan complete: ${progress.vulnerabilitiesFound} ${issueText} found`);
43520
+ }
43485
43521
  break;
43486
43522
  case "failed":
43487
43523
  spinner.fail(progress.message);
@@ -43614,23 +43650,43 @@ async function login(options) {
43614
43650
  const result = await verifyApiKey(options.apiKey);
43615
43651
  if (!result.valid) {
43616
43652
  spinner.fail("Invalid API key");
43653
+ console.log("");
43654
+ console.log(source_default.dim(" The API key could not be verified. Please check:"));
43655
+ console.log(source_default.dim(" \u2022 The key is copied correctly (no extra spaces)"));
43656
+ console.log(source_default.dim(" \u2022 The key has not expired"));
43657
+ console.log(source_default.dim(" \u2022 Generate a new key at: ") + source_default.cyan("https://oculum.dev/dashboard/api-keys"));
43658
+ console.log("");
43617
43659
  process.exit(1);
43618
43660
  }
43619
43661
  setAuthCredentials(options.apiKey, result.email, result.tier);
43620
43662
  spinner.succeed("Logged in successfully!");
43621
- console.log(source_default.dim(` Email: ${result.email}`));
43622
- console.log(source_default.dim(` Tier: ${result.tier}`));
43663
+ console.log("");
43664
+ console.log(source_default.dim(" Email: ") + source_default.white(result.email));
43665
+ console.log(source_default.dim(" Plan: ") + source_default.white(result.tier));
43666
+ console.log("");
43667
+ console.log(source_default.green(" You can now use AI-powered scans:"));
43668
+ console.log(source_default.cyan(" oculum scan . --depth validated"));
43669
+ console.log("");
43623
43670
  return;
43624
43671
  }
43625
43672
  try {
43673
+ console.log("");
43674
+ console.log(source_default.bold(" \u{1F510} Oculum Login"));
43675
+ console.log(source_default.dim(" " + "\u2500".repeat(38)));
43676
+ console.log("");
43626
43677
  spinner.start("Initiating login...");
43627
- const { authUrl, deviceCode } = await initiateLogin();
43678
+ const { authUrl, deviceCode, userCode } = await initiateLogin();
43628
43679
  spinner.stop();
43629
- console.log("\nTo complete login, open this URL in your browser:");
43630
- console.log(source_default.cyan(`
43631
- ${authUrl}`));
43632
- console.log(source_default.dim("\nWaiting for you to authenticate..."));
43633
- spinner.start("Waiting for browser authorization...");
43680
+ console.log(source_default.white(" Open this URL in your browser to login:"));
43681
+ console.log("");
43682
+ console.log(source_default.cyan.bold(` ${authUrl}`));
43683
+ console.log("");
43684
+ console.log(source_default.white(" Or enter this code manually: ") + source_default.yellow.bold(userCode));
43685
+ console.log("");
43686
+ console.log(source_default.dim(" Waiting for browser authorization..."));
43687
+ console.log(source_default.dim(" (This will timeout after 5 minutes)"));
43688
+ console.log("");
43689
+ spinner.start("Waiting for authorization...");
43634
43690
  const maxAttempts = 60;
43635
43691
  for (let i = 0; i < maxAttempts; i++) {
43636
43692
  await new Promise((resolve7) => setTimeout(resolve7, 5e3));
@@ -43638,15 +43694,35 @@ async function login(options) {
43638
43694
  if (result.complete) {
43639
43695
  setAuthCredentials(result.apiKey, result.email, result.tier);
43640
43696
  spinner.succeed("Logged in successfully!");
43641
- console.log(source_default.dim(` Email: ${result.email}`));
43642
- console.log(source_default.dim(` Tier: ${result.tier}`));
43697
+ console.log("");
43698
+ console.log(source_default.dim(" Email: ") + source_default.white(result.email));
43699
+ console.log(source_default.dim(" Plan: ") + source_default.white(result.tier));
43700
+ console.log("");
43701
+ console.log(source_default.green(" You can now use AI-powered scans:"));
43702
+ console.log(source_default.cyan(" oculum scan . --depth validated"));
43703
+ console.log("");
43643
43704
  return;
43644
43705
  }
43706
+ if (result.expired) {
43707
+ spinner.fail("Login session expired");
43708
+ console.log("");
43709
+ console.log(source_default.dim(" Please try again: ") + source_default.cyan("oculum login"));
43710
+ console.log("");
43711
+ process.exit(1);
43712
+ }
43645
43713
  }
43646
- spinner.fail("Login timed out. Please try again.");
43714
+ spinner.fail("Login timed out");
43715
+ console.log("");
43716
+ console.log(source_default.dim(" The login session expired. Please try again:"));
43717
+ console.log(source_default.cyan(" oculum login"));
43718
+ console.log("");
43647
43719
  process.exit(1);
43648
43720
  } catch (err) {
43649
- spinner.fail(`Login failed: ${err}`);
43721
+ spinner.fail("Login failed");
43722
+ console.log("");
43723
+ console.log(source_default.dim(" Error: ") + source_default.red(String(err)));
43724
+ console.log(source_default.dim(" Please check your internet connection and try again."));
43725
+ console.log("");
43650
43726
  process.exit(1);
43651
43727
  }
43652
43728
  }
@@ -43660,40 +43736,67 @@ function logout() {
43660
43736
  }
43661
43737
  async function status() {
43662
43738
  const config = getConfig();
43663
- console.log("\n" + source_default.bold("Oculum CLI Status"));
43664
- console.log(source_default.dim("\u2500".repeat(40)));
43739
+ console.log("");
43740
+ console.log(source_default.bold(" \u{1F510} Oculum Authentication Status"));
43741
+ console.log(source_default.dim(" " + "\u2500".repeat(38)));
43665
43742
  if (!isAuthenticated()) {
43666
- console.log(source_default.yellow("\nNot logged in."));
43667
- console.log(source_default.dim("\nRun `oculum login` to authenticate."));
43668
- console.log(source_default.dim("Free tier allows cheap (local) scans only."));
43669
- console.log(source_default.dim("Paid tiers unlock AI-powered validated and deep scans.\n"));
43743
+ console.log("");
43744
+ console.log(source_default.yellow(" Status: ") + source_default.white("Not logged in"));
43745
+ console.log("");
43746
+ console.log(source_default.dim(" You can use Oculum without logging in for free local scans."));
43747
+ console.log(source_default.dim(" Login to unlock AI-powered validation and deep analysis."));
43748
+ console.log("");
43749
+ console.log(source_default.bold(" Quick Start:"));
43750
+ console.log(source_default.cyan(" oculum scan .") + source_default.dim(" Free pattern-based scan"));
43751
+ console.log(source_default.cyan(" oculum login") + source_default.dim(" Authenticate for Pro features"));
43752
+ console.log("");
43670
43753
  return;
43671
43754
  }
43672
- const spinner = ora("Verifying credentials...").start();
43755
+ const spinner = ora(" Verifying credentials...").start();
43673
43756
  const result = await verifyApiKey(config.apiKey);
43674
43757
  if (!result.valid) {
43675
- spinner.fail("Stored credentials are invalid or expired.");
43676
- console.log(source_default.dim("Run `oculum login` to re-authenticate."));
43758
+ spinner.fail(" Stored credentials are invalid or expired");
43759
+ console.log("");
43760
+ console.log(source_default.dim(" Your session may have expired. Please login again:"));
43761
+ console.log(source_default.cyan(" oculum login"));
43762
+ console.log("");
43677
43763
  return;
43678
43764
  }
43679
- spinner.succeed("Authenticated");
43680
- console.log(source_default.dim(` Email: ${result.email || config.email}`));
43681
- console.log(source_default.dim(` Tier: ${result.tier || config.tier}`));
43682
- console.log("\n" + source_default.bold("Available Scan Depths:"));
43765
+ spinner.succeed(" Authenticated");
43766
+ console.log("");
43767
+ const email = result.email || config.email || "unknown";
43683
43768
  const tier = result.tier || config.tier || "free";
43684
- console.log(source_default.green(" cheap - Fast pattern matching (always available)"));
43769
+ const tierBadge = tier === "pro" ? source_default.bgBlue.white(" PRO ") : tier === "enterprise" ? source_default.bgMagenta.white(" ENTERPRISE ") : source_default.bgGray.white(" FREE ");
43770
+ console.log(source_default.dim(" Email: ") + source_default.white(email));
43771
+ console.log(source_default.dim(" Plan: ") + tierBadge);
43772
+ console.log("");
43773
+ console.log(source_default.bold(" Available Scan Depths:"));
43774
+ console.log("");
43775
+ console.log(source_default.green(" \u2713 ") + source_default.white("cheap") + source_default.dim(" Fast pattern matching (always free)"));
43685
43776
  if (tier === "pro" || tier === "enterprise") {
43686
- console.log(source_default.green(" validated - AI-powered validation"));
43687
- console.log(source_default.green(" deep - Full AI semantic analysis"));
43777
+ console.log(source_default.green(" \u2713 ") + source_default.white("validated") + source_default.dim(" AI validation (~70% fewer false positives)"));
43778
+ console.log(source_default.dim(" \u{1F512} ") + source_default.white("deep") + source_default.dim(" Multi-agent analysis (coming soon)"));
43688
43779
  } else {
43689
- console.log(source_default.dim(" validated - AI-powered validation (requires Pro)"));
43690
- console.log(source_default.dim(" deep - Full AI semantic analysis (requires Pro)"));
43780
+ console.log(source_default.dim(" \u{1F512} ") + source_default.white("validated") + source_default.dim(" AI validation (requires Pro)"));
43781
+ console.log(source_default.dim(" \u{1F512} ") + source_default.white("deep") + source_default.dim(" Multi-agent analysis (requires Pro)"));
43691
43782
  }
43692
43783
  console.log("");
43784
+ console.log(source_default.dim(" Manage subscription: ") + source_default.cyan("https://oculum.dev/billing"));
43785
+ console.log("");
43693
43786
  }
43694
43787
  function upgrade() {
43695
- console.log(source_default.cyan("\nTo upgrade your subscription, visit:"));
43696
- console.log(source_default.bold(" https://oculum.dev/billing\n"));
43788
+ console.log("");
43789
+ console.log(source_default.bold(" \u{1F680} Upgrade to Oculum Pro"));
43790
+ console.log(source_default.dim(" " + "\u2500".repeat(38)));
43791
+ console.log("");
43792
+ console.log(source_default.white(" Pro features include:"));
43793
+ console.log(source_default.green(" \u2713 ") + source_default.white("AI-validated scans") + source_default.dim(" - ~70% fewer false positives"));
43794
+ console.log(source_default.green(" \u2713 ") + source_default.white("Higher scan limits") + source_default.dim(" - More scans per month"));
43795
+ console.log(source_default.green(" \u2713 ") + source_default.white("Priority support") + source_default.dim(" - Get help when you need it"));
43796
+ console.log(source_default.green(" \u2713 ") + source_default.white("GitHub Action") + source_default.dim(" - Automated PR scanning"));
43797
+ console.log("");
43798
+ console.log(source_default.dim(" Visit: ") + source_default.cyan.underline("https://oculum.dev/pricing"));
43799
+ console.log("");
43697
43800
  }
43698
43801
  var authCommand = new Command("auth").description("Manage authentication");
43699
43802
  authCommand.command("login").description("Log in to Oculum").option("-k, --api-key <key>", "API key (skip browser auth)").action(login);
@@ -45454,18 +45557,23 @@ async function watch2(targetPath, options) {
45454
45557
  const config = getConfig();
45455
45558
  if ((options.depth === "validated" || options.depth === "deep") && !isAuthenticated()) {
45456
45559
  if (!options.quiet) {
45457
- console.log(source_default.yellow("\nNote: validated and deep scans require authentication."));
45458
- console.log(source_default.dim("Run `oculum login` to authenticate.\n"));
45560
+ console.log("");
45561
+ console.log(source_default.yellow("\u26A0\uFE0F AI-powered scans require authentication"));
45562
+ console.log(source_default.dim(" Using free local scan instead. Run `oculum login` to unlock Pro features."));
45459
45563
  }
45460
45564
  options.depth = "cheap";
45461
45565
  }
45462
45566
  if (!options.quiet) {
45463
- console.log(source_default.bold("\nOculum Watch Mode"));
45464
- console.log(source_default.dim("\u2500".repeat(40)));
45465
- console.log(source_default.dim(`Watching: ${absolutePath}`));
45466
- console.log(source_default.dim(`Depth: ${options.depth}`));
45467
- console.log(source_default.dim(`Debounce: ${options.debounce}ms`));
45468
- console.log(source_default.dim("\nPress Ctrl+C to stop\n"));
45567
+ console.log("");
45568
+ console.log(source_default.bold(" \u{1F441}\uFE0F Oculum Watch Mode"));
45569
+ console.log(source_default.dim(" " + "\u2500".repeat(38)));
45570
+ console.log("");
45571
+ console.log(source_default.dim(" Watching: ") + source_default.white(absolutePath));
45572
+ console.log(source_default.dim(" Depth: ") + source_default.white(options.depth === "cheap" ? "Quick (pattern matching)" : options.depth));
45573
+ console.log(source_default.dim(" Debounce: ") + source_default.white(`${options.debounce}ms`));
45574
+ console.log("");
45575
+ console.log(source_default.dim(" Press ") + source_default.white("Ctrl+C") + source_default.dim(" to stop watching"));
45576
+ console.log("");
45469
45577
  }
45470
45578
  const changedFiles = /* @__PURE__ */ new Set();
45471
45579
  let isScanning = false;
@@ -45478,8 +45586,9 @@ async function watch2(targetPath, options) {
45478
45586
  console.clear();
45479
45587
  }
45480
45588
  if (!options.quiet) {
45481
- console.log(source_default.cyan(`
45482
- [${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] Scanning ${filesToScan.length} changed file(s)...`));
45589
+ const fileText = filesToScan.length === 1 ? "file" : "files";
45590
+ console.log("");
45591
+ console.log(source_default.cyan(` \u27F3 [${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] Scanning ${filesToScan.length} changed ${fileText}...`));
45483
45592
  }
45484
45593
  const scanFiles = [];
45485
45594
  for (const filePath of filesToScan) {
@@ -45509,9 +45618,12 @@ async function watch2(targetPath, options) {
45509
45618
  );
45510
45619
  if (result.vulnerabilities.length === 0) {
45511
45620
  if (!options.quiet) {
45512
- console.log(source_default.green("No issues found."));
45621
+ console.log(source_default.green(" \u2713 No issues found"));
45513
45622
  }
45514
45623
  } else {
45624
+ const issueCount = result.vulnerabilities.length;
45625
+ const issueText = issueCount === 1 ? "issue" : "issues";
45626
+ console.log(source_default.yellow(` \u26A0 Found ${issueCount} ${issueText}:`));
45515
45627
  console.log((0, import_formatters2.formatTerminalOutput)(result, {
45516
45628
  maxFindingsPerGroup: 5
45517
45629
  }));
@@ -46498,6 +46610,7 @@ async function showFeatureExploration() {
46498
46610
  console.log(source_default.dim(" oculum watch . ") + source_default.white("Watch for changes"));
46499
46611
  console.log(source_default.dim(" oculum ui ") + source_default.white("Interactive mode"));
46500
46612
  console.log(source_default.dim(" oculum login ") + source_default.white("Authenticate for Pro features"));
46613
+ console.log(source_default.dim(" oculum usage ") + source_default.white("View credits and quota"));
46501
46614
  console.log(source_default.dim(" oculum --help ") + source_default.white("Show all commands\n"));
46502
46615
  await ve({
46503
46616
  message: "Press Enter to continue",
@@ -47588,6 +47701,120 @@ var uiCommand = new Command("ui").description("Interactive terminal UI").action(
47588
47701
  await runUI();
47589
47702
  });
47590
47703
 
47704
+ // src/commands/usage.ts
47705
+ function formatNumber(num) {
47706
+ if (num === -1) return "unlimited";
47707
+ return num.toLocaleString();
47708
+ }
47709
+ function createProgressBar(percentage, width = 20) {
47710
+ const filled = Math.round(percentage / 100 * width);
47711
+ const empty = width - filled;
47712
+ const bar = "\u2588".repeat(filled) + "\u2591".repeat(empty);
47713
+ if (percentage >= 90) return source_default.red(bar);
47714
+ if (percentage >= 70) return source_default.yellow(bar);
47715
+ return source_default.green(bar);
47716
+ }
47717
+ function formatDate(dateStr) {
47718
+ const date = new Date(dateStr);
47719
+ return date.toLocaleDateString("en-US", {
47720
+ month: "short",
47721
+ day: "numeric",
47722
+ year: "numeric"
47723
+ });
47724
+ }
47725
+ async function usage(options) {
47726
+ const config = getConfig();
47727
+ if (!isAuthenticated()) {
47728
+ console.log("");
47729
+ console.log(source_default.yellow(" \u26A0 Not logged in"));
47730
+ console.log("");
47731
+ console.log(source_default.dim(" Login to view your usage and quota:"));
47732
+ console.log(source_default.cyan(" oculum login"));
47733
+ console.log("");
47734
+ process.exit(1);
47735
+ }
47736
+ const spinner = ora("Fetching usage data...").start();
47737
+ try {
47738
+ const result = await getUsage(config.apiKey);
47739
+ if (!result.success || !result.usage || !result.plan) {
47740
+ spinner.fail("Failed to fetch usage data");
47741
+ console.log("");
47742
+ console.log(source_default.dim(" Error: ") + source_default.red(result.error || "Unknown error"));
47743
+ console.log("");
47744
+ process.exit(1);
47745
+ }
47746
+ spinner.stop();
47747
+ if (options.json) {
47748
+ console.log(JSON.stringify(result, null, 2));
47749
+ return;
47750
+ }
47751
+ const { plan, usage: usageData } = result;
47752
+ console.log("");
47753
+ console.log(source_default.bold(" \u{1F4CA} Oculum Usage"));
47754
+ console.log(source_default.dim(" " + "\u2500".repeat(38)));
47755
+ console.log("");
47756
+ const planBadge = plan.name === "pro" ? source_default.bgBlue.white(" PRO ") : plan.name === "enterprise" || plan.name === "max" ? source_default.bgMagenta.white(" MAX ") : plan.name === "starter" ? source_default.bgCyan.white(" STARTER ") : source_default.bgGray.white(" FREE ");
47757
+ console.log(source_default.dim(" Plan: ") + planBadge + source_default.white(` ${plan.displayName}`));
47758
+ console.log(source_default.dim(" Month: ") + source_default.white(usageData.month));
47759
+ console.log("");
47760
+ console.log(source_default.bold(" Credits Usage"));
47761
+ console.log("");
47762
+ const creditsDisplay = usageData.creditsLimit === -1 ? `${formatNumber(usageData.creditsUsed)} / unlimited` : `${formatNumber(usageData.creditsUsed)} / ${formatNumber(usageData.creditsLimit)}`;
47763
+ console.log(source_default.dim(" Used: ") + source_default.white(creditsDisplay));
47764
+ if (usageData.creditsLimit !== -1) {
47765
+ console.log(source_default.dim(" Remaining: ") + source_default.white(formatNumber(usageData.creditsRemaining)));
47766
+ console.log("");
47767
+ console.log(source_default.dim(" ") + createProgressBar(usageData.usagePercentage) + source_default.dim(` ${usageData.usagePercentage.toFixed(1)}%`));
47768
+ }
47769
+ console.log("");
47770
+ console.log(source_default.bold(" This Month"));
47771
+ console.log("");
47772
+ console.log(source_default.dim(" Scans: ") + source_default.white(formatNumber(usageData.totalScans)));
47773
+ console.log(source_default.dim(" Files: ") + source_default.white(formatNumber(usageData.totalFiles)));
47774
+ console.log(source_default.dim(" Cost: ") + source_default.white(`$${usageData.totalCostDollars.toFixed(4)}`));
47775
+ console.log("");
47776
+ console.log(source_default.dim(" Resets on: ") + source_default.white(formatDate(usageData.resetDate)));
47777
+ console.log("");
47778
+ if (result.recentScans && result.recentScans.length > 0) {
47779
+ console.log(source_default.bold(" Recent Scans"));
47780
+ console.log("");
47781
+ const recentToShow = result.recentScans.slice(0, 5);
47782
+ for (const scan of recentToShow) {
47783
+ const date = new Date(scan.createdAt);
47784
+ const timeAgo = getTimeAgo(date);
47785
+ console.log(source_default.dim(" \u2022 ") + source_default.white(scan.repoName || "unknown") + source_default.dim(` (${timeAgo})`));
47786
+ console.log(source_default.dim(" ") + source_default.dim(`${scan.filesScanned} files, ${scan.findingsCount} findings, ${scan.creditsUsed} credits`));
47787
+ }
47788
+ console.log("");
47789
+ }
47790
+ if (plan.name === "free" || plan.name === "starter") {
47791
+ console.log(source_default.dim(" " + "\u2500".repeat(38)));
47792
+ console.log("");
47793
+ console.log(source_default.dim(" Need more credits? ") + source_default.cyan("oculum upgrade"));
47794
+ console.log("");
47795
+ }
47796
+ } catch (error) {
47797
+ spinner.fail("Failed to fetch usage data");
47798
+ console.log("");
47799
+ console.log(source_default.dim(" Error: ") + source_default.red(String(error)));
47800
+ console.log("");
47801
+ process.exit(1);
47802
+ }
47803
+ }
47804
+ function getTimeAgo(date) {
47805
+ const now = /* @__PURE__ */ new Date();
47806
+ const diffMs = now.getTime() - date.getTime();
47807
+ const diffMins = Math.floor(diffMs / 6e4);
47808
+ const diffHours = Math.floor(diffMs / 36e5);
47809
+ const diffDays = Math.floor(diffMs / 864e5);
47810
+ if (diffMins < 1) return "just now";
47811
+ if (diffMins < 60) return `${diffMins}m ago`;
47812
+ if (diffHours < 24) return `${diffHours}h ago`;
47813
+ if (diffDays < 7) return `${diffDays}d ago`;
47814
+ return date.toLocaleDateString("en-US", { month: "short", day: "numeric" });
47815
+ }
47816
+ var usageCommand = new Command("usage").description("Show current usage and quota").option("--json", "Output as JSON").action(usage);
47817
+
47591
47818
  // src/index.ts
47592
47819
  var program2 = new Command();
47593
47820
  program2.name("oculum").description("AI-native security scanner for detecting vulnerabilities in LLM-generated code").version("1.0.0").addHelpText("after", `
@@ -47602,6 +47829,7 @@ Common Commands:
47602
47829
  ui Interactive terminal UI
47603
47830
  login Authenticate with Oculum
47604
47831
  status Check authentication status
47832
+ usage View credits and quota
47605
47833
 
47606
47834
  Learn More:
47607
47835
  $ oculum scan --help Detailed scan options
@@ -47614,6 +47842,7 @@ program2.addCommand(authCommand);
47614
47842
  program2.addCommand(loginCommand);
47615
47843
  program2.addCommand(statusCommand);
47616
47844
  program2.addCommand(upgradeCommand);
47845
+ program2.addCommand(usageCommand);
47617
47846
  program2.addCommand(watchCommand);
47618
47847
  program2.addCommand(uiCommand);
47619
47848
  program2.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oculum/cli",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "AI-native security scanner CLI for detecting vulnerabilities in AI-generated code, BYOK patterns, and modern web applications",
5
5
  "main": "dist/index.js",
6
6
  "bin": {