@oculum/cli 1.0.1 → 1.0.3

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 +343 -151
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -14406,6 +14406,44 @@ var require_layer1 = __commonJS({
14406
14406
  var weak_crypto_1 = require_weak_crypto();
14407
14407
  var tiers_1 = require_tiers();
14408
14408
  var path_exclusions_1 = require_path_exclusions();
14409
+ function processFileLayer1(file) {
14410
+ const stats = {
14411
+ known_secrets: 0,
14412
+ weak_crypto: 0,
14413
+ sensitive_urls: 0,
14414
+ entropy: 0,
14415
+ config_audit: 0,
14416
+ file_flags: 0,
14417
+ ai_comments: 0
14418
+ };
14419
+ const entropyFindings = (0, entropy_1.detectHighEntropyStrings)(file.content, file.path);
14420
+ const patternFindings = (0, patterns_1.detectKnownPatterns)(file.content, file.path);
14421
+ const configFindings = (0, config_audit_1.auditConfiguration)(file.content, file.path);
14422
+ const fileFlags = (0, file_flags_1.detectDangerousFiles)(file.content, file.path);
14423
+ const commentFindings = (0, comments_1.detectAICommentPatterns)(file.content, file.path);
14424
+ const urlFindings = (0, urls_1.detectSensitiveURLs)(file.content, file.path);
14425
+ const cryptoFindings = (0, weak_crypto_1.detectWeakCrypto)(file.content, file.path);
14426
+ stats.entropy = entropyFindings.length;
14427
+ stats.known_secrets = patternFindings.length;
14428
+ stats.config_audit = configFindings.length;
14429
+ stats.file_flags = fileFlags.length;
14430
+ stats.ai_comments = commentFindings.length;
14431
+ stats.sensitive_urls = urlFindings.length;
14432
+ stats.weak_crypto = cryptoFindings.length;
14433
+ return {
14434
+ findings: [
14435
+ ...entropyFindings,
14436
+ ...patternFindings,
14437
+ ...configFindings,
14438
+ ...fileFlags,
14439
+ ...commentFindings,
14440
+ ...urlFindings,
14441
+ ...cryptoFindings
14442
+ ],
14443
+ stats
14444
+ };
14445
+ }
14446
+ var LAYER1_PARALLEL_BATCH_SIZE = 50;
14409
14447
  async function runLayer1Scan(files) {
14410
14448
  const startTime = Date.now();
14411
14449
  const vulnerabilities = [];
@@ -14418,50 +14456,24 @@ var require_layer1 = __commonJS({
14418
14456
  file_flags: 0,
14419
14457
  ai_comments: 0
14420
14458
  };
14421
- for (const file of files) {
14422
- const entropyFindings = (0, entropy_1.detectHighEntropyStrings)(file.content, file.path);
14423
- const patternFindings = (0, patterns_1.detectKnownPatterns)(file.content, file.path);
14424
- const configFindings = (0, config_audit_1.auditConfiguration)(file.content, file.path);
14425
- const fileFlags = (0, file_flags_1.detectDangerousFiles)(file.content, file.path);
14426
- const commentFindings = (0, comments_1.detectAICommentPatterns)(file.content, file.path);
14427
- const urlFindings = (0, urls_1.detectSensitiveURLs)(file.content, file.path);
14428
- const cryptoFindings = (0, weak_crypto_1.detectWeakCrypto)(file.content, file.path);
14429
- rawStats.entropy += entropyFindings.length;
14430
- rawStats.known_secrets += patternFindings.length;
14431
- rawStats.config_audit += configFindings.length;
14432
- rawStats.file_flags += fileFlags.length;
14433
- rawStats.ai_comments += commentFindings.length;
14434
- rawStats.sensitive_urls += urlFindings.length;
14435
- rawStats.weak_crypto += cryptoFindings.length;
14436
- vulnerabilities.push(...entropyFindings, ...patternFindings, ...configFindings, ...fileFlags, ...commentFindings, ...urlFindings, ...cryptoFindings);
14459
+ for (let i = 0; i < files.length; i += LAYER1_PARALLEL_BATCH_SIZE) {
14460
+ const batch = files.slice(i, i + LAYER1_PARALLEL_BATCH_SIZE);
14461
+ const results = await Promise.all(batch.map((file) => Promise.resolve(processFileLayer1(file))));
14462
+ for (const result of results) {
14463
+ vulnerabilities.push(...result.findings);
14464
+ for (const [key, value] of Object.entries(result.stats)) {
14465
+ rawStats[key] += value;
14466
+ }
14467
+ }
14437
14468
  }
14438
14469
  const dedupedVulnerabilities = deduplicateFindings(vulnerabilities);
14439
14470
  const { kept: uniqueVulnerabilities, suppressed } = (0, path_exclusions_1.filterFindingsByPath)(dedupedVulnerabilities);
14440
- if (suppressed.length > 0) {
14441
- const byReason = {};
14442
- for (const s of suppressed) {
14443
- const reason = s.reason || "unknown";
14444
- byReason[reason] = (byReason[reason] || 0) + 1;
14445
- }
14446
- console.log(`[Layer 1] Suppressed ${suppressed.length} findings in test/seed/example files:`);
14447
- for (const [reason, count] of Object.entries(byReason)) {
14448
- console.log(` - ${reason}: ${count}`);
14449
- }
14450
- }
14451
14471
  const dedupedStats = {};
14452
14472
  for (const vuln of uniqueVulnerabilities) {
14453
14473
  const cat = vuln.category;
14454
14474
  dedupedStats[cat] = (dedupedStats[cat] || 0) + 1;
14455
14475
  }
14456
14476
  const tierStats = (0, tiers_1.computeTierStats)(uniqueVulnerabilities.map((v2) => ({ category: v2.category, layer: 1 })));
14457
- console.log("[Layer 1] Heuristic breakdown (raw findings before dedupe):");
14458
- for (const [name, count] of Object.entries(rawStats)) {
14459
- if (count > 0) {
14460
- const tier = (0, tiers_1.getLayer1DetectorTier)(name);
14461
- console.log(` - ${name}: ${count} (${tier})`);
14462
- }
14463
- }
14464
- console.log(`[Layer 1] Tier breakdown (after dedupe): ${(0, tiers_1.formatTierStats)(tierStats)}`);
14465
14477
  return {
14466
14478
  vulnerabilities: uniqueVulnerabilities,
14467
14479
  filesScanned: files.length,
@@ -17678,7 +17690,7 @@ var require_ai_fingerprinting = __commonJS({
17678
17690
  return vulnerabilities;
17679
17691
  }
17680
17692
  const anyUsageByContext = categorizeAnyUsage(lines, filePath);
17681
- const priorityAny = anyUsageByContext.filter((usage) => usage.context === "api_boundary" || usage.context === "database_layer" || usage.context === "auth_handler");
17693
+ const priorityAny = anyUsageByContext.filter((usage2) => usage2.context === "api_boundary" || usage2.context === "database_layer" || usage2.context === "auth_handler");
17682
17694
  const cappedAny = priorityAny.slice(0, 5);
17683
17695
  if (cappedAny.length === 0) {
17684
17696
  return vulnerabilities;
@@ -17708,21 +17720,21 @@ var require_ai_fingerprinting = __commonJS({
17708
17720
  layer: 2
17709
17721
  });
17710
17722
  } else {
17711
- for (const usage of cappedAny) {
17723
+ for (const usage2 of cappedAny) {
17712
17724
  const contextNames = {
17713
17725
  "api_boundary": "API request/response handler",
17714
17726
  "database_layer": "Database query",
17715
17727
  "auth_handler": "Authentication logic"
17716
17728
  };
17717
17729
  vulnerabilities.push({
17718
- id: `ai-fingerprint-any-${filePath}-${usage.lineNumber}`,
17730
+ id: `ai-fingerprint-any-${filePath}-${usage2.lineNumber}`,
17719
17731
  filePath,
17720
- lineNumber: usage.lineNumber,
17721
- lineContent: usage.lineContent,
17732
+ lineNumber: usage2.lineNumber,
17733
+ lineContent: usage2.lineContent,
17722
17734
  severity: "low",
17723
17735
  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}.`,
17736
+ title: `[AI Pattern] TypeScript 'any' in ${contextNames[usage2.context] || usage2.context}`,
17737
+ 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
17738
  suggestedFix: 'Replace "any" with an explicit type. Use typed request schemas, ORM models, or interface definitions.',
17727
17739
  confidence: "medium",
17728
17740
  layer: 2
@@ -20677,6 +20689,85 @@ var require_layer2 = __commonJS({
20677
20689
  var ai_endpoint_protection_1 = require_ai_endpoint_protection();
20678
20690
  var ai_schema_validation_1 = require_ai_schema_validation();
20679
20691
  var tiers_1 = require_tiers();
20692
+ function processFileLayer2(file, options, authHelperContext) {
20693
+ const stats = {
20694
+ variables: 0,
20695
+ logicGates: 0,
20696
+ dangerousFunctions: 0,
20697
+ riskyImports: 0,
20698
+ authAntipatterns: 0,
20699
+ frameworkIssues: 0,
20700
+ aiFingerprints: 0,
20701
+ dataExposure: 0,
20702
+ byokPatterns: 0,
20703
+ promptHygiene: 0,
20704
+ executionSinks: 0,
20705
+ agentTools: 0,
20706
+ ragSafety: 0,
20707
+ endpointProtection: 0,
20708
+ schemaValidation: 0
20709
+ };
20710
+ if (!isCodeFile(file.path)) {
20711
+ return { findings: [], stats };
20712
+ }
20713
+ const variableFindings = (0, variables_1.detectSensitiveVariables)(file.content, file.path);
20714
+ const logicFindings = (0, logic_gates_1.detectLogicGates)(file.content, file.path);
20715
+ const dangerousFuncFindings = (0, dangerous_functions_1.detectDangerousFunctions)(file.content, file.path);
20716
+ const riskyImportFindings = (0, risky_imports_1.detectRiskyImports)(file.content, file.path);
20717
+ const authFindings = (0, auth_antipatterns_1.detectAuthAntipatterns)(file.content, file.path, {
20718
+ middlewareConfig: options.middlewareConfig,
20719
+ authHelpers: authHelperContext,
20720
+ fileAuthImports: options.fileAuthImports
20721
+ });
20722
+ const frameworkFindings = (0, framework_checks_1.detectFrameworkIssues)(file.content, file.path);
20723
+ const aiFindings = (0, ai_fingerprinting_1.detectAIFingerprints)(file.content, file.path);
20724
+ const dataExposureFindings = (0, data_exposure_1.detectDataExposure)(file.content, file.path);
20725
+ const byokFindings = (0, byok_patterns_1.detectBYOKPatterns)(file.content, file.path, options.middlewareConfig);
20726
+ const promptHygieneFindings = (0, ai_prompt_hygiene_1.detectAIPromptHygiene)(file.content, file.path);
20727
+ const executionSinkFindings = (0, ai_execution_sinks_1.detectAIExecutionSinks)(file.content, file.path);
20728
+ const agentToolFindings = (0, ai_agent_tools_1.detectAIAgentTools)(file.content, file.path);
20729
+ const ragSafetyFindings = (0, ai_rag_safety_1.detectRAGSafetyIssues)(file.content, file.path);
20730
+ const endpointProtectionFindings = (0, ai_endpoint_protection_1.detectAIEndpointProtection)(file.content, file.path, {
20731
+ middlewareConfig: options.middlewareConfig
20732
+ });
20733
+ const schemaValidationFindings = (0, ai_schema_validation_1.detectAISchemaValidation)(file.content, file.path);
20734
+ stats.variables = variableFindings.length;
20735
+ stats.logicGates = logicFindings.length;
20736
+ stats.dangerousFunctions = dangerousFuncFindings.length;
20737
+ stats.riskyImports = riskyImportFindings.length;
20738
+ stats.authAntipatterns = authFindings.length;
20739
+ stats.frameworkIssues = frameworkFindings.length;
20740
+ stats.aiFingerprints = aiFindings.length;
20741
+ stats.dataExposure = dataExposureFindings.length;
20742
+ stats.byokPatterns = byokFindings.length;
20743
+ stats.promptHygiene = promptHygieneFindings.length;
20744
+ stats.executionSinks = executionSinkFindings.length;
20745
+ stats.agentTools = agentToolFindings.length;
20746
+ stats.ragSafety = ragSafetyFindings.length;
20747
+ stats.endpointProtection = endpointProtectionFindings.length;
20748
+ stats.schemaValidation = schemaValidationFindings.length;
20749
+ return {
20750
+ findings: [
20751
+ ...variableFindings,
20752
+ ...logicFindings,
20753
+ ...dangerousFuncFindings,
20754
+ ...riskyImportFindings,
20755
+ ...authFindings,
20756
+ ...frameworkFindings,
20757
+ ...aiFindings,
20758
+ ...dataExposureFindings,
20759
+ ...byokFindings,
20760
+ ...promptHygieneFindings,
20761
+ ...executionSinkFindings,
20762
+ ...agentToolFindings,
20763
+ ...ragSafetyFindings,
20764
+ ...endpointProtectionFindings,
20765
+ ...schemaValidationFindings
20766
+ ],
20767
+ stats
20768
+ };
20769
+ }
20770
+ var LAYER2_PARALLEL_BATCH_SIZE = 50;
20680
20771
  async function runLayer2Scan(files, options = {}) {
20681
20772
  const startTime = Date.now();
20682
20773
  const vulnerabilities = [];
@@ -20693,51 +20784,19 @@ var require_layer2 = __commonJS({
20693
20784
  promptHygiene: 0,
20694
20785
  executionSinks: 0,
20695
20786
  agentTools: 0,
20696
- // M5: New AI-era detectors
20697
20787
  ragSafety: 0,
20698
20788
  endpointProtection: 0,
20699
20789
  schemaValidation: 0
20700
20790
  };
20701
20791
  const authHelperContext = options.authHelperContext || (0, auth_helper_detector_1.detectAuthHelpers)(files);
20702
- for (const file of files) {
20703
- if (isCodeFile(file.path)) {
20704
- const variableFindings = (0, variables_1.detectSensitiveVariables)(file.content, file.path);
20705
- const logicFindings = (0, logic_gates_1.detectLogicGates)(file.content, file.path);
20706
- const dangerousFuncFindings = (0, dangerous_functions_1.detectDangerousFunctions)(file.content, file.path);
20707
- const riskyImportFindings = (0, risky_imports_1.detectRiskyImports)(file.content, file.path);
20708
- const authFindings = (0, auth_antipatterns_1.detectAuthAntipatterns)(file.content, file.path, {
20709
- middlewareConfig: options.middlewareConfig,
20710
- authHelpers: authHelperContext,
20711
- fileAuthImports: options.fileAuthImports
20712
- });
20713
- const frameworkFindings = (0, framework_checks_1.detectFrameworkIssues)(file.content, file.path);
20714
- const aiFindings = (0, ai_fingerprinting_1.detectAIFingerprints)(file.content, file.path);
20715
- const dataExposureFindings = (0, data_exposure_1.detectDataExposure)(file.content, file.path);
20716
- const byokFindings = (0, byok_patterns_1.detectBYOKPatterns)(file.content, file.path, options.middlewareConfig);
20717
- const promptHygieneFindings = (0, ai_prompt_hygiene_1.detectAIPromptHygiene)(file.content, file.path);
20718
- const executionSinkFindings = (0, ai_execution_sinks_1.detectAIExecutionSinks)(file.content, file.path);
20719
- const agentToolFindings = (0, ai_agent_tools_1.detectAIAgentTools)(file.content, file.path);
20720
- const ragSafetyFindings = (0, ai_rag_safety_1.detectRAGSafetyIssues)(file.content, file.path);
20721
- const endpointProtectionFindings = (0, ai_endpoint_protection_1.detectAIEndpointProtection)(file.content, file.path, {
20722
- middlewareConfig: options.middlewareConfig
20723
- });
20724
- const schemaValidationFindings = (0, ai_schema_validation_1.detectAISchemaValidation)(file.content, file.path);
20725
- stats.variables += variableFindings.length;
20726
- stats.logicGates += logicFindings.length;
20727
- stats.dangerousFunctions += dangerousFuncFindings.length;
20728
- stats.riskyImports += riskyImportFindings.length;
20729
- stats.authAntipatterns += authFindings.length;
20730
- stats.frameworkIssues += frameworkFindings.length;
20731
- stats.aiFingerprints += aiFindings.length;
20732
- stats.dataExposure += dataExposureFindings.length;
20733
- stats.byokPatterns += byokFindings.length;
20734
- stats.promptHygiene += promptHygieneFindings.length;
20735
- stats.executionSinks += executionSinkFindings.length;
20736
- stats.agentTools += agentToolFindings.length;
20737
- stats.ragSafety += ragSafetyFindings.length;
20738
- stats.endpointProtection += endpointProtectionFindings.length;
20739
- stats.schemaValidation += schemaValidationFindings.length;
20740
- vulnerabilities.push(...variableFindings, ...logicFindings, ...dangerousFuncFindings, ...riskyImportFindings, ...authFindings, ...frameworkFindings, ...aiFindings, ...dataExposureFindings, ...byokFindings, ...promptHygieneFindings, ...executionSinkFindings, ...agentToolFindings, ...ragSafetyFindings, ...endpointProtectionFindings, ...schemaValidationFindings);
20792
+ for (let i = 0; i < files.length; i += LAYER2_PARALLEL_BATCH_SIZE) {
20793
+ const batch = files.slice(i, i + LAYER2_PARALLEL_BATCH_SIZE);
20794
+ const results = await Promise.all(batch.map((file) => Promise.resolve(processFileLayer2(file, options, authHelperContext))));
20795
+ for (const result of results) {
20796
+ vulnerabilities.push(...result.findings);
20797
+ for (const [key, value] of Object.entries(result.stats)) {
20798
+ stats[key] += value;
20799
+ }
20741
20800
  }
20742
20801
  }
20743
20802
  const dedupedVulnerabilities = deduplicateFindings(vulnerabilities);
@@ -20757,16 +20816,6 @@ var require_layer2 = __commonJS({
20757
20816
  ];
20758
20817
  }
20759
20818
  const { kept: uniqueVulnerabilities, suppressed } = (0, path_exclusions_1.filterFindingsByPath)(dedupedVulnerabilities, Object.keys(exclusionConfig).length > 0 ? exclusionConfig : void 0);
20760
- if (suppressed.length > 0) {
20761
- console.log(`[Layer 2] Suppressed ${suppressed.length} findings in test/seed/example files:`);
20762
- const byReason = /* @__PURE__ */ new Map();
20763
- for (const { reason } of suppressed) {
20764
- byReason.set(reason || "unknown", (byReason.get(reason || "unknown") || 0) + 1);
20765
- }
20766
- for (const [reason, count] of byReason) {
20767
- console.log(` - ${reason}: ${count}`);
20768
- }
20769
- }
20770
20819
  const rawStats = {
20771
20820
  sensitive_variables: stats.variables,
20772
20821
  logic_gates: stats.logicGates,
@@ -20813,15 +20862,6 @@ var require_layer2 = __commonJS({
20813
20862
  ai_endpoint_protection: "ai_endpoint_protection",
20814
20863
  ai_schema_validation: "ai_schema_validation"
20815
20864
  };
20816
- console.log("[Layer 2] Heuristic breakdown (raw findings before dedupe):");
20817
- for (const [name, count] of Object.entries(rawStats)) {
20818
- if (count > 0) {
20819
- const detectorName = detectorNameMap[name];
20820
- const tier = detectorName ? (0, tiers_1.getLayer2DetectorTier)(detectorName) : "unknown";
20821
- console.log(` - ${name}: ${count} (${tier})`);
20822
- }
20823
- }
20824
- console.log(`[Layer 2] Tier breakdown (after dedupe): ${(0, tiers_1.formatTierStats)(tierStats)}`);
20825
20865
  return {
20826
20866
  vulnerabilities: uniqueVulnerabilities,
20827
20867
  filesScanned: files.filter((f) => isCodeFile(f.path)).length,
@@ -29411,11 +29451,11 @@ var require_AbstractChatCompletionRunner = __commonJS({
29411
29451
  prompt_tokens: 0,
29412
29452
  total_tokens: 0
29413
29453
  };
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;
29454
+ for (const { usage: usage2 } of this._chatCompletions) {
29455
+ if (usage2) {
29456
+ total.completion_tokens += usage2.completion_tokens;
29457
+ total.prompt_tokens += usage2.prompt_tokens;
29458
+ total.total_tokens += usage2.total_tokens;
29419
29459
  }
29420
29460
  }
29421
29461
  return total;
@@ -36127,8 +36167,8 @@ var require_anthropic = __commonJS({
36127
36167
  var context_helpers_1 = require_context_helpers();
36128
36168
  var project_context_builder_1 = require_project_context_builder();
36129
36169
  var tiers_1 = require_tiers();
36130
- var FILES_PER_API_BATCH = 5;
36131
- var PARALLEL_API_BATCHES = 4;
36170
+ var FILES_PER_API_BATCH = 8;
36171
+ var PARALLEL_API_BATCHES = 6;
36132
36172
  function getAnthropicClient() {
36133
36173
  const apiKey = process.env.ANTHROPIC_API_KEY;
36134
36174
  if (!apiKey) {
@@ -37001,11 +37041,11 @@ For each candidate finding, return:
37001
37041
  max_completion_tokens: 4096
37002
37042
  }));
37003
37043
  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;
37044
+ const usage2 = response.usage;
37045
+ if (usage2) {
37046
+ const promptTokens = usage2.prompt_tokens || 0;
37047
+ const completionTokens = usage2.completion_tokens || 0;
37048
+ const cachedTokens = usage2.prompt_tokens_details?.cached_tokens || 0;
37009
37049
  const freshInputTokens = promptTokens - cachedTokens;
37010
37050
  statsLock.estimatedInputTokens += freshInputTokens;
37011
37051
  statsLock.estimatedOutputTokens += completionTokens;
@@ -37226,19 +37266,19 @@ For each candidate finding, return:
37226
37266
  }));
37227
37267
  stats.apiCalls++;
37228
37268
  totalApiBatches++;
37229
- const usage = response.usage;
37230
- if (usage) {
37269
+ const usage2 = response.usage;
37270
+ if (usage2) {
37231
37271
  console.log(`[DEBUG] Batch ${batchNum} - Full API Response Usage:`);
37232
- console.log(JSON.stringify(usage, null, 2));
37272
+ console.log(JSON.stringify(usage2, null, 2));
37233
37273
  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;
37274
+ console.log(` - input_tokens: ${usage2.input_tokens || 0}`);
37275
+ console.log(` - output_tokens: ${usage2.output_tokens || 0}`);
37276
+ console.log(` - cache_creation_input_tokens: ${usage2.cache_creation_input_tokens || 0}`);
37277
+ console.log(` - cache_read_input_tokens: ${usage2.cache_read_input_tokens || 0}`);
37278
+ stats.estimatedInputTokens += usage2.input_tokens || 0;
37279
+ stats.estimatedOutputTokens += usage2.output_tokens || 0;
37280
+ const cacheCreation = usage2.cache_creation_input_tokens || 0;
37281
+ const cacheRead = usage2.cache_read_input_tokens || 0;
37242
37282
  stats.cacheCreationTokens += cacheCreation;
37243
37283
  stats.cacheReadTokens += cacheRead;
37244
37284
  }
@@ -38758,9 +38798,15 @@ var require_dist = __commonJS({
38758
38798
  const scanModeConfig = resolveScanModeConfig(options);
38759
38799
  const isIncremental = scanModeConfig.mode === "incremental";
38760
38800
  const depth = scanModeConfig.scanDepth || "cheap";
38761
- console.log(`[Scanner] repo=${repoInfo.name} mode=${scanModeConfig.mode} depth=${depth} files=${files.length}`);
38801
+ const quiet = options.quiet ?? false;
38802
+ const log = (message) => {
38803
+ if (!quiet) {
38804
+ console.log(message);
38805
+ }
38806
+ };
38807
+ log(`[Scanner] repo=${repoInfo.name} mode=${scanModeConfig.mode} depth=${depth} files=${files.length}`);
38762
38808
  if (isIncremental && scanModeConfig.changedFiles) {
38763
- console.log(`[Scanner] repo=${repoInfo.name} incremental_files=${scanModeConfig.changedFiles.length}`);
38809
+ log(`[Scanner] repo=${repoInfo.name} incremental_files=${scanModeConfig.changedFiles.length}`);
38764
38810
  }
38765
38811
  const reportProgress = (status2, message, vulnerabilitiesFound = allVulnerabilities.length) => {
38766
38812
  if (onProgress) {
@@ -38777,12 +38823,12 @@ var require_dist = __commonJS({
38777
38823
  try {
38778
38824
  const middlewareConfig = (0, middleware_detector_1.detectGlobalAuthMiddleware)(files);
38779
38825
  if (middlewareConfig.hasAuthMiddleware) {
38780
- console.log(`[Scanner] repo=${repoInfo.name} auth_middleware=${middlewareConfig.authType || "unknown"} file=${middlewareConfig.middlewareFile}`);
38826
+ log(`[Scanner] repo=${repoInfo.name} auth_middleware=${middlewareConfig.authType || "unknown"} file=${middlewareConfig.middlewareFile}`);
38781
38827
  }
38782
38828
  const fileAuthImports = (0, imported_auth_detector_1.buildFileAuthImports)(files);
38783
38829
  const filesWithImportedAuth = Array.from(fileAuthImports.values()).filter((f) => f.usesImportedAuth).length;
38784
38830
  if (filesWithImportedAuth > 0) {
38785
- console.log(`[Scanner] repo=${repoInfo.name} files_with_imported_auth=${filesWithImportedAuth}`);
38831
+ log(`[Scanner] repo=${repoInfo.name} files_with_imported_auth=${filesWithImportedAuth}`);
38786
38832
  }
38787
38833
  reportProgress("layer1", "Running surface scan (patterns, entropy, config)...");
38788
38834
  let layer1Result = await (0, layer1_1.runLayer1Scan)(files);
@@ -38791,18 +38837,18 @@ var require_dist = __commonJS({
38791
38837
  ...layer1Result,
38792
38838
  vulnerabilities: (0, urls_1.aggregateLocalhostFindings)(layer1Result.vulnerabilities)
38793
38839
  };
38794
- console.log(`[Layer1] repo=${repoInfo.name} findings_raw=${layer1RawCount} findings_deduped=${layer1Result.vulnerabilities.length}`);
38840
+ log(`[Layer1] repo=${repoInfo.name} findings_raw=${layer1RawCount} findings_deduped=${layer1Result.vulnerabilities.length}`);
38795
38841
  reportProgress("layer2", "Running structural scan (variables, logic gates)...", layer1Result.vulnerabilities.length);
38796
38842
  const layer2Result = await (0, layer2_1.runLayer2Scan)(files, { middlewareConfig, fileAuthImports });
38797
38843
  const heuristicBreakdown = Object.entries(layer2Result.stats.raw).filter(([, count]) => count > 0).map(([name, count]) => `${name}:${count}`).join(",");
38798
- console.log(`[Layer2] repo=${repoInfo.name} findings_raw=${Object.values(layer2Result.stats.raw).reduce((a, b3) => a + b3, 0)} findings_deduped=${layer2Result.vulnerabilities.length} heuristic_breakdown={${heuristicBreakdown}}`);
38844
+ log(`[Layer2] repo=${repoInfo.name} findings_raw=${Object.values(layer2Result.stats.raw).reduce((a, b3) => a + b3, 0)} findings_deduped=${layer2Result.vulnerabilities.length} heuristic_breakdown={${heuristicBreakdown}}`);
38799
38845
  const layer12Findings = [...layer1Result.vulnerabilities, ...layer2Result.vulnerabilities];
38800
38846
  const beforeAggregationCount = layer12Findings.length;
38801
38847
  const aggregatedFindings = aggregateNoisyFindings(layer12Findings);
38802
38848
  const aggregatedCount = beforeAggregationCount - aggregatedFindings.length;
38803
38849
  const tierFiltered = filterByTierAndDepth(aggregatedFindings, depth);
38804
- console.log(`[Scanner] repo=${repoInfo.name} tier_breakdown=${(0, tiers_1.formatTierStats)(tierFiltered.tierStats)}`);
38805
- console.log(`[Scanner] repo=${repoInfo.name} depth=${depth} tier_routing: surface=${tierFiltered.toSurface.length} validate=${tierFiltered.toValidate.length} hidden=${tierFiltered.hidden.length}`);
38850
+ log(`[Scanner] repo=${repoInfo.name} tier_breakdown=${(0, tiers_1.formatTierStats)(tierFiltered.tierStats)}`);
38851
+ log(`[Scanner] repo=${repoInfo.name} depth=${depth} tier_routing: surface=${tierFiltered.toSurface.length} validate=${tierFiltered.toValidate.length} hidden=${tierFiltered.hidden.length}`);
38806
38852
  const additionalValidation = tierFiltered.toSurface.filter(
38807
38853
  (v2) => v2.requiresAIValidation || v2.category === "ai_prompt_injection" || // Story B1: Prompt hygiene
38808
38854
  v2.category === "ai_unsafe_execution" || // Story B2: LLM output execution
@@ -38817,7 +38863,7 @@ var require_dist = __commonJS({
38817
38863
  autoDismissBySeverity[d3.finding.severity] = (autoDismissBySeverity[d3.finding.severity] || 0) + 1;
38818
38864
  }
38819
38865
  if (autoDismissed.length > 0) {
38820
- console.log(`[Layer2] repo=${repoInfo.name} auto_dismissed_total=${autoDismissed.length} by_severity={info:${autoDismissBySeverity.info},low:${autoDismissBySeverity.low},medium:${autoDismissBySeverity.medium},high:${autoDismissBySeverity.high}}`);
38866
+ log(`[Layer2] repo=${repoInfo.name} auto_dismissed_total=${autoDismissed.length} by_severity={info:${autoDismissBySeverity.info},low:${autoDismissBySeverity.low},medium:${autoDismissBySeverity.medium},high:${autoDismissBySeverity.high}}`);
38821
38867
  }
38822
38868
  const maxValidationFiles = scanModeConfig.maxAIValidationFiles || MAX_VALIDATION_CANDIDATES_PER_FILE;
38823
38869
  const cappedValidation = capValidationCandidatesPerFile(afterAutoDismiss, maxValidationFiles);
@@ -38832,13 +38878,13 @@ var require_dist = __commonJS({
38832
38878
  validatedFindings = validationResult.vulnerabilities;
38833
38879
  const { stats: validationStats } = validationResult;
38834
38880
  capturedValidationStats = validationStats;
38835
- console.log(`[AI Validation] repo=${repoInfo.name} depth=${depth} candidates=${findingsToValidate.length} capped_from=${requiresValidation.length} auto_dismissed=${autoDismissed.length} kept=${validationStats.confirmedFindings} rejected=${validationStats.dismissedFindings} downgraded=${validationStats.downgradedFindings}`);
38836
- console.log(`[AI Validation] cost_estimate: input_tokens=${validationStats.estimatedInputTokens} output_tokens=${validationStats.estimatedOutputTokens} cost=$${validationStats.estimatedCost.toFixed(4)} api_calls=${validationStats.apiCalls}`);
38881
+ log(`[AI Validation] repo=${repoInfo.name} depth=${depth} candidates=${findingsToValidate.length} capped_from=${requiresValidation.length} auto_dismissed=${autoDismissed.length} kept=${validationStats.confirmedFindings} rejected=${validationStats.dismissedFindings} downgraded=${validationStats.downgradedFindings}`);
38882
+ log(`[AI Validation] cost_estimate: input_tokens=${validationStats.estimatedInputTokens} output_tokens=${validationStats.estimatedOutputTokens} cost=$${validationStats.estimatedCost.toFixed(4)} api_calls=${validationStats.apiCalls}`);
38837
38883
  const notValidated = cappedValidation.filter((v2) => !findingsToValidate.includes(v2));
38838
38884
  validatedFindings.push(...notValidated);
38839
38885
  }
38840
38886
  } else if (scanModeConfig.skipAIValidation) {
38841
- console.log(`[AI Validation] repo=${repoInfo.name} depth=${depth} skipped=true reason=scan_mode_config`);
38887
+ log(`[AI Validation] repo=${repoInfo.name} depth=${depth} skipped=true reason=scan_mode_config`);
38842
38888
  }
38843
38889
  allVulnerabilities.push(...validatedFindings, ...noValidationNeeded);
38844
38890
  const shouldRunLayer3 = options.enableAI !== false && !scanModeConfig.skipLayer3;
@@ -38863,9 +38909,9 @@ var require_dist = __commonJS({
38863
38909
  }
38864
38910
  });
38865
38911
  allVulnerabilities.push(...layer3Result.vulnerabilities);
38866
- console.log(`[Layer3] repo=${repoInfo.name} depth=${depth} files_analyzed=${layer3Result.aiAnalyzed} findings=${layer3Result.vulnerabilities.length}`);
38912
+ log(`[Layer3] repo=${repoInfo.name} depth=${depth} files_analyzed=${layer3Result.aiAnalyzed} findings=${layer3Result.vulnerabilities.length}`);
38867
38913
  } else if (scanModeConfig.skipLayer3) {
38868
- console.log(`[Layer3] repo=${repoInfo.name} depth=${depth} skipped=true reason=scan_mode_config`);
38914
+ log(`[Layer3] repo=${repoInfo.name} depth=${depth} skipped=true reason=scan_mode_config`);
38869
38915
  }
38870
38916
  const uniqueVulnerabilities = deduplicateVulnerabilities(allVulnerabilities);
38871
38917
  const resolvedVulnerabilities = resolveContradictions(uniqueVulnerabilities, middlewareConfig);
@@ -38976,9 +39022,6 @@ Found ${group.length} occurrences at lines: ${lineDisplay}`,
38976
39022
  });
38977
39023
  const capped = sorted.slice(0, maxPerFile);
38978
39024
  result.push(...capped);
38979
- if (sorted.length > maxPerFile) {
38980
- console.log(`[Scanner] Capped ${filePath}: ${sorted.length} \u2192 ${maxPerFile} validation candidates`);
38981
- }
38982
39025
  }
38983
39026
  return result;
38984
39027
  }
@@ -39031,7 +39074,6 @@ Found ${group.length} occurrences at lines: ${lineDisplay}`,
39031
39074
  for (const vuln of otherAuthFindings) {
39032
39075
  if (vuln.severity === "critical" || vuln.severity === "high") {
39033
39076
  const reason = isAPIRouteProtected ? "API route protected by middleware" : isClientCallingProtectedAPI ? "client component calling protected API" : "route is protected";
39034
- console.log(`[Contradiction] Dropping "${vuln.title}" (${vuln.severity}) - ${reason}`);
39035
39077
  continue;
39036
39078
  }
39037
39079
  result.push(vuln);
@@ -42827,6 +42869,27 @@ async function pollForLogin(deviceCode) {
42827
42869
  tier: result.tier
42828
42870
  };
42829
42871
  }
42872
+ async function getUsage(apiKey) {
42873
+ const baseUrl = getApiBaseUrl();
42874
+ const url = `${baseUrl}/v1/usage`;
42875
+ try {
42876
+ const response = await fetch(url, {
42877
+ method: "GET",
42878
+ headers: {
42879
+ "Authorization": `Bearer ${apiKey}`,
42880
+ "X-Oculum-Client": "cli",
42881
+ "X-Oculum-Version": "1.0.0"
42882
+ }
42883
+ });
42884
+ const result = await response.json();
42885
+ return result;
42886
+ } catch (error) {
42887
+ return {
42888
+ success: false,
42889
+ error: "Failed to fetch usage data. Please check your internet connection."
42890
+ };
42891
+ }
42892
+ }
42830
42893
 
42831
42894
  // src/utils/errors.ts
42832
42895
  function enhanceError(error) {
@@ -43477,18 +43540,19 @@ async function runScanOnce(targetPath, options) {
43477
43540
  }
43478
43541
  spinner.succeed(`Found ${files.length} files to scan`);
43479
43542
  const onProgress = (progress) => {
43543
+ const fileInfo = source_default.dim(`(${progress.totalFiles} files)`);
43480
43544
  switch (progress.status) {
43481
43545
  case "layer1":
43482
- spinner.text = `Scanning patterns... ${source_default.dim(`(${progress.vulnerabilitiesFound} potential issues)`)}`;
43546
+ spinner.text = `Layer 1: Pattern matching ${fileInfo} ${source_default.dim(`\u2192 ${progress.vulnerabilitiesFound} candidates`)}`;
43483
43547
  break;
43484
43548
  case "layer2":
43485
- spinner.text = `Analyzing code structure... ${source_default.dim(`(${progress.vulnerabilitiesFound} findings)`)}`;
43549
+ spinner.text = `Layer 2: Code structure analysis ${fileInfo} ${source_default.dim(`\u2192 ${progress.vulnerabilitiesFound} findings`)}`;
43486
43550
  break;
43487
43551
  case "validating":
43488
- spinner.text = `AI validating findings... ${source_default.dim(`(${progress.vulnerabilitiesFound} candidates)`)}`;
43552
+ spinner.text = `AI validation ${source_default.dim(`\u2192 reviewing ${progress.vulnerabilitiesFound} candidates`)}`;
43489
43553
  break;
43490
43554
  case "layer3":
43491
- spinner.text = `Deep AI analysis in progress...`;
43555
+ spinner.text = `Layer 3: Deep AI analysis ${source_default.dim(`\u2192 semantic security check`)}`;
43492
43556
  break;
43493
43557
  case "complete":
43494
43558
  const issueText = progress.vulnerabilitiesFound === 1 ? "issue" : "issues";
@@ -43522,6 +43586,7 @@ async function runScanOnce(targetPath, options) {
43522
43586
  spinner.succeed(`Backend scan complete`);
43523
43587
  } else {
43524
43588
  const enableAI = options.depth !== "cheap" && hasLocalAI;
43589
+ const shouldBeQuiet = options.quiet ?? !options.verbose;
43525
43590
  result = await (0, import_scanner.runScan)(
43526
43591
  files,
43527
43592
  {
@@ -43531,7 +43596,8 @@ async function runScanOnce(targetPath, options) {
43531
43596
  },
43532
43597
  {
43533
43598
  enableAI,
43534
- scanDepth: options.depth
43599
+ scanDepth: options.depth,
43600
+ quiet: shouldBeQuiet
43535
43601
  },
43536
43602
  onProgress
43537
43603
  );
@@ -43574,6 +43640,7 @@ async function runScan(targetPath, cliOptions) {
43574
43640
  failOn: cliOptions.failOn ?? "high",
43575
43641
  color: cliOptions.color,
43576
43642
  quiet: cliOptions.quiet,
43643
+ verbose: cliOptions.verbose,
43577
43644
  incremental: cliOptions.incremental,
43578
43645
  diff: cliOptions.diff,
43579
43646
  output: cliOptions.output
@@ -43584,6 +43651,7 @@ async function runScan(targetPath, cliOptions) {
43584
43651
  format: baseOptions.format ?? projectConfig?.format ?? "terminal",
43585
43652
  failOn: baseOptions.failOn ?? projectConfig?.failOn ?? "high",
43586
43653
  quiet: baseOptions.quiet ?? projectConfig?.quiet,
43654
+ verbose: baseOptions.verbose,
43587
43655
  output: baseOptions.output ?? projectConfig?.output
43588
43656
  };
43589
43657
  try {
@@ -43598,7 +43666,7 @@ async function runScan(targetPath, cliOptions) {
43598
43666
  process.exit(1);
43599
43667
  }
43600
43668
  }
43601
- var scanCommand = new Command("scan").description("Scan a directory or file for security vulnerabilities").argument("[path]", "path to scan", ".").option("-d, --depth <depth>", "scan depth: cheap (free), validated, deep", "cheap").option("-m, --mode <mode>", "alias for --depth (quick, validated, deep)").option("-f, --format <format>", "output format: terminal, json, sarif, markdown", "terminal").option("--fail-on <severity>", "exit with error code if findings at severity", "high").option("--no-color", "disable colored output").option("-q, --quiet", "minimal output for CI (suppress spinners and decorations)").option("-o, --output <file>", "write output to file").option("--incremental", "only scan changed files (requires git)").option("--diff <ref>", "diff against branch/commit for incremental scan").addHelpText("after", `
43669
+ var scanCommand = new Command("scan").description("Scan a directory or file for security vulnerabilities").argument("[path]", "path to scan", ".").option("-d, --depth <depth>", "scan depth: cheap (free), validated, deep", "cheap").option("-m, --mode <mode>", "alias for --depth (quick, validated, deep)").option("-f, --format <format>", "output format: terminal, json, sarif, markdown", "terminal").option("--fail-on <severity>", "exit with error code if findings at severity", "high").option("--no-color", "disable colored output").option("-q, --quiet", "minimal output for CI (suppress spinners and decorations)").option("-v, --verbose", "show detailed scanner logs (debug mode)").option("-o, --output <file>", "write output to file").option("--incremental", "only scan changed files (requires git)").option("--diff <ref>", "diff against branch/commit for incremental scan").addHelpText("after", `
43602
43670
  Examples:
43603
43671
  $ oculum scan . # Scan current directory (quick/free)
43604
43672
  $ oculum scan ./src # Scan specific directory
@@ -43654,12 +43722,14 @@ async function login(options) {
43654
43722
  console.log(source_default.dim(" " + "\u2500".repeat(38)));
43655
43723
  console.log("");
43656
43724
  spinner.start("Initiating login...");
43657
- const { authUrl, deviceCode } = await initiateLogin();
43725
+ const { authUrl, deviceCode, userCode } = await initiateLogin();
43658
43726
  spinner.stop();
43659
43727
  console.log(source_default.white(" Open this URL in your browser to login:"));
43660
43728
  console.log("");
43661
43729
  console.log(source_default.cyan.bold(` ${authUrl}`));
43662
43730
  console.log("");
43731
+ console.log(source_default.white(" Or enter this code manually: ") + source_default.yellow.bold(userCode));
43732
+ console.log("");
43663
43733
  console.log(source_default.dim(" Waiting for browser authorization..."));
43664
43734
  console.log(source_default.dim(" (This will timeout after 5 minutes)"));
43665
43735
  console.log("");
@@ -46587,6 +46657,7 @@ async function showFeatureExploration() {
46587
46657
  console.log(source_default.dim(" oculum watch . ") + source_default.white("Watch for changes"));
46588
46658
  console.log(source_default.dim(" oculum ui ") + source_default.white("Interactive mode"));
46589
46659
  console.log(source_default.dim(" oculum login ") + source_default.white("Authenticate for Pro features"));
46660
+ console.log(source_default.dim(" oculum usage ") + source_default.white("View credits and quota"));
46590
46661
  console.log(source_default.dim(" oculum --help ") + source_default.white("Show all commands\n"));
46591
46662
  await ve({
46592
46663
  message: "Press Enter to continue",
@@ -47028,6 +47099,8 @@ async function runScanWizard() {
47028
47099
  format,
47029
47100
  failOn,
47030
47101
  color: true,
47102
+ quiet: true,
47103
+ // Suppress scanner logs in interactive UI mode
47031
47104
  incremental,
47032
47105
  diff: diffRef,
47033
47106
  output: outputPath
@@ -47055,7 +47128,9 @@ async function runQuickScan(targetPath = ".") {
47055
47128
  depth: "cheap",
47056
47129
  format: "terminal",
47057
47130
  failOn: "high",
47058
- color: true
47131
+ color: true,
47132
+ quiet: true
47133
+ // Suppress scanner logs in interactive UI mode
47059
47134
  },
47060
47135
  cancelled: false
47061
47136
  };
@@ -47508,7 +47583,9 @@ async function runUI() {
47508
47583
  depth,
47509
47584
  format: "terminal",
47510
47585
  failOn: "high",
47511
- color: true
47586
+ color: true,
47587
+ quiet: true
47588
+ // Suppress scanner logs in interactive UI mode
47512
47589
  });
47513
47590
  console.log(output);
47514
47591
  recordScan();
@@ -47677,6 +47754,119 @@ var uiCommand = new Command("ui").description("Interactive terminal UI").action(
47677
47754
  await runUI();
47678
47755
  });
47679
47756
 
47757
+ // src/commands/usage.ts
47758
+ function formatNumber(num) {
47759
+ if (num === -1) return "unlimited";
47760
+ return num.toLocaleString();
47761
+ }
47762
+ function createProgressBar(percentage, width = 20) {
47763
+ const filled = Math.round(percentage / 100 * width);
47764
+ const empty = width - filled;
47765
+ const bar = "\u2588".repeat(filled) + "\u2591".repeat(empty);
47766
+ if (percentage >= 90) return source_default.red(bar);
47767
+ if (percentage >= 70) return source_default.yellow(bar);
47768
+ return source_default.green(bar);
47769
+ }
47770
+ function formatDate(dateStr) {
47771
+ const date = new Date(dateStr);
47772
+ return date.toLocaleDateString("en-US", {
47773
+ month: "short",
47774
+ day: "numeric",
47775
+ year: "numeric"
47776
+ });
47777
+ }
47778
+ async function usage(options) {
47779
+ const config = getConfig();
47780
+ if (!isAuthenticated()) {
47781
+ console.log("");
47782
+ console.log(source_default.yellow(" \u26A0 Not logged in"));
47783
+ console.log("");
47784
+ console.log(source_default.dim(" Login to view your usage and quota:"));
47785
+ console.log(source_default.cyan(" oculum login"));
47786
+ console.log("");
47787
+ process.exit(1);
47788
+ }
47789
+ const spinner = ora("Fetching usage data...").start();
47790
+ try {
47791
+ const result = await getUsage(config.apiKey);
47792
+ if (!result.success || !result.usage || !result.plan) {
47793
+ spinner.fail("Failed to fetch usage data");
47794
+ console.log("");
47795
+ console.log(source_default.dim(" Error: ") + source_default.red(result.error || "Unknown error"));
47796
+ console.log("");
47797
+ process.exit(1);
47798
+ }
47799
+ spinner.stop();
47800
+ if (options.json) {
47801
+ console.log(JSON.stringify(result, null, 2));
47802
+ return;
47803
+ }
47804
+ const { plan, usage: usageData } = result;
47805
+ console.log("");
47806
+ console.log(source_default.bold(" \u{1F4CA} Oculum Usage"));
47807
+ console.log(source_default.dim(" " + "\u2500".repeat(38)));
47808
+ console.log("");
47809
+ 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 ");
47810
+ console.log(source_default.dim(" Plan: ") + planBadge + source_default.white(` ${plan.displayName}`));
47811
+ console.log(source_default.dim(" Month: ") + source_default.white(usageData.month));
47812
+ console.log("");
47813
+ console.log(source_default.bold(" Credits Usage"));
47814
+ console.log("");
47815
+ const creditsDisplay = usageData.creditsLimit === -1 ? `${formatNumber(usageData.creditsUsed)} / unlimited` : `${formatNumber(usageData.creditsUsed)} / ${formatNumber(usageData.creditsLimit)}`;
47816
+ console.log(source_default.dim(" Used: ") + source_default.white(creditsDisplay));
47817
+ if (usageData.creditsLimit !== -1) {
47818
+ console.log(source_default.dim(" Remaining: ") + source_default.white(formatNumber(usageData.creditsRemaining)));
47819
+ console.log("");
47820
+ console.log(source_default.dim(" ") + createProgressBar(usageData.usagePercentage) + source_default.dim(` ${usageData.usagePercentage.toFixed(1)}%`));
47821
+ }
47822
+ console.log("");
47823
+ console.log(source_default.bold(" This Month"));
47824
+ console.log("");
47825
+ console.log(source_default.dim(" Scans: ") + source_default.white(formatNumber(usageData.totalScans)));
47826
+ console.log(source_default.dim(" Files: ") + source_default.white(formatNumber(usageData.totalFiles)));
47827
+ console.log("");
47828
+ console.log(source_default.dim(" Resets on: ") + source_default.white(formatDate(usageData.resetDate)));
47829
+ console.log("");
47830
+ if (result.recentScans && result.recentScans.length > 0) {
47831
+ console.log(source_default.bold(" Recent Scans"));
47832
+ console.log("");
47833
+ const recentToShow = result.recentScans.slice(0, 5);
47834
+ for (const scan of recentToShow) {
47835
+ const date = new Date(scan.createdAt);
47836
+ const timeAgo = getTimeAgo(date);
47837
+ console.log(source_default.dim(" \u2022 ") + source_default.white(scan.repoName || "unknown") + source_default.dim(` (${timeAgo})`));
47838
+ console.log(source_default.dim(" ") + source_default.dim(`${scan.filesScanned} files, ${scan.findingsCount} findings, ${scan.creditsUsed} credits`));
47839
+ }
47840
+ console.log("");
47841
+ }
47842
+ if (plan.name === "free" || plan.name === "starter") {
47843
+ console.log(source_default.dim(" " + "\u2500".repeat(38)));
47844
+ console.log("");
47845
+ console.log(source_default.dim(" Need more credits? ") + source_default.cyan("oculum upgrade"));
47846
+ console.log("");
47847
+ }
47848
+ } catch (error) {
47849
+ spinner.fail("Failed to fetch usage data");
47850
+ console.log("");
47851
+ console.log(source_default.dim(" Error: ") + source_default.red(String(error)));
47852
+ console.log("");
47853
+ process.exit(1);
47854
+ }
47855
+ }
47856
+ function getTimeAgo(date) {
47857
+ const now = /* @__PURE__ */ new Date();
47858
+ const diffMs = now.getTime() - date.getTime();
47859
+ const diffMins = Math.floor(diffMs / 6e4);
47860
+ const diffHours = Math.floor(diffMs / 36e5);
47861
+ const diffDays = Math.floor(diffMs / 864e5);
47862
+ if (diffMins < 1) return "just now";
47863
+ if (diffMins < 60) return `${diffMins}m ago`;
47864
+ if (diffHours < 24) return `${diffHours}h ago`;
47865
+ if (diffDays < 7) return `${diffDays}d ago`;
47866
+ return date.toLocaleDateString("en-US", { month: "short", day: "numeric" });
47867
+ }
47868
+ var usageCommand = new Command("usage").description("Show current usage and quota").option("--json", "Output as JSON").action(usage);
47869
+
47680
47870
  // src/index.ts
47681
47871
  var program2 = new Command();
47682
47872
  program2.name("oculum").description("AI-native security scanner for detecting vulnerabilities in LLM-generated code").version("1.0.0").addHelpText("after", `
@@ -47691,6 +47881,7 @@ Common Commands:
47691
47881
  ui Interactive terminal UI
47692
47882
  login Authenticate with Oculum
47693
47883
  status Check authentication status
47884
+ usage View credits and quota
47694
47885
 
47695
47886
  Learn More:
47696
47887
  $ oculum scan --help Detailed scan options
@@ -47703,6 +47894,7 @@ program2.addCommand(authCommand);
47703
47894
  program2.addCommand(loginCommand);
47704
47895
  program2.addCommand(statusCommand);
47705
47896
  program2.addCommand(upgradeCommand);
47897
+ program2.addCommand(usageCommand);
47706
47898
  program2.addCommand(watchCommand);
47707
47899
  program2.addCommand(uiCommand);
47708
47900
  program2.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oculum/cli",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
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": {