@oculum/cli 1.0.7 → 1.0.9

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 +859 -133
  2. package/package.json +6 -5
package/dist/index.js CHANGED
@@ -214,9 +214,9 @@ var require_help = __commonJS({
214
214
  */
215
215
  visibleCommands(cmd) {
216
216
  const visibleCommands = cmd.commands.filter((cmd2) => !cmd2._hidden);
217
- const helpCommand = cmd._getHelpCommand();
218
- if (helpCommand && !helpCommand._hidden) {
219
- visibleCommands.push(helpCommand);
217
+ const helpCommand2 = cmd._getHelpCommand();
218
+ if (helpCommand2 && !helpCommand2._hidden) {
219
+ visibleCommands.push(helpCommand2);
220
220
  }
221
221
  if (this.sortSubcommands) {
222
222
  visibleCommands.sort((a, b3) => {
@@ -1304,12 +1304,12 @@ var require_command = __commonJS({
1304
1304
  enableOrNameAndArgs = enableOrNameAndArgs ?? "help [command]";
1305
1305
  const [, helpName, helpArgs] = enableOrNameAndArgs.match(/([^ ]+) *(.*)/);
1306
1306
  const helpDescription = description ?? "display help for command";
1307
- const helpCommand = this.createCommand(helpName);
1308
- helpCommand.helpOption(false);
1309
- if (helpArgs) helpCommand.arguments(helpArgs);
1310
- if (helpDescription) helpCommand.description(helpDescription);
1307
+ const helpCommand2 = this.createCommand(helpName);
1308
+ helpCommand2.helpOption(false);
1309
+ if (helpArgs) helpCommand2.arguments(helpArgs);
1310
+ if (helpDescription) helpCommand2.description(helpDescription);
1311
1311
  this._addImplicitHelpCommand = true;
1312
- this._helpCommand = helpCommand;
1312
+ this._helpCommand = helpCommand2;
1313
1313
  return this;
1314
1314
  }
1315
1315
  /**
@@ -1319,13 +1319,13 @@ var require_command = __commonJS({
1319
1319
  * @param {string} [deprecatedDescription] - deprecated custom description used with custom name only
1320
1320
  * @return {Command} `this` command for chaining
1321
1321
  */
1322
- addHelpCommand(helpCommand, deprecatedDescription) {
1323
- if (typeof helpCommand !== "object") {
1324
- this.helpCommand(helpCommand, deprecatedDescription);
1322
+ addHelpCommand(helpCommand2, deprecatedDescription) {
1323
+ if (typeof helpCommand2 !== "object") {
1324
+ this.helpCommand(helpCommand2, deprecatedDescription);
1325
1325
  return this;
1326
1326
  }
1327
1327
  this._addImplicitHelpCommand = true;
1328
- this._helpCommand = helpCommand;
1328
+ this._helpCommand = helpCommand2;
1329
1329
  return this;
1330
1330
  }
1331
1331
  /**
@@ -11572,6 +11572,8 @@ var require_context_helpers = __commonJS({
11572
11572
  exports2.isDocumentationFile = isDocumentationFile;
11573
11573
  exports2.isScannerOrFixtureFile = isScannerOrFixtureFile;
11574
11574
  exports2.isClientBundledFile = isClientBundledFile;
11575
+ exports2.isSeedOrDataGenFile = isSeedOrDataGenFile;
11576
+ exports2.isEducationalVulnerabilityFile = isEducationalVulnerabilityFile;
11575
11577
  exports2.isEnvVarReference = isEnvVarReference;
11576
11578
  exports2.isNextPublicEnvVar = isNextPublicEnvVar;
11577
11579
  exports2.isComment = isComment;
@@ -11735,6 +11737,35 @@ var require_context_helpers = __commonJS({
11735
11737
  }
11736
11738
  return clientPatterns.some((pattern) => pattern.test(filePath));
11737
11739
  }
11740
+ function isSeedOrDataGenFile(filePath) {
11741
+ const patterns = [
11742
+ /\/seed\//i,
11743
+ /\/seeds\//i,
11744
+ /seed-database\.(ts|js)$/i,
11745
+ /\/seeder\./i,
11746
+ /datacreator\.(ts|js)$/i,
11747
+ /\/data\/.*creator/i,
11748
+ /\/fixtures\//i,
11749
+ /\.fixture\./i,
11750
+ /\/generators?\//i,
11751
+ /\/factories\//i,
11752
+ /factory\.(ts|js)$/i
11753
+ ];
11754
+ return patterns.some((p2) => p2.test(filePath));
11755
+ }
11756
+ function isEducationalVulnerabilityFile(filePath) {
11757
+ const patterns = [
11758
+ /\/insecurity\.(ts|js)$/i,
11759
+ /\/vulnerable\.(ts|js)$/i,
11760
+ /\/intentionally-vulnerable/i,
11761
+ /\/security-examples?\//i,
11762
+ /\/vuln-examples?\//i,
11763
+ /\/challenge-\d+/i,
11764
+ // OWASP Juice Shop challenges
11765
+ /\/exploit-examples?\//i
11766
+ ];
11767
+ return patterns.some((p2) => p2.test(filePath));
11768
+ }
11738
11769
  function isEnvVarReference(line) {
11739
11770
  return /process\.env\.[A-Z_]+/.test(line) || /\$\{?[A-Z_]+\}?/.test(line) || /import\.meta\.env\.[A-Z_]+/.test(line) || /Deno\.env\.get\(/.test(line) || /os\.environ\[/.test(line) || // Python
11740
11771
  /os\.getenv\(/.test(line) || // Python
@@ -11977,12 +12008,12 @@ var require_entropy = __commonJS({
11977
12008
  const strings = [];
11978
12009
  const lines = content.split("\n");
11979
12010
  const patterns = [
11980
- /"([^"\\]|\\.){20,}"/g,
11981
- // Double-quoted strings 20+ chars
11982
- /'([^'\\]|\\.){20,}'/g,
11983
- // Single-quoted strings 20+ chars
11984
- /`([^`\\]|\\.){20,}`/g
11985
- // Template literals 20+ chars
12011
+ /"[^"\\]{20,}(?:\\.[^"\\]*)*"/g,
12012
+ // Double-quoted strings 20+ chars (unrolled loop)
12013
+ /'[^'\\]{20,}(?:\\.[^'\\]*)*'/g,
12014
+ // Single-quoted strings 20+ chars (unrolled loop)
12015
+ /`[^`\\]{20,}(?:\\.[^`\\]*)*`/g
12016
+ // Template literals 20+ chars (unrolled loop)
11986
12017
  ];
11987
12018
  lines.forEach((line, index) => {
11988
12019
  for (const pattern of patterns) {
@@ -15848,19 +15879,11 @@ var require_dangerous_functions = __commonJS({
15848
15879
  // Math.random() * 100 + 50 + 'px'
15849
15880
  /Math\.random.*\*\s*\d+\s*\+\s*\d+.*\bms\b/i,
15850
15881
  // Math.random() * 1000 + 500 + 'ms'
15851
- /Math\.random.*\*\s*\d+\s*\+\s*\d+.*\bs\b/i,
15882
+ /Math\.random.*\*\s*\d+\s*\+\s*\d+.*\bs\b/i
15852
15883
  // Math.random() * 5 + 2 + 's'
15853
- // UI identifier generation (short strings for element IDs, keys, etc.)
15854
- /Math\.random\(\)\.toString\(36\)\.substring\(/,
15855
- // .toString(36).substring(2, 9) - short UI IDs
15856
- /Math\.random\(\)\.toString\(36\)\.substr\(/,
15857
- // .substr() variant
15858
- /Math\.random\(\)\.toString\(36\)\.slice\(/,
15859
- // .slice() variant
15860
- /Math\.random\(\)\.toString\(16\)\.substring\(/,
15861
- // .toString(16).substring() - hex UI IDs
15862
- /Math\.random\(\)\.toString\(16\)\.slice\(/
15863
- // hex slice variant
15884
+ // NOTE: toString patterns removed - now handled by analyzeToStringPattern()
15885
+ // which provides more granular severity classification (info/low/medium/high)
15886
+ // based on truncation length and context
15864
15887
  ];
15865
15888
  if (cosmeticLinePatterns.some((p2) => p2.test(lineContent))) {
15866
15889
  return true;
@@ -15911,6 +15934,71 @@ var require_dangerous_functions = __commonJS({
15911
15934
  }
15912
15935
  return false;
15913
15936
  }
15937
+ function extractFunctionContext(content, lineNumber) {
15938
+ const lines = content.split("\n");
15939
+ const start = Math.max(0, lineNumber - 10);
15940
+ for (let i = lineNumber; i >= start; i--) {
15941
+ const line = lines[i];
15942
+ const funcDeclMatch = line.match(/(?:export\s+)?function\s+(\w+)/i);
15943
+ if (funcDeclMatch) {
15944
+ return funcDeclMatch[1].toLowerCase();
15945
+ }
15946
+ const arrowFuncMatch = line.match(/(?:export\s+)?(?:const|let|var)\s+(\w+)\s*=\s*(?:function|\(|async)/i);
15947
+ if (arrowFuncMatch) {
15948
+ return arrowFuncMatch[1].toLowerCase();
15949
+ }
15950
+ }
15951
+ return null;
15952
+ }
15953
+ function classifyFunctionIntent(functionName) {
15954
+ if (!functionName)
15955
+ return "unknown";
15956
+ const lower = functionName.toLowerCase();
15957
+ const uuidPatterns = ["uuid", "guid", "uniqueid", "correlationid"];
15958
+ const idGenerationPatterns = /^(generate|create|make|build)(id|identifier)$/i;
15959
+ if (uuidPatterns.some((p2) => lower.includes(p2)) || idGenerationPatterns.test(lower)) {
15960
+ return "uuid";
15961
+ }
15962
+ const captchaPatterns = ["captcha", "puzzle", "mathproblem"];
15963
+ if (captchaPatterns.some((p2) => lower.includes(p2)))
15964
+ return "captcha";
15965
+ if (lower.includes("challenge") && !lower.includes("auth"))
15966
+ return "captcha";
15967
+ const demoPatterns = ["seed", "fixture", "demo", "mock", "fake"];
15968
+ if (demoPatterns.some((p2) => lower.includes(p2)))
15969
+ return "demo";
15970
+ const securityPatterns = ["token", "secret", "key", "password", "credential", "signature"];
15971
+ const securityFunctionPattern = /^(generate|create|make)(token|secret|key|session|password|credential)/i;
15972
+ if (securityPatterns.some((p2) => lower.includes(p2)) || securityFunctionPattern.test(lower)) {
15973
+ return "security";
15974
+ }
15975
+ return "unknown";
15976
+ }
15977
+ function analyzeToStringPattern(lineContent) {
15978
+ const toString36Match = lineContent.match(/Math\.random\(\)\.toString\(36\)/);
15979
+ const toString16Match = lineContent.match(/Math\.random\(\)\.toString\(16\)/);
15980
+ if (!toString36Match && !toString16Match) {
15981
+ return { hasToString: false, base: null, isTruncated: false, truncationLength: null, intent: "unknown" };
15982
+ }
15983
+ const base = toString36Match ? 36 : 16;
15984
+ const substringMatch = lineContent.match(/\.substring\((\d+)(?:,\s*(\d+))?\)/);
15985
+ const sliceMatch = lineContent.match(/\.slice\((\d+)(?:,\s*(\d+))?\)/);
15986
+ const substrMatch = lineContent.match(/\.substr\((\d+)(?:,\s*(\d+))?\)/);
15987
+ const truncMatch = substringMatch || sliceMatch || substrMatch;
15988
+ if (!truncMatch) {
15989
+ return { hasToString: true, base, isTruncated: false, truncationLength: null, intent: "full-token" };
15990
+ }
15991
+ const start = parseInt(truncMatch[1]);
15992
+ const end = truncMatch[2] ? parseInt(truncMatch[2]) : null;
15993
+ const length = end ? end - start : null;
15994
+ if (length && length <= 9) {
15995
+ return { hasToString: true, base, isTruncated: true, truncationLength: length, intent: "short-ui-id" };
15996
+ } else if (length && length <= 15) {
15997
+ return { hasToString: true, base, isTruncated: true, truncationLength: length, intent: "business-id" };
15998
+ } else {
15999
+ return { hasToString: true, base, isTruncated: true, truncationLength: length, intent: "business-id" };
16000
+ }
16001
+ }
15914
16002
  function extractMathRandomVariableName(lineContent) {
15915
16003
  const assignmentMatch = lineContent.match(/(?:const|let|var)\s+(\w+)\s*=.*Math\.random/);
15916
16004
  if (assignmentMatch)
@@ -16021,9 +16109,7 @@ var require_dangerous_functions = __commonJS({
16021
16109
  const inUIContext = isCosmeticMathRandom(lineContent, content, lineNumber);
16022
16110
  const businessLogicPatterns = [
16023
16111
  /\b(business|order|invoice|customer|product|transaction)Id\b/i,
16024
- /\b(reference|tracking|confirmation)Number\b/i,
16025
- /\bgenerate.*Id\b/i,
16026
- /\bcreate.*Id\b/i
16112
+ /\b(reference|tracking|confirmation)Number\b/i
16027
16113
  ];
16028
16114
  const inBusinessLogicContext = businessLogicPatterns.some((p2) => p2.test(context)) && !inSecurityContext;
16029
16115
  let contextDescription = "unknown context";
@@ -16267,12 +16353,24 @@ var require_dangerous_functions = __commonJS({
16267
16353
  }
16268
16354
  }
16269
16355
  if (funcPattern.name === "Math.random for security") {
16356
+ if ((0, context_helpers_1.isSeedOrDataGenFile)(filePath)) {
16357
+ break;
16358
+ }
16359
+ if ((0, context_helpers_1.isEducationalVulnerabilityFile)(filePath)) {
16360
+ break;
16361
+ }
16270
16362
  const varName = extractMathRandomVariableName(line);
16271
16363
  const nameRisk = classifyVariableNameRisk(varName);
16272
16364
  const context = analyzeMathRandomContext(content, filePath, index);
16365
+ const functionName = extractFunctionContext(content, index);
16366
+ const functionIntent = classifyFunctionIntent(functionName);
16367
+ const toStringPattern = analyzeToStringPattern(line);
16273
16368
  if (context.inUIContext) {
16274
16369
  break;
16275
16370
  }
16371
+ if (functionIntent === "uuid" || functionIntent === "captcha") {
16372
+ break;
16373
+ }
16276
16374
  let severity2 = "medium";
16277
16375
  let confidence2 = "medium";
16278
16376
  let explanation = "";
@@ -16284,15 +16382,27 @@ var require_dangerous_functions = __commonJS({
16284
16382
  explanation = " (test data generation)";
16285
16383
  description = "Math.random() used in test context for generating mock data. Not security-critical, but consider crypto.randomUUID() for better uniqueness in tests.";
16286
16384
  suggestedFix = "Consider crypto.randomUUID() for test data uniqueness, though Math.random() is acceptable in tests";
16287
- } else if (nameRisk === "high" || context.inSecurityContext) {
16385
+ } else if (functionIntent === "demo") {
16386
+ severity2 = "info";
16387
+ confidence2 = "low";
16388
+ explanation = " (seed/demo data generation)";
16389
+ description = "Math.random() used for generating fixture/seed data. Not security-critical in development contexts.";
16390
+ suggestedFix = "Acceptable for seed data. Use crypto.randomUUID() if uniqueness guarantees needed.";
16391
+ } else if (nameRisk === "high" || context.inSecurityContext || functionIntent === "security") {
16288
16392
  severity2 = "high";
16289
16393
  confidence2 = "high";
16290
16394
  explanation = " (security-sensitive context)";
16291
16395
  description = "Math.random() is NOT cryptographically secure and MUST NOT be used for tokens, keys, passwords, or session IDs. This can lead to predictable values that attackers can exploit.";
16292
16396
  suggestedFix = "Replace with crypto.randomBytes() or crypto.randomUUID() for security-sensitive operations";
16293
- } else if (nameRisk === "low" || context.inBusinessLogicContext) {
16397
+ } else if (toStringPattern.intent === "short-ui-id") {
16398
+ severity2 = "info";
16399
+ confidence2 = "low";
16400
+ explanation = " (UI correlation ID)";
16401
+ description = "Math.random() used for short UI correlation IDs. Not security-critical, but collisions possible in high-volume scenarios.";
16402
+ suggestedFix = "For UI correlation, crypto.randomUUID() provides better uniqueness guarantees";
16403
+ } else if (nameRisk === "low" || context.inBusinessLogicContext || toStringPattern.intent === "business-id") {
16294
16404
  severity2 = "low";
16295
- confidence2 = "medium";
16405
+ confidence2 = "low";
16296
16406
  explanation = " (business identifier)";
16297
16407
  description = "Math.random() is being used for non-security purposes (business IDs, tracking numbers). While not critical, Math.random() can produce collisions in high-volume scenarios.";
16298
16408
  suggestedFix = "Consider crypto.randomUUID() for better uniqueness guarantees and collision resistance";
@@ -36950,6 +37060,35 @@ Distinguish these patterns:
36950
37060
  - Cross-tenant storage: medium (data isolation risk)
36951
37061
  - Do NOT describe transient BYOK keys as "stored without encryption" - they are NOT stored
36952
37062
 
37063
+ **Math.random() for Security:**
37064
+ Distinguish legitimate uses from security-critical misuse:
37065
+ - **Seed/Data Generation Files**: Files in /seed/, /fixtures/, /factories/, datacreator.ts, *.fixture.* are for test data generation
37066
+ - Math.random() in seed files is acceptable - these are never production security code
37067
+ - REJECT findings from seed/data generation files entirely
37068
+ - **Educational Vulnerability Files**: Files named insecurity.ts, vulnerable.ts, or in /intentionally-vulnerable/ paths
37069
+ - These are OWASP Juice Shop challenges or security training examples
37070
+ - REJECT entirely - they're intentionally vulnerable for educational purposes
37071
+ - **UUID/Identifier Generation**: Functions named generateUUID(), createId(), correlationId(), etc.
37072
+ - Use Math.random() for UI correlation, React keys, element IDs
37073
+ - Short toString(36).substring(2, 9) patterns are for UI correlation, NOT security tokens
37074
+ - REJECT unless function name explicitly indicates security (generateToken, createSessionId, generateSecret)
37075
+ - **CAPTCHA/Puzzle Generation**: Math.random() for CAPTCHA questions, puzzle difficulty, game mechanics
37076
+ - These don't need cryptographic randomness - legitimate non-security use
37077
+ - REJECT findings in CAPTCHA/puzzle generation functions
37078
+ - **Security-Sensitive Context**: Only keep as HIGH/CRITICAL when:
37079
+ - Variable names indicate security: token, secret, key, auth, session, password
37080
+ - Function names indicate security: generateToken, createSession, makeSecret
37081
+ - Used in security-critical files: auth.ts, crypto.ts, session.ts
37082
+ - Long toString() patterns without truncation (potential token generation)
37083
+
37084
+ **Severity Ladder for Math.random():**
37085
+ - Seed/educational files: REJECT (not production code)
37086
+ - UUID/CAPTCHA functions: REJECT (legitimate use)
37087
+ - Short UI IDs (toString(36).substring(2, 9)): INFO (UI correlation, suggest crypto.randomUUID())
37088
+ - Business IDs: LOW (suggest crypto.randomUUID() for collision resistance)
37089
+ - Security contexts (tokens/secrets/keys): HIGH (cryptographic weakness)
37090
+ - Unknown context: MEDIUM (needs manual review)
37091
+
36953
37092
  ### 3.6 DOM Sinks and Bootstrap Scripts
36954
37093
  Recognise LOW-RISK patterns:
36955
37094
  - Static scripts reading localStorage for theme/preferences
@@ -37130,19 +37269,23 @@ AI-generated structured outputs need validation before use in security-sensitive
37130
37269
  - Generic success messages
37131
37270
  - Placeholder comments in non-security code
37132
37271
 
37133
- ## Response Format
37272
+ ## Response Format (OPTIMIZED FOR MINIMAL OUTPUT)
37134
37273
 
37135
37274
  For each candidate finding, return:
37136
37275
  \`\`\`json
37137
37276
  {
37138
37277
  "index": <number>,
37139
37278
  "keep": true | false,
37140
- "reason": "<brief explanation referencing specific code/context>",
37141
- "adjustedSeverity": "critical" | "high" | "medium" | "low" | "info" | null,
37142
- "validationNotes": "<optional: additional context for the developer>"
37279
+ "adjustedSeverity": "critical" | "high" | "medium" | "low" | "info" | null, // Only if keep=true
37280
+ "notes": "<concise context for developer>" // Only if keep=true, 1-2 sentences max
37143
37281
  }
37144
37282
  \`\`\`
37145
37283
 
37284
+ **CRITICAL**: To minimize costs:
37285
+ - For \`keep: false\` (rejected): ONLY include \`index\` and \`keep\` fields. NO explanation needed.
37286
+ - For \`keep: true\` (accepted): Include \`notes\` field with brief context (10-30 words). Be concise.
37287
+ - Omit \`adjustedSeverity\` if keeping original severity (null is wasteful).
37288
+
37146
37289
  ## Severity Guidelines
37147
37290
  - **critical/high**: Realistically exploitable, should block deploys - ONLY for clear vulnerabilities
37148
37291
  - **medium/low**: Important but non-blocking, hardening opportunities - use sparingly
@@ -37165,7 +37308,35 @@ For each candidate finding, return:
37165
37308
  - No visible mitigating factors in context
37166
37309
  - Real-world attack scenario is plausible
37167
37310
 
37168
- **REMEMBER**: You are the last line of defense against noise. A finding that reaches the user should be CLEARLY worth their time. When in doubt, REJECT.`;
37311
+ **REMEMBER**: You are the last line of defense against noise. A finding that reaches the user should be CLEARLY worth their time. When in doubt, REJECT.
37312
+
37313
+ ## Response Format
37314
+
37315
+ For EACH file, provide a JSON object with the file path and validation results.
37316
+ Return a JSON array where each element has:
37317
+ - "file": the file path (e.g., "src/routes/api.ts")
37318
+ - "validations": array of validation results for that file's candidates
37319
+
37320
+ Example response format (OPTIMIZED):
37321
+ \`\`\`json
37322
+ [
37323
+ {
37324
+ "file": "src/auth.ts",
37325
+ "validations": [
37326
+ { "index": 0, "keep": true, "adjustedSeverity": "medium", "notes": "Protected by middleware" },
37327
+ { "index": 1, "keep": false }
37328
+ ]
37329
+ },
37330
+ {
37331
+ "file": "src/api.ts",
37332
+ "validations": [
37333
+ { "index": 0, "keep": true, "notes": "User input flows to SQL query" }
37334
+ ]
37335
+ }
37336
+ ]
37337
+ \`\`\`
37338
+
37339
+ **REMEMBER**: Rejected findings (keep: false) need NO explanation. Keep notes brief (10-30 words).`;
37169
37340
  var cachedProjectContext = null;
37170
37341
  async function makeAnthropicRequestWithRetry(requestFn, maxRetries = 3, initialDelayMs = 1e3) {
37171
37342
  let lastError = null;
@@ -37279,7 +37450,45 @@ For each candidate finding, return:
37279
37450
  { role: "system", content: HIGH_CONTEXT_VALIDATION_PROMPT },
37280
37451
  { role: "user", content: validationRequest }
37281
37452
  ],
37282
- max_completion_tokens: 4096
37453
+ max_completion_tokens: 1500,
37454
+ // Reduced from 4096 - optimized format needs less output
37455
+ response_format: {
37456
+ type: "json_schema",
37457
+ json_schema: {
37458
+ name: "validation_response",
37459
+ strict: true,
37460
+ schema: {
37461
+ type: "object",
37462
+ properties: {
37463
+ validations: {
37464
+ type: "array",
37465
+ items: {
37466
+ type: "object",
37467
+ properties: {
37468
+ file: { type: "string" },
37469
+ validations: {
37470
+ type: "array",
37471
+ items: {
37472
+ type: "object",
37473
+ properties: {
37474
+ index: { type: "number" },
37475
+ keep: { type: "boolean" }
37476
+ },
37477
+ required: ["index", "keep"],
37478
+ additionalProperties: true
37479
+ }
37480
+ }
37481
+ },
37482
+ required: ["file", "validations"],
37483
+ additionalProperties: false
37484
+ }
37485
+ }
37486
+ },
37487
+ required: ["validations"],
37488
+ additionalProperties: false
37489
+ }
37490
+ }
37491
+ }
37283
37492
  }));
37284
37493
  statsLock.apiCalls++;
37285
37494
  const usage2 = response.usage;
@@ -37311,8 +37520,18 @@ For each candidate finding, return:
37311
37520
  }
37312
37521
  return batchFindings;
37313
37522
  }
37523
+ let parsedContent;
37524
+ try {
37525
+ parsedContent = JSON.parse(content);
37526
+ if (parsedContent.validations && Array.isArray(parsedContent.validations)) {
37527
+ parsedContent = parsedContent.validations;
37528
+ }
37529
+ } catch (e2) {
37530
+ console.warn("[OpenAI] Failed to parse JSON response:", e2);
37531
+ parsedContent = content;
37532
+ }
37314
37533
  const expectedFiles = fileDataList.map(({ filePath }) => filePath);
37315
- const validationResultsMap = parseMultiFileValidationResponse(content, expectedFiles);
37534
+ const validationResultsMap = parseMultiFileValidationResponse(typeof parsedContent === "string" ? parsedContent : JSON.stringify(parsedContent), expectedFiles);
37316
37535
  for (const { filePath, findings: fileFindings } of fileDataList) {
37317
37536
  const fileResults = validationResultsMap.get(filePath);
37318
37537
  if (!fileResults || fileResults.length === 0) {
@@ -37501,8 +37720,8 @@ For each candidate finding, return:
37501
37720
  const validationRequest = buildMultiFileValidationRequest(fileDataList.map(({ file, findings: findings2 }) => ({ file, findings: findings2 })), context);
37502
37721
  const response = await makeAnthropicRequestWithRetry(() => client.messages.create({
37503
37722
  model: "claude-3-5-haiku-20241022",
37504
- max_tokens: 4096,
37505
- // Increased for multi-file responses
37723
+ max_tokens: 1500,
37724
+ // Reduced from 4096 - optimized format needs less output
37506
37725
  system: [
37507
37726
  {
37508
37727
  type: "text",
@@ -37696,14 +37915,14 @@ Example response format:
37696
37915
  {
37697
37916
  "file": "src/auth.ts",
37698
37917
  "validations": [
37699
- { "index": 0, "keep": true, "reason": "Valid finding", "adjustedSeverity": null, "validationNotes": "..." },
37700
- { "index": 1, "keep": false, "reason": "False positive because..." }
37918
+ { "index": 0, "keep": true, "adjustedSeverity": "medium", "notes": "Protected by middleware" },
37919
+ { "index": 1, "keep": false }
37701
37920
  ]
37702
37921
  },
37703
37922
  {
37704
37923
  "file": "src/api.ts",
37705
37924
  "validations": [
37706
- { "index": 0, "keep": true, "reason": "...", "adjustedSeverity": "high", "validationNotes": "..." }
37925
+ { "index": 0, "keep": true, "notes": "User input flows to SQL query" }
37707
37926
  ]
37708
37927
  }
37709
37928
  ]
@@ -37771,13 +37990,18 @@ Remember: Be AGGRESSIVE in rejecting false positives. Use the full file context
37771
37990
  continue;
37772
37991
  }
37773
37992
  const filePath = fileResult.file;
37774
- const validations = fileResult.validations.filter((item) => typeof item.index === "number" && typeof item.keep === "boolean").map((item) => ({
37775
- index: item.index,
37776
- keep: item.keep,
37777
- reason: item.reason || "",
37778
- adjustedSeverity: item.adjustedSeverity || null,
37779
- validationNotes: item.validationNotes || void 0
37780
- }));
37993
+ const validations = fileResult.validations.filter((item) => typeof item.index === "number" && typeof item.keep === "boolean").map((item) => {
37994
+ const notes = item.notes || item.validationNotes || item.reason || void 0;
37995
+ return {
37996
+ index: item.index,
37997
+ keep: item.keep,
37998
+ notes,
37999
+ adjustedSeverity: item.adjustedSeverity || null,
38000
+ // Keep legacy fields for backward compatibility
38001
+ reason: item.reason,
38002
+ validationNotes: item.validationNotes
38003
+ };
38004
+ });
37781
38005
  resultMap.set(filePath, validations);
37782
38006
  }
37783
38007
  for (const expectedFile of expectedFiles) {
@@ -37810,18 +38034,19 @@ Remember: Be AGGRESSIVE in rejecting false positives. Use the full file context
37810
38034
  validatedByAI: true,
37811
38035
  confidence: "high"
37812
38036
  };
38037
+ const validationNotes = validation.notes || validation.validationNotes || validation.reason || void 0;
37813
38038
  if (validation.adjustedSeverity && validation.adjustedSeverity !== finding.severity) {
37814
38039
  adjustedFinding.originalSeverity = finding.severity;
37815
38040
  adjustedFinding.severity = validation.adjustedSeverity;
37816
38041
  adjustedFinding.validationStatus = "downgraded";
37817
- adjustedFinding.validationNotes = validation.validationNotes || validation.reason || "Severity adjusted by AI validation";
38042
+ adjustedFinding.validationNotes = validationNotes || "Severity adjusted by AI validation";
37818
38043
  } else {
37819
38044
  adjustedFinding.validationStatus = "confirmed";
37820
- adjustedFinding.validationNotes = validation.validationNotes || validation.reason;
38045
+ adjustedFinding.validationNotes = validationNotes;
37821
38046
  }
37822
38047
  processed.push(adjustedFinding);
37823
38048
  } else {
37824
- console.log(`[AI Validation] Rejected: ${finding.title} at ${finding.filePath}:${finding.lineNumber} - ${validation.reason}`);
38049
+ console.log(`[AI Validation] Rejected: ${finding.title} at ${finding.filePath}:${finding.lineNumber}`);
37825
38050
  }
37826
38051
  }
37827
38052
  return processed;
@@ -37894,13 +38119,18 @@ Remember: Be AGGRESSIVE in rejecting false positives. Use the full file context
37894
38119
  const parsed = JSON.parse(jsonSlice);
37895
38120
  if (!Array.isArray(parsed))
37896
38121
  return [];
37897
- return parsed.filter((item) => typeof item.index === "number" && typeof item.keep === "boolean").map((item) => ({
37898
- index: item.index,
37899
- keep: item.keep,
37900
- reason: item.reason || "",
37901
- adjustedSeverity: item.adjustedSeverity || null,
37902
- validationNotes: item.validationNotes || void 0
37903
- }));
38122
+ return parsed.filter((item) => typeof item.index === "number" && typeof item.keep === "boolean").map((item) => {
38123
+ const notes = item.notes || item.validationNotes || item.reason || void 0;
38124
+ return {
38125
+ index: item.index,
38126
+ keep: item.keep,
38127
+ notes,
38128
+ adjustedSeverity: item.adjustedSeverity || null,
38129
+ // Keep legacy fields for backward compatibility
38130
+ reason: item.reason,
38131
+ validationNotes: item.validationNotes
38132
+ };
38133
+ });
37904
38134
  } catch (error) {
37905
38135
  console.error("Failed to parse validation response:", error);
37906
38136
  return [];
@@ -43239,8 +43469,11 @@ function enhanceAPIError(error) {
43239
43469
  case 401:
43240
43470
  return {
43241
43471
  message: "Authentication required",
43242
- suggestion: "Run `oculum login` to authenticate, or use `--depth cheap` for free scans.",
43472
+ suggestion: "You need to login to use AI-powered scans.",
43243
43473
  category: "auth",
43474
+ errorCode: "OCU-E401",
43475
+ quickFix: "oculum login",
43476
+ learnMoreUrl: "https://oculum.dev/docs/authentication",
43244
43477
  recoveryActions: [
43245
43478
  { label: "Login", command: "oculum login", action: "login" },
43246
43479
  { label: "Use free scan", action: "fallback" }
@@ -43250,8 +43483,11 @@ function enhanceAPIError(error) {
43250
43483
  if (error.reason === "insufficient_tier") {
43251
43484
  return {
43252
43485
  message: "This feature requires a Pro subscription",
43253
- suggestion: "Visit https://oculum.dev/billing to upgrade your plan.",
43486
+ suggestion: "Validated and deep scans require a Pro plan.",
43254
43487
  category: "auth",
43488
+ errorCode: "OCU-E403-TIER",
43489
+ quickFix: "oculum scan . --depth cheap",
43490
+ learnMoreUrl: "https://oculum.dev/billing",
43255
43491
  recoveryActions: [
43256
43492
  { label: "View pricing", action: "upgrade" },
43257
43493
  { label: "Use free scan", action: "fallback" }
@@ -43261,8 +43497,11 @@ function enhanceAPIError(error) {
43261
43497
  if (error.reason === "expired") {
43262
43498
  return {
43263
43499
  message: "Your API key has expired",
43264
- suggestion: "Generate a new key at https://oculum.dev/dashboard/api-keys",
43500
+ suggestion: "Generate a new key to continue using AI-powered scans.",
43265
43501
  category: "auth",
43502
+ errorCode: "OCU-E403-EXP",
43503
+ quickFix: "oculum login",
43504
+ learnMoreUrl: "https://oculum.dev/dashboard/api-keys",
43266
43505
  recoveryActions: [
43267
43506
  { label: "Login again", command: "oculum login", action: "login" }
43268
43507
  ]
@@ -43271,8 +43510,11 @@ function enhanceAPIError(error) {
43271
43510
  if (error.reason === "invalid_key") {
43272
43511
  return {
43273
43512
  message: "Invalid API key",
43274
- suggestion: "Your API key is not recognized. Please login again or check your key.",
43513
+ suggestion: "Your API key is not recognized.",
43275
43514
  category: "auth",
43515
+ errorCode: "OCU-E403-KEY",
43516
+ quickFix: "oculum login",
43517
+ learnMoreUrl: "https://oculum.dev/dashboard/api-keys",
43276
43518
  recoveryActions: [
43277
43519
  { label: "Login", command: "oculum login", action: "login" },
43278
43520
  { label: "Use free scan", action: "fallback" }
@@ -43281,19 +43523,24 @@ function enhanceAPIError(error) {
43281
43523
  }
43282
43524
  return {
43283
43525
  message: "Access denied",
43284
- suggestion: "Check your API key permissions or run `oculum login` to re-authenticate.",
43526
+ suggestion: "Check your API key permissions.",
43285
43527
  category: "auth",
43528
+ errorCode: "OCU-E403",
43529
+ quickFix: "oculum login",
43286
43530
  recoveryActions: [
43287
43531
  { label: "Login", command: "oculum login", action: "login" }
43288
43532
  ]
43289
43533
  };
43290
43534
  case 429:
43291
43535
  const rateLimitInfo = error.reason === "quota_exceeded" ? "You've reached your monthly scan quota." : "Too many requests in a short period.";
43292
- 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.";
43536
+ const rateLimitSuggestion = error.reason === "quota_exceeded" ? "Your quota resets at the start of next month." : "Wait a moment before trying again.";
43293
43537
  return {
43294
43538
  message: rateLimitInfo,
43295
43539
  suggestion: rateLimitSuggestion,
43296
43540
  category: "server",
43541
+ errorCode: error.reason === "quota_exceeded" ? "OCU-E429-QUOTA" : "OCU-E429-RATE",
43542
+ quickFix: "oculum scan . --depth cheap",
43543
+ learnMoreUrl: "https://oculum.dev/dashboard/usage",
43297
43544
  recoveryActions: [
43298
43545
  { label: "Use free local scan", command: "oculum scan . --depth cheap", action: "fallback" },
43299
43546
  { label: "View usage & upgrade", action: "upgrade" },
@@ -43306,8 +43553,11 @@ function enhanceAPIError(error) {
43306
43553
  case 503:
43307
43554
  return {
43308
43555
  message: "Oculum servers are experiencing issues",
43309
- suggestion: "Try again in a few minutes. Check https://status.oculum.dev for updates.",
43556
+ suggestion: "This is temporary. Try again in a few minutes.",
43310
43557
  category: "server",
43558
+ errorCode: `OCU-E${error.statusCode}`,
43559
+ quickFix: "oculum scan . --depth cheap",
43560
+ learnMoreUrl: "https://status.oculum.dev",
43311
43561
  recoveryActions: [
43312
43562
  { label: "Retry", action: "retry" },
43313
43563
  { label: "Use free scan (offline)", action: "fallback" }
@@ -43316,15 +43566,22 @@ function enhanceAPIError(error) {
43316
43566
  case 504:
43317
43567
  return {
43318
43568
  message: "Request timed out",
43319
- suggestion: "The scan may be too large. Try scanning fewer files or use `--depth cheap`.",
43569
+ suggestion: "The scan may be too large for the server.",
43320
43570
  category: "server",
43571
+ errorCode: "OCU-E504",
43572
+ quickFix: "oculum scan . --depth cheap",
43573
+ learnMoreUrl: "https://oculum.dev/docs/troubleshooting#timeout",
43321
43574
  recoveryActions: [
43322
43575
  { label: "Retry", action: "retry" },
43323
43576
  { label: "Use quick scan", action: "fallback" }
43324
43577
  ]
43325
43578
  };
43326
43579
  default:
43327
- return { message: error.message, category: "unknown" };
43580
+ return {
43581
+ message: error.message,
43582
+ category: "unknown",
43583
+ errorCode: "OCU-E000"
43584
+ };
43328
43585
  }
43329
43586
  }
43330
43587
  function detectNetworkErrorType(msg) {
@@ -43372,6 +43629,7 @@ function enhanceStandardError(error) {
43372
43629
  message: `Path not found: ${path2}`,
43373
43630
  suggestion: "Check that the path exists and is spelled correctly.",
43374
43631
  category: "file",
43632
+ errorCode: "OCU-E001",
43375
43633
  details: "The file or directory does not exist at the specified location."
43376
43634
  };
43377
43635
  }
@@ -43380,15 +43638,33 @@ function enhanceStandardError(error) {
43380
43638
  message: "Permission denied",
43381
43639
  suggestion: "Check file permissions or try running with appropriate access.",
43382
43640
  category: "file",
43641
+ errorCode: "OCU-E002",
43383
43642
  details: "You do not have permission to access this file or directory."
43384
43643
  };
43385
43644
  }
43645
+ if (msg.includes("self-signed") || msg.includes("unable to verify") || msg.includes("certificate") && (msg.includes("invalid") || msg.includes("expired"))) {
43646
+ return {
43647
+ message: "SSL certificate error",
43648
+ suggestion: "Your network may be intercepting HTTPS traffic (corporate proxy).",
43649
+ category: "network",
43650
+ errorCode: "OCU-E003",
43651
+ quickFix: "oculum scan . --depth cheap",
43652
+ learnMoreUrl: "https://oculum.dev/docs/troubleshooting#ssl",
43653
+ recoveryActions: [
43654
+ { label: "Use offline scan", command: "oculum scan . --depth cheap", action: "fallback" },
43655
+ { label: "View help", action: "help" }
43656
+ ]
43657
+ };
43658
+ }
43386
43659
  if (msg.includes("fetch failed") || msg.includes("network") || msg.includes("econnrefused") || msg.includes("enotfound") || msg.includes("etimedout") || msg.includes("econnreset") || msg.includes("socket")) {
43387
43660
  const { type, suggestion } = detectNetworkErrorType(msg);
43388
43661
  return {
43389
43662
  message: type,
43390
43663
  suggestion,
43391
43664
  category: "network",
43665
+ errorCode: "OCU-E004",
43666
+ quickFix: "oculum scan . --depth cheap",
43667
+ learnMoreUrl: "https://oculum.dev/docs/troubleshooting#network",
43392
43668
  recoveryActions: [
43393
43669
  { label: "Retry", action: "retry" },
43394
43670
  { label: "Use offline scan", action: "fallback" }
@@ -43400,52 +43676,105 @@ function enhanceStandardError(error) {
43400
43676
  message: "Invalid configuration file",
43401
43677
  suggestion: "Check your config file for valid JSON syntax.",
43402
43678
  category: "file",
43679
+ errorCode: "OCU-E005",
43680
+ learnMoreUrl: "https://oculum.dev/docs/configuration",
43403
43681
  details: "The configuration file contains invalid JSON. Use a JSON validator to find the error."
43404
43682
  };
43405
43683
  }
43406
43684
  if (msg.includes("no scannable files")) {
43407
43685
  return {
43408
43686
  message: "No scannable files found",
43409
- suggestion: "Check that the path contains supported file types (.js, .ts, .py, .go, etc.)",
43687
+ suggestion: "Check that the path contains supported file types.",
43410
43688
  category: "scan",
43411
- details: "Supported extensions: .js, .jsx, .ts, .tsx, .py, .go, .java, .rb, .php, .yaml, .json"
43689
+ errorCode: "OCU-E006",
43690
+ details: "Supported: .js, .jsx, .ts, .tsx, .py, .go, .java, .rb, .php, .yaml, .json"
43412
43691
  };
43413
43692
  }
43414
43693
  if (msg.includes("symlink") || msg.includes("eloop")) {
43415
43694
  return {
43416
43695
  message: "Symbolic link error",
43417
43696
  suggestion: "There may be a circular symlink. Try scanning a specific directory instead.",
43418
- category: "file"
43697
+ category: "file",
43698
+ errorCode: "OCU-E007"
43419
43699
  };
43420
43700
  }
43421
43701
  if (msg.includes("heap") || msg.includes("memory") || msg.includes("enomem")) {
43422
43702
  return {
43423
43703
  message: "Out of memory",
43424
- suggestion: "The scan ran out of memory. Try scanning fewer files or a smaller directory.",
43425
- category: "scan"
43704
+ suggestion: "The scan ran out of memory. Try scanning fewer files.",
43705
+ category: "scan",
43706
+ errorCode: "OCU-E008",
43707
+ quickFix: 'NODE_OPTIONS="--max-old-space-size=4096" oculum scan .',
43708
+ learnMoreUrl: "https://oculum.dev/docs/troubleshooting#memory"
43426
43709
  };
43427
43710
  }
43428
- return { message: error.message, category: "unknown" };
43711
+ if (msg.includes("enospc") || msg.includes("no space left")) {
43712
+ return {
43713
+ message: "Disk full",
43714
+ suggestion: "Free up disk space and try again.",
43715
+ category: "file",
43716
+ errorCode: "OCU-E009"
43717
+ };
43718
+ }
43719
+ return {
43720
+ message: error.message,
43721
+ category: "unknown",
43722
+ errorCode: "OCU-E000",
43723
+ learnMoreUrl: "https://oculum.dev/docs/troubleshooting"
43724
+ };
43429
43725
  }
43430
43726
  function formatError(enhanced) {
43431
- let output = source_default.red(`Error: ${enhanced.message}`);
43727
+ const icon = getErrorIcon(enhanced.category);
43728
+ const lines = [];
43729
+ if (enhanced.errorCode) {
43730
+ lines.push(source_default.red.bold(`${icon} ${enhanced.errorCode} ${enhanced.message}`));
43731
+ } else {
43732
+ lines.push(source_default.red.bold(`${icon} ${enhanced.message}`));
43733
+ }
43734
+ lines.push("");
43432
43735
  if (enhanced.suggestion) {
43433
- output += "\n" + source_default.dim(`Tip: ${enhanced.suggestion}`);
43736
+ lines.push(source_default.white(enhanced.suggestion));
43434
43737
  }
43435
43738
  if (enhanced.details) {
43436
- output += "\n" + source_default.gray(`Details: ${enhanced.details}`);
43739
+ lines.push(source_default.dim(enhanced.details));
43740
+ }
43741
+ if (enhanced.quickFix) {
43742
+ lines.push("");
43743
+ lines.push(source_default.bold("Quick fix:"));
43744
+ lines.push(source_default.cyan(` $ ${enhanced.quickFix}`));
43437
43745
  }
43438
43746
  if (enhanced.recoveryActions && enhanced.recoveryActions.length > 0) {
43439
- output += "\n\n" + source_default.dim("Possible actions:");
43747
+ lines.push("");
43748
+ lines.push(source_default.dim("Or try:"));
43440
43749
  for (const action of enhanced.recoveryActions) {
43441
43750
  if (action.command) {
43442
- output += "\n " + source_default.cyan(`\u2022 ${action.label}: `) + source_default.white(action.command);
43751
+ lines.push(source_default.dim(" - ") + source_default.white(action.label + ": ") + source_default.cyan(action.command));
43443
43752
  } else {
43444
- output += "\n " + source_default.cyan(`\u2022 ${action.label}`);
43753
+ lines.push(source_default.dim(" - ") + source_default.white(action.label));
43445
43754
  }
43446
43755
  }
43447
43756
  }
43448
- return output;
43757
+ if (enhanced.learnMoreUrl) {
43758
+ lines.push("");
43759
+ lines.push(source_default.dim("Need help? ") + source_default.cyan.underline(enhanced.learnMoreUrl));
43760
+ }
43761
+ return lines.join("\n");
43762
+ }
43763
+ function getErrorIcon(category) {
43764
+ switch (category) {
43765
+ case "network":
43766
+ return "\u{1F310}";
43767
+ case "auth":
43768
+ return "\u{1F510}";
43769
+ case "file":
43770
+ return "\u{1F4C1}";
43771
+ case "scan":
43772
+ return "\u{1F50D}";
43773
+ case "server":
43774
+ return "\u{1F5A5}\uFE0F";
43775
+ default:
43776
+ return "\u274C";
43777
+ }
43449
43778
  }
43450
43779
 
43451
43780
  // src/utils/project-config.ts
@@ -43946,7 +44275,7 @@ async function runScanOnce(targetPath, options) {
43946
44275
  }
43947
44276
  try {
43948
44277
  spinner.start("Starting scan...");
43949
- const hasLocalAI = !!process.env.ANTHROPIC_API_KEY;
44278
+ const hasLocalAI = !!(process.env.ANTHROPIC_API_KEY || process.env.OPENAI_API_KEY);
43950
44279
  if (options.depth !== "cheap" && isAuthenticated() && !hasLocalAI) {
43951
44280
  spinner.text = `Backend ${options.depth} scan analyzing ${files.length} files...`;
43952
44281
  result = await callBackendAPI(
@@ -44066,26 +44395,37 @@ async function runScan(targetPath, cliOptions) {
44066
44395
  }
44067
44396
  }
44068
44397
  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", `
44398
+ Scan Modes:
44399
+ cheap Free Fast pattern matching, runs locally
44400
+ Best for: Quick checks, CI/CD pipelines
44401
+
44402
+ validated ~$0.03 AI validates findings, ~70% fewer false positives
44403
+ Best for: Pre-commit checks, PR reviews
44404
+
44405
+ deep ~$0.10 Full semantic analysis with AI reasoning
44406
+ Best for: Security audits, release checks
44407
+
44069
44408
  Examples:
44070
- $ oculum scan . # Scan current directory (quick/free)
44071
- $ oculum scan ./src # Scan specific directory
44072
- $ oculum scan . --mode validated # AI-validated scan (Pro)
44073
- $ oculum scan . -d validated # Same as above (--depth alias)
44074
- $ oculum scan . --incremental # Diff scan vs HEAD (changed files only)
44075
- $ oculum scan . --incremental --mode validated # Diff scan with AI validation
44076
- $ oculum scan . --diff origin/main --mode validated # Diff vs base branch
44077
- $ oculum scan . -f sarif -o out # Output SARIF for GitHub
44078
- $ oculum scan . --fail-on critical # Only fail on critical issues
44079
- $ oculum scan . -q # Quiet mode for CI/CD
44080
-
44081
- Scan Modes (--mode or --depth):
44082
- quick Fast pattern matching, runs locally (free) [alias: cheap]
44083
- validated AI validates findings, ~70% fewer false positives (Pro)
44084
- deep Full semantic analysis with AI reasoning (Pro)
44085
-
44086
- Project Config:
44087
- Create oculum.config.json in your project root to set defaults:
44088
- { "depth": "cheap", "format": "terminal", "failOn": "high" }
44409
+ $ oculum scan . Scan current directory (free)
44410
+ $ oculum scan . -d validated AI-validated scan
44411
+ $ oculum scan ./src --fail-on high Fail CI on high severity findings
44412
+ $ oculum scan . -f sarif -o report Export for GitHub Code Scanning
44413
+ $ oculum scan . --incremental Only scan changed files (git)
44414
+ $ oculum scan . --diff origin/main Compare against base branch
44415
+ $ oculum scan . -q Quiet mode for CI/CD
44416
+
44417
+ Configuration:
44418
+ Create oculum.config.json in your project:
44419
+ {
44420
+ "depth": "validated",
44421
+ "failOn": "high",
44422
+ "ignore": ["**/test/**"]
44423
+ }
44424
+
44425
+ More Help:
44426
+ $ oculum help scan-modes Detailed mode comparison
44427
+ $ oculum help ci-setup CI/CD integration examples
44428
+ $ oculum help config Full configuration options
44089
44429
  `).action(runScan);
44090
44430
 
44091
44431
  // src/commands/auth.ts
@@ -44788,10 +45128,10 @@ var foreach = (val, fn) => {
44788
45128
  fn(val);
44789
45129
  }
44790
45130
  };
44791
- var addAndConvert = (main2, prop, item) => {
44792
- let container = main2[prop];
45131
+ var addAndConvert = (main3, prop, item) => {
45132
+ let container = main3[prop];
44793
45133
  if (!(container instanceof Set)) {
44794
- main2[prop] = container = /* @__PURE__ */ new Set([container]);
45134
+ main3[prop] = container = /* @__PURE__ */ new Set([container]);
44795
45135
  }
44796
45136
  container.add(item);
44797
45137
  };
@@ -44803,12 +45143,12 @@ var clearItem = (cont) => (key) => {
44803
45143
  delete cont[key];
44804
45144
  }
44805
45145
  };
44806
- var delFromSet = (main2, prop, item) => {
44807
- const container = main2[prop];
45146
+ var delFromSet = (main3, prop, item) => {
45147
+ const container = main3[prop];
44808
45148
  if (container instanceof Set) {
44809
45149
  container.delete(item);
44810
45150
  } else if (container === item) {
44811
- delete main2[prop];
45151
+ delete main3[prop];
44812
45152
  }
44813
45153
  };
44814
45154
  var isEmptySet = (val) => val instanceof Set ? val.size === 0 : !val;
@@ -46853,6 +47193,7 @@ var CONFIG_DIR3 = (0, import_path6.join)((0, import_os4.homedir)(), ".oculum");
46853
47193
  var STATE_FILE = (0, import_path6.join)(CONFIG_DIR3, "state.json");
46854
47194
  var DEFAULT_STATE = {
46855
47195
  onboardingComplete: false,
47196
+ setupWizardComplete: false,
46856
47197
  totalScans: 0,
46857
47198
  welcomeShown: false
46858
47199
  };
@@ -46881,7 +47222,7 @@ function updateUserState(updates) {
46881
47222
  }
46882
47223
  function isFirstTimeUser() {
46883
47224
  const state = getUserState();
46884
- return !state.onboardingComplete && state.totalScans === 0;
47225
+ return !state.setupWizardComplete && !state.onboardingComplete && state.totalScans === 0;
46885
47226
  }
46886
47227
  function shouldShowWelcome() {
46887
47228
  const state = getUserState();
@@ -46906,8 +47247,7 @@ function recordScan() {
46906
47247
  }
46907
47248
 
46908
47249
  // src/ui/onboarding.ts
46909
- async function showWelcomeScreen() {
46910
- console.clear();
47250
+ function showLogo() {
46911
47251
  console.log(source_default.cyan(`
46912
47252
  \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2557
46913
47253
  \u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2551
@@ -46916,6 +47256,10 @@ async function showWelcomeScreen() {
46916
47256
  \u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u255A\u2550\u255D \u2588\u2588\u2551
46917
47257
  \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D
46918
47258
  `));
47259
+ }
47260
+ async function showWelcomeScreen() {
47261
+ console.clear();
47262
+ showLogo();
46919
47263
  console.log(source_default.bold.white(" AI-Native Security Scanner for Modern Codebases\n"));
46920
47264
  console.log(source_default.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n"));
46921
47265
  console.log(source_default.white(" Oculum detects security vulnerabilities in AI-generated"));
@@ -47972,11 +48316,136 @@ async function runHistoryFlow() {
47972
48316
  }
47973
48317
  }
47974
48318
  }
48319
+ function formatNumber(num) {
48320
+ if (num === -1) return "unlimited";
48321
+ return num.toLocaleString();
48322
+ }
48323
+ function createProgressBar(percentage, width = 20) {
48324
+ const filled = Math.round(percentage / 100 * width);
48325
+ const empty = width - filled;
48326
+ const bar = "\u2588".repeat(filled) + "\u2591".repeat(empty);
48327
+ if (percentage >= 90) return source_default.red(bar);
48328
+ if (percentage >= 70) return source_default.yellow(bar);
48329
+ return source_default.green(bar);
48330
+ }
48331
+ function formatDate(dateStr) {
48332
+ const date = new Date(dateStr);
48333
+ return date.toLocaleDateString("en-US", {
48334
+ month: "short",
48335
+ day: "numeric",
48336
+ year: "numeric"
48337
+ });
48338
+ }
48339
+ function getTimeAgo(date) {
48340
+ const now = /* @__PURE__ */ new Date();
48341
+ const diffMs = now.getTime() - date.getTime();
48342
+ const diffMins = Math.floor(diffMs / 6e4);
48343
+ const diffHours = Math.floor(diffMs / 36e5);
48344
+ const diffDays = Math.floor(diffMs / 864e5);
48345
+ if (diffMins < 1) return "just now";
48346
+ if (diffMins < 60) return `${diffMins}m ago`;
48347
+ if (diffHours < 24) return `${diffHours}h ago`;
48348
+ if (diffDays < 7) return `${diffDays}d ago`;
48349
+ return date.toLocaleDateString("en-US", { month: "short", day: "numeric" });
48350
+ }
48351
+ async function runUsageFlow() {
48352
+ const config = getConfig();
48353
+ if (!isAuthenticated()) {
48354
+ console.log("");
48355
+ M2.warn("Not logged in");
48356
+ console.log("");
48357
+ console.log(source_default.dim(" Login to view your usage and quota:"));
48358
+ console.log(source_default.cyan(" oculum login"));
48359
+ console.log("");
48360
+ await ve({
48361
+ message: "Press Enter to continue",
48362
+ options: [{ value: "back", label: "\u2190 Back to menu" }]
48363
+ });
48364
+ return;
48365
+ }
48366
+ const spinner = Y2();
48367
+ spinner.start("Fetching usage data...");
48368
+ try {
48369
+ const result = await getUsage(config.apiKey);
48370
+ if (!result.success || !result.usage || !result.plan) {
48371
+ spinner.stop("Failed to fetch usage data");
48372
+ console.log("");
48373
+ M2.error(result.error || "Unknown error");
48374
+ console.log("");
48375
+ await ve({
48376
+ message: "Press Enter to continue",
48377
+ options: [{ value: "back", label: "\u2190 Back to menu" }]
48378
+ });
48379
+ return;
48380
+ }
48381
+ spinner.stop("Usage data loaded");
48382
+ const { plan, usage: usageData } = result;
48383
+ console.clear();
48384
+ console.log("");
48385
+ console.log(source_default.bold(" \u{1F4CA} Oculum Usage"));
48386
+ console.log(source_default.dim(" " + "\u2500".repeat(38)));
48387
+ console.log("");
48388
+ 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 ");
48389
+ console.log(source_default.dim(" Plan: ") + planBadge + source_default.white(` ${plan.displayName}`));
48390
+ console.log(source_default.dim(" Month: ") + source_default.white(usageData.month));
48391
+ console.log("");
48392
+ console.log(source_default.bold(" Credits Usage"));
48393
+ console.log("");
48394
+ const creditsDisplay = usageData.creditsLimit === -1 ? `${formatNumber(usageData.creditsUsed)} / unlimited` : `${formatNumber(usageData.creditsUsed)} / ${formatNumber(usageData.creditsLimit)}`;
48395
+ console.log(source_default.dim(" Used: ") + source_default.white(creditsDisplay));
48396
+ if (usageData.creditsLimit !== -1) {
48397
+ console.log(source_default.dim(" Remaining: ") + source_default.white(formatNumber(usageData.creditsRemaining)));
48398
+ console.log("");
48399
+ console.log(source_default.dim(" ") + createProgressBar(usageData.usagePercentage) + source_default.dim(` ${usageData.usagePercentage.toFixed(1)}%`));
48400
+ }
48401
+ console.log("");
48402
+ console.log(source_default.bold(" This Month"));
48403
+ console.log("");
48404
+ console.log(source_default.dim(" Scans: ") + source_default.white(formatNumber(usageData.totalScans)));
48405
+ console.log(source_default.dim(" Files: ") + source_default.white(formatNumber(usageData.totalFiles)));
48406
+ console.log("");
48407
+ console.log(source_default.dim(" Resets on: ") + source_default.white(formatDate(usageData.resetDate)));
48408
+ console.log("");
48409
+ if (result.recentScans && result.recentScans.length > 0) {
48410
+ console.log(source_default.bold(" Recent Scans"));
48411
+ console.log("");
48412
+ const recentToShow = result.recentScans.slice(0, 5);
48413
+ for (const scan of recentToShow) {
48414
+ const date = new Date(scan.createdAt);
48415
+ const timeAgo = getTimeAgo(date);
48416
+ console.log(source_default.dim(" \u2022 ") + source_default.white(scan.repoName || "unknown") + source_default.dim(` (${timeAgo})`));
48417
+ console.log(source_default.dim(" ") + source_default.dim(`${scan.filesScanned} files, ${scan.findingsCount} findings, ${scan.creditsUsed} credits`));
48418
+ }
48419
+ console.log("");
48420
+ }
48421
+ if (plan.name === "free" || plan.name === "starter") {
48422
+ console.log(source_default.dim(" " + "\u2500".repeat(38)));
48423
+ console.log("");
48424
+ console.log(source_default.dim(" Need more credits? Upgrade your plan"));
48425
+ console.log("");
48426
+ }
48427
+ await ve({
48428
+ message: "Press Enter to continue",
48429
+ options: [{ value: "back", label: "\u2190 Back to menu" }]
48430
+ });
48431
+ } catch (error) {
48432
+ spinner.stop("Failed to fetch usage data");
48433
+ console.log("");
48434
+ M2.error(String(error));
48435
+ console.log("");
48436
+ await ve({
48437
+ message: "Press Enter to continue",
48438
+ options: [{ value: "back", label: "\u2190 Back to menu" }]
48439
+ });
48440
+ }
48441
+ }
47975
48442
  async function runUI() {
48443
+ console.clear();
48444
+ showLogo();
48445
+ console.log();
47976
48446
  const onboardingResult = await handleOnboarding();
47977
48447
  if (onboardingResult.quickStartResult) {
47978
48448
  const { path: path2, depth } = onboardingResult.quickStartResult;
47979
- Ie(source_default.bold("Oculum"));
47980
48449
  try {
47981
48450
  const { output, exitCode, result } = await runScanOnce(path2, {
47982
48451
  depth,
@@ -48004,8 +48473,6 @@ async function runUI() {
48004
48473
  } catch (err) {
48005
48474
  M2.error(`Scan failed: ${String(err)}`);
48006
48475
  }
48007
- } else {
48008
- Ie(source_default.bold("Oculum"));
48009
48476
  }
48010
48477
  let lastScanEntry;
48011
48478
  const userState = getUserState();
@@ -48021,6 +48488,7 @@ async function runUI() {
48021
48488
  { value: "scan", label: "\u{1F50D} Custom Scan", hint: "Configure scan options" },
48022
48489
  { value: "history", label: "\u{1F4DC} Scan History", hint: `${listScanHistory().length} past scans` },
48023
48490
  { value: "auth", label: "\u{1F510} Auth", hint: isAuthenticated() ? `Logged in (${getConfig().tier || "free"})` : "Not logged in" },
48491
+ { value: "usage", label: "\u{1F4CA} Usage", hint: isAuthenticated() ? "View credits and quota" : "Requires login" },
48024
48492
  { value: "help", label: "\u2753 Help", hint: "Commands and tips" },
48025
48493
  { value: "exit", label: "\u{1F44B} Exit" }
48026
48494
  ];
@@ -48102,6 +48570,10 @@ async function runUI() {
48102
48570
  await runHistoryFlow();
48103
48571
  continue;
48104
48572
  }
48573
+ if (action === "usage") {
48574
+ await runUsageFlow();
48575
+ continue;
48576
+ }
48105
48577
  if (action === "help") {
48106
48578
  await showHelpScreen();
48107
48579
  continue;
@@ -48154,11 +48626,11 @@ var uiCommand = new Command("ui").description("Interactive terminal UI").action(
48154
48626
  });
48155
48627
 
48156
48628
  // src/commands/usage.ts
48157
- function formatNumber(num) {
48629
+ function formatNumber2(num) {
48158
48630
  if (num === -1) return "unlimited";
48159
48631
  return num.toLocaleString();
48160
48632
  }
48161
- function createProgressBar(percentage, width = 20) {
48633
+ function createProgressBar2(percentage, width = 20) {
48162
48634
  const filled = Math.round(percentage / 100 * width);
48163
48635
  const empty = width - filled;
48164
48636
  const bar = "\u2588".repeat(filled) + "\u2591".repeat(empty);
@@ -48166,7 +48638,7 @@ function createProgressBar(percentage, width = 20) {
48166
48638
  if (percentage >= 70) return source_default.yellow(bar);
48167
48639
  return source_default.green(bar);
48168
48640
  }
48169
- function formatDate(dateStr) {
48641
+ function formatDate2(dateStr) {
48170
48642
  const date = new Date(dateStr);
48171
48643
  return date.toLocaleDateString("en-US", {
48172
48644
  month: "short",
@@ -48211,20 +48683,20 @@ async function usage(options) {
48211
48683
  console.log("");
48212
48684
  console.log(source_default.bold(" Credits Usage"));
48213
48685
  console.log("");
48214
- const creditsDisplay = usageData.creditsLimit === -1 ? `${formatNumber(usageData.creditsUsed)} / unlimited` : `${formatNumber(usageData.creditsUsed)} / ${formatNumber(usageData.creditsLimit)}`;
48686
+ const creditsDisplay = usageData.creditsLimit === -1 ? `${formatNumber2(usageData.creditsUsed)} / unlimited` : `${formatNumber2(usageData.creditsUsed)} / ${formatNumber2(usageData.creditsLimit)}`;
48215
48687
  console.log(source_default.dim(" Used: ") + source_default.white(creditsDisplay));
48216
48688
  if (usageData.creditsLimit !== -1) {
48217
- console.log(source_default.dim(" Remaining: ") + source_default.white(formatNumber(usageData.creditsRemaining)));
48689
+ console.log(source_default.dim(" Remaining: ") + source_default.white(formatNumber2(usageData.creditsRemaining)));
48218
48690
  console.log("");
48219
- console.log(source_default.dim(" ") + createProgressBar(usageData.usagePercentage) + source_default.dim(` ${usageData.usagePercentage.toFixed(1)}%`));
48691
+ console.log(source_default.dim(" ") + createProgressBar2(usageData.usagePercentage) + source_default.dim(` ${usageData.usagePercentage.toFixed(1)}%`));
48220
48692
  }
48221
48693
  console.log("");
48222
48694
  console.log(source_default.bold(" This Month"));
48223
48695
  console.log("");
48224
- console.log(source_default.dim(" Scans: ") + source_default.white(formatNumber(usageData.totalScans)));
48225
- console.log(source_default.dim(" Files: ") + source_default.white(formatNumber(usageData.totalFiles)));
48696
+ console.log(source_default.dim(" Scans: ") + source_default.white(formatNumber2(usageData.totalScans)));
48697
+ console.log(source_default.dim(" Files: ") + source_default.white(formatNumber2(usageData.totalFiles)));
48226
48698
  console.log("");
48227
- console.log(source_default.dim(" Resets on: ") + source_default.white(formatDate(usageData.resetDate)));
48699
+ console.log(source_default.dim(" Resets on: ") + source_default.white(formatDate2(usageData.resetDate)));
48228
48700
  console.log("");
48229
48701
  if (result.recentScans && result.recentScans.length > 0) {
48230
48702
  console.log(source_default.bold(" Recent Scans"));
@@ -48232,7 +48704,7 @@ async function usage(options) {
48232
48704
  const recentToShow = result.recentScans.slice(0, 5);
48233
48705
  for (const scan of recentToShow) {
48234
48706
  const date = new Date(scan.createdAt);
48235
- const timeAgo = getTimeAgo(date);
48707
+ const timeAgo = getTimeAgo2(date);
48236
48708
  console.log(source_default.dim(" \u2022 ") + source_default.white(scan.repoName || "unknown") + source_default.dim(` (${timeAgo})`));
48237
48709
  console.log(source_default.dim(" ") + source_default.dim(`${scan.filesScanned} files, ${scan.findingsCount} findings, ${scan.creditsUsed} credits`));
48238
48710
  }
@@ -48252,7 +48724,7 @@ async function usage(options) {
48252
48724
  process.exit(1);
48253
48725
  }
48254
48726
  }
48255
- function getTimeAgo(date) {
48727
+ function getTimeAgo2(date) {
48256
48728
  const now = /* @__PURE__ */ new Date();
48257
48729
  const diffMs = now.getTime() - date.getTime();
48258
48730
  const diffMins = Math.floor(diffMs / 6e4);
@@ -48266,9 +48738,251 @@ function getTimeAgo(date) {
48266
48738
  }
48267
48739
  var usageCommand = new Command("usage").description("Show current usage and quota").option("--json", "Output as JSON").action(usage);
48268
48740
 
48741
+ // src/commands/help.ts
48742
+ var TOPICS = {
48743
+ "scan-modes": showScanModes,
48744
+ "ci-setup": showCISetup,
48745
+ "config": showConfig,
48746
+ "troubleshooting": showTroubleshooting
48747
+ };
48748
+ var helpCommand = new Command("help").description("Get detailed help on specific topics").argument("[topic]", "Help topic (scan-modes, ci-setup, config, troubleshooting)").action(async (topic) => {
48749
+ if (!topic) {
48750
+ showTopicList();
48751
+ return;
48752
+ }
48753
+ const normalizedTopic = topic.toLowerCase().replace(/_/g, "-");
48754
+ if (normalizedTopic in TOPICS) {
48755
+ TOPICS[normalizedTopic]();
48756
+ } else {
48757
+ console.log(source_default.red(`Unknown topic: ${topic}
48758
+ `));
48759
+ showTopicList();
48760
+ }
48761
+ });
48762
+ function showTopicList() {
48763
+ console.log(source_default.bold("\nOculum Help Topics\n"));
48764
+ console.log(source_default.dim("\u2500".repeat(50)));
48765
+ console.log();
48766
+ console.log(source_default.cyan(" scan-modes ") + source_default.white("Compare cheap, validated, and deep scans"));
48767
+ console.log(source_default.cyan(" ci-setup ") + source_default.white("GitHub Actions and GitLab CI examples"));
48768
+ console.log(source_default.cyan(" config ") + source_default.white("Configuration file documentation"));
48769
+ console.log(source_default.cyan(" troubleshooting ") + source_default.white("Common issues and solutions"));
48770
+ console.log();
48771
+ console.log(source_default.dim("\u2500".repeat(50)));
48772
+ console.log(source_default.dim("\nUsage: oculum help <topic>"));
48773
+ console.log(source_default.dim("Example: oculum help scan-modes\n"));
48774
+ }
48775
+ function showScanModes() {
48776
+ console.log(source_default.bold("\nScan Modes Comparison\n"));
48777
+ console.log(source_default.dim("\u2500".repeat(60) + "\n"));
48778
+ console.log(source_default.green.bold(" CHEAP (Quick Scan)"));
48779
+ console.log(source_default.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
48780
+ console.log(source_default.white(" Cost: ") + source_default.green("Free"));
48781
+ console.log(source_default.white(" Speed: ") + source_default.white("~1000 files/second"));
48782
+ console.log(source_default.white(" How: ") + source_default.dim("Pattern matching + entropy analysis"));
48783
+ console.log(source_default.white(" Best for: ") + source_default.dim("Quick checks, CI/CD pipelines"));
48784
+ console.log(source_default.white(" Limitation: ") + source_default.dim("May have more false positives\n"));
48785
+ console.log(source_default.blue.bold(" VALIDATED"));
48786
+ console.log(source_default.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
48787
+ console.log(source_default.white(" Cost: ") + source_default.yellow("~$0.03 per 300 files"));
48788
+ console.log(source_default.white(" Speed: ") + source_default.white("~30 seconds for 300 files"));
48789
+ console.log(source_default.white(" How: ") + source_default.dim("Pattern matching + AI validation"));
48790
+ console.log(source_default.white(" Best for: ") + source_default.dim("Pre-commit checks, PR reviews"));
48791
+ console.log(source_default.white(" Benefit: ") + source_default.dim("~70% fewer false positives\n"));
48792
+ console.log(source_default.magenta.bold(" DEEP"));
48793
+ console.log(source_default.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
48794
+ console.log(source_default.white(" Cost: ") + source_default.yellow("~$0.10 per 300 files"));
48795
+ console.log(source_default.white(" Speed: ") + source_default.white("~60 seconds for 300 files"));
48796
+ console.log(source_default.white(" How: ") + source_default.dim("Full semantic analysis with AI reasoning"));
48797
+ console.log(source_default.white(" Best for: ") + source_default.dim("Security audits, release checks"));
48798
+ console.log(source_default.white(" Benefit: ") + source_default.dim("Deepest analysis, remediation advice\n"));
48799
+ console.log(source_default.dim("\u2500".repeat(60)));
48800
+ console.log(source_default.bold("\nUsage Examples:\n"));
48801
+ console.log(source_default.dim(" $ oculum scan . ") + source_default.white("# Quick scan (free)"));
48802
+ console.log(source_default.dim(" $ oculum scan . -d validated ") + source_default.white("# AI-validated scan"));
48803
+ console.log(source_default.dim(" $ oculum scan . -d deep ") + source_default.white("# Deep semantic analysis"));
48804
+ console.log();
48805
+ }
48806
+ function showCISetup() {
48807
+ console.log(source_default.bold("\nCI/CD Integration\n"));
48808
+ console.log(source_default.dim("\u2500".repeat(60) + "\n"));
48809
+ console.log(source_default.bold(" GitHub Actions\n"));
48810
+ console.log(source_default.dim(" Create .github/workflows/oculum.yml:\n"));
48811
+ console.log(source_default.cyan(` name: Security Scan
48812
+ on: [push, pull_request]
48813
+ jobs:
48814
+ scan:
48815
+ runs-on: ubuntu-latest
48816
+ steps:
48817
+ - uses: actions/checkout@v4
48818
+ - uses: actions/setup-node@v4
48819
+ with:
48820
+ node-version: '20'
48821
+ - run: npm install -g oculum
48822
+ - run: oculum scan . --fail-on high
48823
+ env:
48824
+ OCULUM_API_KEY: \${{ secrets.OCULUM_API_KEY }}`));
48825
+ console.log(source_default.dim("\n \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n"));
48826
+ console.log(source_default.bold(" GitLab CI\n"));
48827
+ console.log(source_default.dim(" Add to .gitlab-ci.yml:\n"));
48828
+ console.log(source_default.cyan(` security-scan:
48829
+ image: node:20
48830
+ script:
48831
+ - npm install -g oculum
48832
+ - oculum scan . --fail-on high
48833
+ variables:
48834
+ OCULUM_API_KEY: $OCULUM_API_KEY`));
48835
+ console.log(source_default.dim("\n\u2500".repeat(60)));
48836
+ console.log(source_default.bold("\nTips:\n"));
48837
+ console.log(source_default.white(" - Use ") + source_default.cyan("--fail-on high") + source_default.white(" to fail builds on high/critical findings"));
48838
+ console.log(source_default.white(" - Use ") + source_default.cyan("--format sarif") + source_default.white(" for GitHub Code Scanning integration"));
48839
+ console.log(source_default.white(" - Use ") + source_default.cyan("-d cheap") + source_default.white(" for fastest scans (free, no API key needed)"));
48840
+ console.log(source_default.white(" - Store API key in secrets, not in code\n"));
48841
+ }
48842
+ function showConfig() {
48843
+ console.log(source_default.bold("\nConfiguration Files\n"));
48844
+ console.log(source_default.dim("\u2500".repeat(60) + "\n"));
48845
+ console.log(source_default.bold(" Project Config (oculum.config.json)\n"));
48846
+ console.log(source_default.dim(" Create in your project root:\n"));
48847
+ console.log(source_default.cyan(` {
48848
+ "depth": "validated",
48849
+ "failOn": "high",
48850
+ "format": "terminal",
48851
+ "ignore": [
48852
+ "**/node_modules/**",
48853
+ "**/dist/**",
48854
+ "**/*.test.ts"
48855
+ ],
48856
+ "include": [
48857
+ "src/**"
48858
+ ],
48859
+ "watch": {
48860
+ "debounce": 500,
48861
+ "clear": true
48862
+ }
48863
+ }`));
48864
+ console.log(source_default.dim("\n \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n"));
48865
+ console.log(source_default.bold(" Config Options\n"));
48866
+ console.log(source_default.white(" depth ") + source_default.dim("Scan depth: cheap | validated | deep"));
48867
+ console.log(source_default.white(" failOn ") + source_default.dim("Exit code 1 on: critical | high | medium | low | none"));
48868
+ console.log(source_default.white(" format ") + source_default.dim("Output format: terminal | json | sarif | markdown"));
48869
+ console.log(source_default.white(" output ") + source_default.dim("Output file path"));
48870
+ console.log(source_default.white(" quiet ") + source_default.dim("Only show findings (boolean)"));
48871
+ console.log(source_default.white(" ignore ") + source_default.dim("Glob patterns to exclude"));
48872
+ console.log(source_default.white(" include ") + source_default.dim("Glob patterns to include"));
48873
+ console.log(source_default.dim("\n \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n"));
48874
+ console.log(source_default.bold(" Config File Priority\n"));
48875
+ console.log(source_default.dim(" CLI flags > Project config > User config > Defaults\n"));
48876
+ console.log(source_default.white(" Supported filenames:"));
48877
+ console.log(source_default.dim(" - oculum.config.json"));
48878
+ console.log(source_default.dim(" - .oculumrc.json"));
48879
+ console.log(source_default.dim(" - .oculumrc\n"));
48880
+ }
48881
+ function showTroubleshooting() {
48882
+ console.log(source_default.bold("\nTroubleshooting\n"));
48883
+ console.log(source_default.dim("\u2500".repeat(60) + "\n"));
48884
+ console.log(source_default.red.bold(" Authentication Errors\n"));
48885
+ console.log(source_default.white(' "Authentication required"'));
48886
+ console.log(source_default.dim(" \u2192 Run `oculum login` to authenticate"));
48887
+ console.log(source_default.dim(" \u2192 Or use `--depth cheap` for free local scans\n"));
48888
+ console.log(source_default.white(' "API key invalid or expired"'));
48889
+ console.log(source_default.dim(" \u2192 Run `oculum login` to re-authenticate"));
48890
+ console.log(source_default.dim(" \u2192 Check key at https://oculum.dev/dashboard/api-keys\n"));
48891
+ console.log(source_default.white(' "Insufficient tier"'));
48892
+ console.log(source_default.dim(" \u2192 Validated/deep scans require Pro subscription"));
48893
+ console.log(source_default.dim(" \u2192 Visit https://oculum.dev/billing to upgrade\n"));
48894
+ console.log(source_default.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n"));
48895
+ console.log(source_default.yellow.bold(" Network Errors\n"));
48896
+ console.log(source_default.white(' "Connection refused" / "Network error"'));
48897
+ console.log(source_default.dim(" \u2192 Check your internet connection"));
48898
+ console.log(source_default.dim(" \u2192 Use `--depth cheap` for offline scans"));
48899
+ console.log(source_default.dim(" \u2192 Check https://status.oculum.dev\n"));
48900
+ console.log(source_default.white(' "SSL certificate error"'));
48901
+ console.log(source_default.dim(" \u2192 Corporate proxy may be intercepting HTTPS"));
48902
+ console.log(source_default.dim(" \u2192 Use `--depth cheap` for offline scans"));
48903
+ console.log(source_default.dim(" \u2192 Contact IT for proxy configuration\n"));
48904
+ console.log(source_default.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n"));
48905
+ console.log(source_default.blue.bold(" Scan Issues\n"));
48906
+ console.log(source_default.white(' "No scannable files found"'));
48907
+ console.log(source_default.dim(" \u2192 Check path exists: ls <path>"));
48908
+ console.log(source_default.dim(" \u2192 Ensure files have supported extensions"));
48909
+ console.log(source_default.dim(" \u2192 Check .gitignore isn't excluding files\n"));
48910
+ console.log(source_default.white(' "Request timed out"'));
48911
+ console.log(source_default.dim(" \u2192 Try scanning fewer files"));
48912
+ console.log(source_default.dim(" \u2192 Use `--depth cheap` for large codebases"));
48913
+ console.log(source_default.dim(" \u2192 Split into multiple scans\n"));
48914
+ console.log(source_default.white(' "Out of memory"'));
48915
+ console.log(source_default.dim(" \u2192 Scan a smaller directory"));
48916
+ console.log(source_default.dim(' \u2192 Increase Node memory: NODE_OPTIONS="--max-old-space-size=4096"\n'));
48917
+ console.log(source_default.dim("\u2500".repeat(60)));
48918
+ console.log(source_default.bold("\nStill stuck?\n"));
48919
+ console.log(source_default.white(" - Check docs: ") + source_default.cyan("https://oculum.dev/docs"));
48920
+ console.log(source_default.white(" - Report issues: ") + source_default.cyan("https://github.com/oculum-dev/oculum/issues"));
48921
+ console.log(source_default.white(" - Get support: ") + source_default.cyan("support@oculum.dev\n"));
48922
+ }
48923
+
48924
+ // src/utils/ci-detect.ts
48925
+ var CI_ENV_VARS = [
48926
+ "CI",
48927
+ // Generic CI flag (GitHub Actions, GitLab CI, etc.)
48928
+ "GITHUB_ACTIONS",
48929
+ // GitHub Actions
48930
+ "GITLAB_CI",
48931
+ // GitLab CI
48932
+ "JENKINS_URL",
48933
+ // Jenkins
48934
+ "CIRCLECI",
48935
+ // CircleCI
48936
+ "TRAVIS",
48937
+ // Travis CI
48938
+ "BUILDKITE",
48939
+ // Buildkite
48940
+ "DRONE",
48941
+ // Drone CI
48942
+ "TEAMCITY_VERSION",
48943
+ // TeamCity
48944
+ "BITBUCKET_COMMIT",
48945
+ // Bitbucket Pipelines
48946
+ "AZURE_HTTP_USER_AGENT",
48947
+ // Azure Pipelines
48948
+ "TF_BUILD",
48949
+ // Azure Pipelines (alternative)
48950
+ "CODEBUILD_BUILD_ID",
48951
+ // AWS CodeBuild
48952
+ "APPVEYOR",
48953
+ // AppVeyor
48954
+ "SEMAPHORE",
48955
+ // Semaphore CI
48956
+ "HEROKU_TEST_RUN_ID",
48957
+ // Heroku CI
48958
+ "RENDER",
48959
+ // Render
48960
+ "VERCEL",
48961
+ // Vercel
48962
+ "NETLIFY",
48963
+ // Netlify
48964
+ "CF_PAGES",
48965
+ // Cloudflare Pages
48966
+ "RAILWAY_ENVIRONMENT"
48967
+ // Railway
48968
+ ];
48969
+ function isCI() {
48970
+ return CI_ENV_VARS.some((envVar) => !!process.env[envVar]);
48971
+ }
48972
+ function isInteractiveTerminal() {
48973
+ return !!(process.stdin.isTTY && !isCI());
48974
+ }
48975
+
48269
48976
  // src/index.ts
48977
+ function shouldRunUI() {
48978
+ const programName = process.argv[1] || "";
48979
+ const args = process.argv.slice(2);
48980
+ const isOcAlias = programName.endsWith("oc") && !programName.endsWith("oculum");
48981
+ const isUICommand = args.length === 0 || args.length === 1 && args[0] === "ui";
48982
+ return isOcAlias || isUICommand;
48983
+ }
48270
48984
  var program2 = new Command();
48271
- program2.name("oculum").description("AI-native security scanner for detecting vulnerabilities in LLM-generated code").version("1.0.7").addHelpText("after", `
48985
+ program2.name("oculum").description("AI-native security scanner for detecting vulnerabilities in LLM-generated code").version("1.0.8").addHelpText("after", `
48272
48986
  Quick Start:
48273
48987
  $ oculum scan . Scan current directory (free)
48274
48988
  $ oculum ui Interactive mode with guided setup
@@ -48281,11 +48995,13 @@ Common Commands:
48281
48995
  login Authenticate with Oculum
48282
48996
  status Check authentication status
48283
48997
  usage View credits and quota
48998
+ help [topic] Get help on specific topics
48284
48999
 
48285
49000
  Learn More:
48286
49001
  $ oculum scan --help Detailed scan options
49002
+ $ oculum help scan-modes Compare scan depth options
48287
49003
  $ oculum --help All available commands
48288
-
49004
+
48289
49005
  Documentation: https://oculum.dev/docs
48290
49006
  `);
48291
49007
  program2.addCommand(scanCommand, { isDefault: true });
@@ -48296,7 +49012,17 @@ program2.addCommand(upgradeCommand);
48296
49012
  program2.addCommand(usageCommand);
48297
49013
  program2.addCommand(watchCommand);
48298
49014
  program2.addCommand(uiCommand);
48299
- program2.parse();
49015
+ program2.addCommand(helpCommand);
49016
+ async function main2() {
49017
+ const interactive = isInteractiveTerminal();
49018
+ if (interactive && shouldRunUI()) {
49019
+ process.argv = ["node", "oculum", "ui"];
49020
+ program2.parse();
49021
+ return;
49022
+ }
49023
+ program2.parse();
49024
+ }
49025
+ main2();
48300
49026
  /*! Bundled license information:
48301
49027
 
48302
49028
  chokidar/esm/index.js: