@oculum/cli 1.0.8 → 1.0.10

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 +743 -120
  2. package/package.json +4 -3
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 [];
@@ -43083,6 +43313,16 @@ function getApiBaseUrl() {
43083
43313
  function setAuthCredentials(apiKey, email, tier) {
43084
43314
  updateConfig({ apiKey, email, tier });
43085
43315
  }
43316
+ function syncAuthFromVerification(verifyResponse) {
43317
+ if (!verifyResponse.valid) return;
43318
+ const config = getConfig();
43319
+ if (!config.apiKey) return;
43320
+ const email = verifyResponse.email || config.email;
43321
+ const tier = verifyResponse.tier || config.tier || "free";
43322
+ if (tier !== config.tier || email !== config.email) {
43323
+ setAuthCredentials(config.apiKey, email, tier);
43324
+ }
43325
+ }
43086
43326
 
43087
43327
  // src/utils/api.ts
43088
43328
  var APIError = class extends Error {
@@ -43239,8 +43479,11 @@ function enhanceAPIError(error) {
43239
43479
  case 401:
43240
43480
  return {
43241
43481
  message: "Authentication required",
43242
- suggestion: "Run `oculum login` to authenticate, or use `--depth cheap` for free scans.",
43482
+ suggestion: "You need to login to use AI-powered scans.",
43243
43483
  category: "auth",
43484
+ errorCode: "OCU-E401",
43485
+ quickFix: "oculum login",
43486
+ learnMoreUrl: "https://oculum.dev/docs/authentication",
43244
43487
  recoveryActions: [
43245
43488
  { label: "Login", command: "oculum login", action: "login" },
43246
43489
  { label: "Use free scan", action: "fallback" }
@@ -43250,8 +43493,11 @@ function enhanceAPIError(error) {
43250
43493
  if (error.reason === "insufficient_tier") {
43251
43494
  return {
43252
43495
  message: "This feature requires a Pro subscription",
43253
- suggestion: "Visit https://oculum.dev/billing to upgrade your plan.",
43496
+ suggestion: "Validated and deep scans require a Pro plan.",
43254
43497
  category: "auth",
43498
+ errorCode: "OCU-E403-TIER",
43499
+ quickFix: "oculum scan . --depth cheap",
43500
+ learnMoreUrl: "https://oculum.dev/billing",
43255
43501
  recoveryActions: [
43256
43502
  { label: "View pricing", action: "upgrade" },
43257
43503
  { label: "Use free scan", action: "fallback" }
@@ -43261,8 +43507,11 @@ function enhanceAPIError(error) {
43261
43507
  if (error.reason === "expired") {
43262
43508
  return {
43263
43509
  message: "Your API key has expired",
43264
- suggestion: "Generate a new key at https://oculum.dev/dashboard/api-keys",
43510
+ suggestion: "Generate a new key to continue using AI-powered scans.",
43265
43511
  category: "auth",
43512
+ errorCode: "OCU-E403-EXP",
43513
+ quickFix: "oculum login",
43514
+ learnMoreUrl: "https://oculum.dev/dashboard/api-keys",
43266
43515
  recoveryActions: [
43267
43516
  { label: "Login again", command: "oculum login", action: "login" }
43268
43517
  ]
@@ -43271,8 +43520,11 @@ function enhanceAPIError(error) {
43271
43520
  if (error.reason === "invalid_key") {
43272
43521
  return {
43273
43522
  message: "Invalid API key",
43274
- suggestion: "Your API key is not recognized. Please login again or check your key.",
43523
+ suggestion: "Your API key is not recognized.",
43275
43524
  category: "auth",
43525
+ errorCode: "OCU-E403-KEY",
43526
+ quickFix: "oculum login",
43527
+ learnMoreUrl: "https://oculum.dev/dashboard/api-keys",
43276
43528
  recoveryActions: [
43277
43529
  { label: "Login", command: "oculum login", action: "login" },
43278
43530
  { label: "Use free scan", action: "fallback" }
@@ -43281,19 +43533,24 @@ function enhanceAPIError(error) {
43281
43533
  }
43282
43534
  return {
43283
43535
  message: "Access denied",
43284
- suggestion: "Check your API key permissions or run `oculum login` to re-authenticate.",
43536
+ suggestion: "Check your API key permissions.",
43285
43537
  category: "auth",
43538
+ errorCode: "OCU-E403",
43539
+ quickFix: "oculum login",
43286
43540
  recoveryActions: [
43287
43541
  { label: "Login", command: "oculum login", action: "login" }
43288
43542
  ]
43289
43543
  };
43290
43544
  case 429:
43291
43545
  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.";
43546
+ const rateLimitSuggestion = error.reason === "quota_exceeded" ? "Your quota resets at the start of next month." : "Wait a moment before trying again.";
43293
43547
  return {
43294
43548
  message: rateLimitInfo,
43295
43549
  suggestion: rateLimitSuggestion,
43296
43550
  category: "server",
43551
+ errorCode: error.reason === "quota_exceeded" ? "OCU-E429-QUOTA" : "OCU-E429-RATE",
43552
+ quickFix: "oculum scan . --depth cheap",
43553
+ learnMoreUrl: "https://oculum.dev/dashboard/usage",
43297
43554
  recoveryActions: [
43298
43555
  { label: "Use free local scan", command: "oculum scan . --depth cheap", action: "fallback" },
43299
43556
  { label: "View usage & upgrade", action: "upgrade" },
@@ -43306,8 +43563,11 @@ function enhanceAPIError(error) {
43306
43563
  case 503:
43307
43564
  return {
43308
43565
  message: "Oculum servers are experiencing issues",
43309
- suggestion: "Try again in a few minutes. Check https://status.oculum.dev for updates.",
43566
+ suggestion: "This is temporary. Try again in a few minutes.",
43310
43567
  category: "server",
43568
+ errorCode: `OCU-E${error.statusCode}`,
43569
+ quickFix: "oculum scan . --depth cheap",
43570
+ learnMoreUrl: "https://status.oculum.dev",
43311
43571
  recoveryActions: [
43312
43572
  { label: "Retry", action: "retry" },
43313
43573
  { label: "Use free scan (offline)", action: "fallback" }
@@ -43316,15 +43576,22 @@ function enhanceAPIError(error) {
43316
43576
  case 504:
43317
43577
  return {
43318
43578
  message: "Request timed out",
43319
- suggestion: "The scan may be too large. Try scanning fewer files or use `--depth cheap`.",
43579
+ suggestion: "The scan may be too large for the server.",
43320
43580
  category: "server",
43581
+ errorCode: "OCU-E504",
43582
+ quickFix: "oculum scan . --depth cheap",
43583
+ learnMoreUrl: "https://oculum.dev/docs/troubleshooting#timeout",
43321
43584
  recoveryActions: [
43322
43585
  { label: "Retry", action: "retry" },
43323
43586
  { label: "Use quick scan", action: "fallback" }
43324
43587
  ]
43325
43588
  };
43326
43589
  default:
43327
- return { message: error.message, category: "unknown" };
43590
+ return {
43591
+ message: error.message,
43592
+ category: "unknown",
43593
+ errorCode: "OCU-E000"
43594
+ };
43328
43595
  }
43329
43596
  }
43330
43597
  function detectNetworkErrorType(msg) {
@@ -43372,6 +43639,7 @@ function enhanceStandardError(error) {
43372
43639
  message: `Path not found: ${path2}`,
43373
43640
  suggestion: "Check that the path exists and is spelled correctly.",
43374
43641
  category: "file",
43642
+ errorCode: "OCU-E001",
43375
43643
  details: "The file or directory does not exist at the specified location."
43376
43644
  };
43377
43645
  }
@@ -43380,15 +43648,33 @@ function enhanceStandardError(error) {
43380
43648
  message: "Permission denied",
43381
43649
  suggestion: "Check file permissions or try running with appropriate access.",
43382
43650
  category: "file",
43651
+ errorCode: "OCU-E002",
43383
43652
  details: "You do not have permission to access this file or directory."
43384
43653
  };
43385
43654
  }
43655
+ if (msg.includes("self-signed") || msg.includes("unable to verify") || msg.includes("certificate") && (msg.includes("invalid") || msg.includes("expired"))) {
43656
+ return {
43657
+ message: "SSL certificate error",
43658
+ suggestion: "Your network may be intercepting HTTPS traffic (corporate proxy).",
43659
+ category: "network",
43660
+ errorCode: "OCU-E003",
43661
+ quickFix: "oculum scan . --depth cheap",
43662
+ learnMoreUrl: "https://oculum.dev/docs/troubleshooting#ssl",
43663
+ recoveryActions: [
43664
+ { label: "Use offline scan", command: "oculum scan . --depth cheap", action: "fallback" },
43665
+ { label: "View help", action: "help" }
43666
+ ]
43667
+ };
43668
+ }
43386
43669
  if (msg.includes("fetch failed") || msg.includes("network") || msg.includes("econnrefused") || msg.includes("enotfound") || msg.includes("etimedout") || msg.includes("econnreset") || msg.includes("socket")) {
43387
43670
  const { type, suggestion } = detectNetworkErrorType(msg);
43388
43671
  return {
43389
43672
  message: type,
43390
43673
  suggestion,
43391
43674
  category: "network",
43675
+ errorCode: "OCU-E004",
43676
+ quickFix: "oculum scan . --depth cheap",
43677
+ learnMoreUrl: "https://oculum.dev/docs/troubleshooting#network",
43392
43678
  recoveryActions: [
43393
43679
  { label: "Retry", action: "retry" },
43394
43680
  { label: "Use offline scan", action: "fallback" }
@@ -43400,52 +43686,105 @@ function enhanceStandardError(error) {
43400
43686
  message: "Invalid configuration file",
43401
43687
  suggestion: "Check your config file for valid JSON syntax.",
43402
43688
  category: "file",
43689
+ errorCode: "OCU-E005",
43690
+ learnMoreUrl: "https://oculum.dev/docs/configuration",
43403
43691
  details: "The configuration file contains invalid JSON. Use a JSON validator to find the error."
43404
43692
  };
43405
43693
  }
43406
43694
  if (msg.includes("no scannable files")) {
43407
43695
  return {
43408
43696
  message: "No scannable files found",
43409
- suggestion: "Check that the path contains supported file types (.js, .ts, .py, .go, etc.)",
43697
+ suggestion: "Check that the path contains supported file types.",
43410
43698
  category: "scan",
43411
- details: "Supported extensions: .js, .jsx, .ts, .tsx, .py, .go, .java, .rb, .php, .yaml, .json"
43699
+ errorCode: "OCU-E006",
43700
+ details: "Supported: .js, .jsx, .ts, .tsx, .py, .go, .java, .rb, .php, .yaml, .json"
43412
43701
  };
43413
43702
  }
43414
43703
  if (msg.includes("symlink") || msg.includes("eloop")) {
43415
43704
  return {
43416
43705
  message: "Symbolic link error",
43417
43706
  suggestion: "There may be a circular symlink. Try scanning a specific directory instead.",
43418
- category: "file"
43707
+ category: "file",
43708
+ errorCode: "OCU-E007"
43419
43709
  };
43420
43710
  }
43421
43711
  if (msg.includes("heap") || msg.includes("memory") || msg.includes("enomem")) {
43422
43712
  return {
43423
43713
  message: "Out of memory",
43424
- suggestion: "The scan ran out of memory. Try scanning fewer files or a smaller directory.",
43425
- category: "scan"
43714
+ suggestion: "The scan ran out of memory. Try scanning fewer files.",
43715
+ category: "scan",
43716
+ errorCode: "OCU-E008",
43717
+ quickFix: 'NODE_OPTIONS="--max-old-space-size=4096" oculum scan .',
43718
+ learnMoreUrl: "https://oculum.dev/docs/troubleshooting#memory"
43719
+ };
43720
+ }
43721
+ if (msg.includes("enospc") || msg.includes("no space left")) {
43722
+ return {
43723
+ message: "Disk full",
43724
+ suggestion: "Free up disk space and try again.",
43725
+ category: "file",
43726
+ errorCode: "OCU-E009"
43426
43727
  };
43427
43728
  }
43428
- return { message: error.message, category: "unknown" };
43729
+ return {
43730
+ message: error.message,
43731
+ category: "unknown",
43732
+ errorCode: "OCU-E000",
43733
+ learnMoreUrl: "https://oculum.dev/docs/troubleshooting"
43734
+ };
43429
43735
  }
43430
43736
  function formatError(enhanced) {
43431
- let output = source_default.red(`Error: ${enhanced.message}`);
43737
+ const icon = getErrorIcon(enhanced.category);
43738
+ const lines = [];
43739
+ if (enhanced.errorCode) {
43740
+ lines.push(source_default.red.bold(`${icon} ${enhanced.errorCode} ${enhanced.message}`));
43741
+ } else {
43742
+ lines.push(source_default.red.bold(`${icon} ${enhanced.message}`));
43743
+ }
43744
+ lines.push("");
43432
43745
  if (enhanced.suggestion) {
43433
- output += "\n" + source_default.dim(`Tip: ${enhanced.suggestion}`);
43746
+ lines.push(source_default.white(enhanced.suggestion));
43434
43747
  }
43435
43748
  if (enhanced.details) {
43436
- output += "\n" + source_default.gray(`Details: ${enhanced.details}`);
43749
+ lines.push(source_default.dim(enhanced.details));
43750
+ }
43751
+ if (enhanced.quickFix) {
43752
+ lines.push("");
43753
+ lines.push(source_default.bold("Quick fix:"));
43754
+ lines.push(source_default.cyan(` $ ${enhanced.quickFix}`));
43437
43755
  }
43438
43756
  if (enhanced.recoveryActions && enhanced.recoveryActions.length > 0) {
43439
- output += "\n\n" + source_default.dim("Possible actions:");
43757
+ lines.push("");
43758
+ lines.push(source_default.dim("Or try:"));
43440
43759
  for (const action of enhanced.recoveryActions) {
43441
43760
  if (action.command) {
43442
- output += "\n " + source_default.cyan(`\u2022 ${action.label}: `) + source_default.white(action.command);
43761
+ lines.push(source_default.dim(" - ") + source_default.white(action.label + ": ") + source_default.cyan(action.command));
43443
43762
  } else {
43444
- output += "\n " + source_default.cyan(`\u2022 ${action.label}`);
43763
+ lines.push(source_default.dim(" - ") + source_default.white(action.label));
43445
43764
  }
43446
43765
  }
43447
43766
  }
43448
- return output;
43767
+ if (enhanced.learnMoreUrl) {
43768
+ lines.push("");
43769
+ lines.push(source_default.dim("Need help? ") + source_default.cyan.underline(enhanced.learnMoreUrl));
43770
+ }
43771
+ return lines.join("\n");
43772
+ }
43773
+ function getErrorIcon(category) {
43774
+ switch (category) {
43775
+ case "network":
43776
+ return "\u{1F310}";
43777
+ case "auth":
43778
+ return "\u{1F510}";
43779
+ case "file":
43780
+ return "\u{1F4C1}";
43781
+ case "scan":
43782
+ return "\u{1F50D}";
43783
+ case "server":
43784
+ return "\u{1F5A5}\uFE0F";
43785
+ default:
43786
+ return "\u274C";
43787
+ }
43449
43788
  }
43450
43789
 
43451
43790
  // src/utils/project-config.ts
@@ -43863,6 +44202,13 @@ async function runScanOnce(targetPath, options) {
43863
44202
  const config = getConfig();
43864
44203
  const noColor = options.color === false;
43865
44204
  const cancellationToken = (0, import_scanner.createCancellationToken)();
44205
+ if ((options.depth === "validated" || options.depth === "deep") && isAuthenticated()) {
44206
+ try {
44207
+ const verified = await verifyApiKey(config.apiKey);
44208
+ syncAuthFromVerification(verified);
44209
+ } catch {
44210
+ }
44211
+ }
43866
44212
  if ((options.depth === "validated" || options.depth === "deep") && !isAuthenticated()) {
43867
44213
  if (!options.quiet) {
43868
44214
  console.log("");
@@ -43946,7 +44292,7 @@ async function runScanOnce(targetPath, options) {
43946
44292
  }
43947
44293
  try {
43948
44294
  spinner.start("Starting scan...");
43949
- const hasLocalAI = !!process.env.ANTHROPIC_API_KEY;
44295
+ const hasLocalAI = !!(process.env.ANTHROPIC_API_KEY || process.env.OPENAI_API_KEY);
43950
44296
  if (options.depth !== "cheap" && isAuthenticated() && !hasLocalAI) {
43951
44297
  spinner.text = `Backend ${options.depth} scan analyzing ${files.length} files...`;
43952
44298
  result = await callBackendAPI(
@@ -44066,26 +44412,37 @@ async function runScan(targetPath, cliOptions) {
44066
44412
  }
44067
44413
  }
44068
44414
  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", `
44415
+ Scan Modes:
44416
+ cheap Free Fast pattern matching, runs locally
44417
+ Best for: Quick checks, CI/CD pipelines
44418
+
44419
+ validated ~$0.03 AI validates findings, ~70% fewer false positives
44420
+ Best for: Pre-commit checks, PR reviews
44421
+
44422
+ deep ~$0.10 Full semantic analysis with AI reasoning
44423
+ Best for: Security audits, release checks
44424
+
44069
44425
  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" }
44426
+ $ oculum scan . Scan current directory (free)
44427
+ $ oculum scan . -d validated AI-validated scan
44428
+ $ oculum scan ./src --fail-on high Fail CI on high severity findings
44429
+ $ oculum scan . -f sarif -o report Export for GitHub Code Scanning
44430
+ $ oculum scan . --incremental Only scan changed files (git)
44431
+ $ oculum scan . --diff origin/main Compare against base branch
44432
+ $ oculum scan . -q Quiet mode for CI/CD
44433
+
44434
+ Configuration:
44435
+ Create oculum.config.json in your project:
44436
+ {
44437
+ "depth": "validated",
44438
+ "failOn": "high",
44439
+ "ignore": ["**/test/**"]
44440
+ }
44441
+
44442
+ More Help:
44443
+ $ oculum help scan-modes Detailed mode comparison
44444
+ $ oculum help ci-setup CI/CD integration examples
44445
+ $ oculum help config Full configuration options
44089
44446
  `).action(runScan);
44090
44447
 
44091
44448
  // src/commands/auth.ts
@@ -44211,7 +44568,10 @@ async function status() {
44211
44568
  spinner.succeed(" Authenticated");
44212
44569
  console.log("");
44213
44570
  const email = result.email || config.email || "unknown";
44214
- const tier = result.tier || config.tier || "free";
44571
+ const tier = result.tier || "free";
44572
+ if (tier !== config.tier || email !== config.email) {
44573
+ setAuthCredentials(config.apiKey, email, tier);
44574
+ }
44215
44575
  const tierBadge = tier === "pro" ? source_default.bgBlue.white(" PRO ") : tier === "enterprise" ? source_default.bgMagenta.white(" ENTERPRISE ") : source_default.bgGray.white(" FREE ");
44216
44576
  console.log(source_default.dim(" Email: ") + source_default.white(email));
44217
44577
  console.log(source_default.dim(" Plan: ") + tierBadge);
@@ -44788,10 +45148,10 @@ var foreach = (val, fn) => {
44788
45148
  fn(val);
44789
45149
  }
44790
45150
  };
44791
- var addAndConvert = (main2, prop, item) => {
44792
- let container = main2[prop];
45151
+ var addAndConvert = (main3, prop, item) => {
45152
+ let container = main3[prop];
44793
45153
  if (!(container instanceof Set)) {
44794
- main2[prop] = container = /* @__PURE__ */ new Set([container]);
45154
+ main3[prop] = container = /* @__PURE__ */ new Set([container]);
44795
45155
  }
44796
45156
  container.add(item);
44797
45157
  };
@@ -44803,12 +45163,12 @@ var clearItem = (cont) => (key) => {
44803
45163
  delete cont[key];
44804
45164
  }
44805
45165
  };
44806
- var delFromSet = (main2, prop, item) => {
44807
- const container = main2[prop];
45166
+ var delFromSet = (main3, prop, item) => {
45167
+ const container = main3[prop];
44808
45168
  if (container instanceof Set) {
44809
45169
  container.delete(item);
44810
45170
  } else if (container === item) {
44811
- delete main2[prop];
45171
+ delete main3[prop];
44812
45172
  }
44813
45173
  };
44814
45174
  var isEmptySet = (val) => val instanceof Set ? val.size === 0 : !val;
@@ -46853,6 +47213,7 @@ var CONFIG_DIR3 = (0, import_path6.join)((0, import_os4.homedir)(), ".oculum");
46853
47213
  var STATE_FILE = (0, import_path6.join)(CONFIG_DIR3, "state.json");
46854
47214
  var DEFAULT_STATE = {
46855
47215
  onboardingComplete: false,
47216
+ setupWizardComplete: false,
46856
47217
  totalScans: 0,
46857
47218
  welcomeShown: false
46858
47219
  };
@@ -46881,7 +47242,7 @@ function updateUserState(updates) {
46881
47242
  }
46882
47243
  function isFirstTimeUser() {
46883
47244
  const state = getUserState();
46884
- return !state.onboardingComplete && state.totalScans === 0;
47245
+ return !state.setupWizardComplete && !state.onboardingComplete && state.totalScans === 0;
46885
47246
  }
46886
47247
  function shouldShowWelcome() {
46887
47248
  const state = getUserState();
@@ -47797,6 +48158,9 @@ async function runAuthFlow() {
47797
48158
  const verified = await verifyApiKey(config.apiKey);
47798
48159
  if (verified.valid && verified.tier) {
47799
48160
  currentTier = verified.tier;
48161
+ if (currentTier !== config.tier || verified.email && verified.email !== config.email) {
48162
+ setAuthCredentials(config.apiKey, verified.email || config.email, currentTier);
48163
+ }
47800
48164
  }
47801
48165
  } catch {
47802
48166
  }
@@ -47826,9 +48190,14 @@ async function runAuthFlow() {
47826
48190
  s.stop("Stored credentials are invalid or expired.");
47827
48191
  continue;
47828
48192
  }
48193
+ const email = verified.email || getConfig().email || "unknown";
48194
+ const tier = verified.tier || "free";
48195
+ if (tier !== getConfig().tier || email !== getConfig().email) {
48196
+ setAuthCredentials(getConfig().apiKey, email, tier);
48197
+ }
47829
48198
  s.stop("Authenticated");
47830
- M2.info(`Email: ${verified.email || getConfig().email || "unknown"}`);
47831
- M2.info(`Tier: ${verified.tier || getConfig().tier || "unknown"}`);
48199
+ M2.info(`Email: ${email}`);
48200
+ M2.info(`Tier: ${tier}`);
47832
48201
  } catch (err) {
47833
48202
  s.stop("Verification failed");
47834
48203
  M2.error(String(err));
@@ -48397,9 +48766,251 @@ function getTimeAgo2(date) {
48397
48766
  }
48398
48767
  var usageCommand = new Command("usage").description("Show current usage and quota").option("--json", "Output as JSON").action(usage);
48399
48768
 
48769
+ // src/commands/help.ts
48770
+ var TOPICS = {
48771
+ "scan-modes": showScanModes,
48772
+ "ci-setup": showCISetup,
48773
+ "config": showConfig,
48774
+ "troubleshooting": showTroubleshooting
48775
+ };
48776
+ 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) => {
48777
+ if (!topic) {
48778
+ showTopicList();
48779
+ return;
48780
+ }
48781
+ const normalizedTopic = topic.toLowerCase().replace(/_/g, "-");
48782
+ if (normalizedTopic in TOPICS) {
48783
+ TOPICS[normalizedTopic]();
48784
+ } else {
48785
+ console.log(source_default.red(`Unknown topic: ${topic}
48786
+ `));
48787
+ showTopicList();
48788
+ }
48789
+ });
48790
+ function showTopicList() {
48791
+ console.log(source_default.bold("\nOculum Help Topics\n"));
48792
+ console.log(source_default.dim("\u2500".repeat(50)));
48793
+ console.log();
48794
+ console.log(source_default.cyan(" scan-modes ") + source_default.white("Compare cheap, validated, and deep scans"));
48795
+ console.log(source_default.cyan(" ci-setup ") + source_default.white("GitHub Actions and GitLab CI examples"));
48796
+ console.log(source_default.cyan(" config ") + source_default.white("Configuration file documentation"));
48797
+ console.log(source_default.cyan(" troubleshooting ") + source_default.white("Common issues and solutions"));
48798
+ console.log();
48799
+ console.log(source_default.dim("\u2500".repeat(50)));
48800
+ console.log(source_default.dim("\nUsage: oculum help <topic>"));
48801
+ console.log(source_default.dim("Example: oculum help scan-modes\n"));
48802
+ }
48803
+ function showScanModes() {
48804
+ console.log(source_default.bold("\nScan Modes Comparison\n"));
48805
+ console.log(source_default.dim("\u2500".repeat(60) + "\n"));
48806
+ console.log(source_default.green.bold(" CHEAP (Quick Scan)"));
48807
+ 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"));
48808
+ console.log(source_default.white(" Cost: ") + source_default.green("Free"));
48809
+ console.log(source_default.white(" Speed: ") + source_default.white("~1000 files/second"));
48810
+ console.log(source_default.white(" How: ") + source_default.dim("Pattern matching + entropy analysis"));
48811
+ console.log(source_default.white(" Best for: ") + source_default.dim("Quick checks, CI/CD pipelines"));
48812
+ console.log(source_default.white(" Limitation: ") + source_default.dim("May have more false positives\n"));
48813
+ console.log(source_default.blue.bold(" VALIDATED"));
48814
+ 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"));
48815
+ console.log(source_default.white(" Cost: ") + source_default.yellow("~$0.03 per 300 files"));
48816
+ console.log(source_default.white(" Speed: ") + source_default.white("~30 seconds for 300 files"));
48817
+ console.log(source_default.white(" How: ") + source_default.dim("Pattern matching + AI validation"));
48818
+ console.log(source_default.white(" Best for: ") + source_default.dim("Pre-commit checks, PR reviews"));
48819
+ console.log(source_default.white(" Benefit: ") + source_default.dim("~70% fewer false positives\n"));
48820
+ console.log(source_default.magenta.bold(" DEEP"));
48821
+ 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"));
48822
+ console.log(source_default.white(" Cost: ") + source_default.yellow("~$0.10 per 300 files"));
48823
+ console.log(source_default.white(" Speed: ") + source_default.white("~60 seconds for 300 files"));
48824
+ console.log(source_default.white(" How: ") + source_default.dim("Full semantic analysis with AI reasoning"));
48825
+ console.log(source_default.white(" Best for: ") + source_default.dim("Security audits, release checks"));
48826
+ console.log(source_default.white(" Benefit: ") + source_default.dim("Deepest analysis, remediation advice\n"));
48827
+ console.log(source_default.dim("\u2500".repeat(60)));
48828
+ console.log(source_default.bold("\nUsage Examples:\n"));
48829
+ console.log(source_default.dim(" $ oculum scan . ") + source_default.white("# Quick scan (free)"));
48830
+ console.log(source_default.dim(" $ oculum scan . -d validated ") + source_default.white("# AI-validated scan"));
48831
+ console.log(source_default.dim(" $ oculum scan . -d deep ") + source_default.white("# Deep semantic analysis"));
48832
+ console.log();
48833
+ }
48834
+ function showCISetup() {
48835
+ console.log(source_default.bold("\nCI/CD Integration\n"));
48836
+ console.log(source_default.dim("\u2500".repeat(60) + "\n"));
48837
+ console.log(source_default.bold(" GitHub Actions\n"));
48838
+ console.log(source_default.dim(" Create .github/workflows/oculum.yml:\n"));
48839
+ console.log(source_default.cyan(` name: Security Scan
48840
+ on: [push, pull_request]
48841
+ jobs:
48842
+ scan:
48843
+ runs-on: ubuntu-latest
48844
+ steps:
48845
+ - uses: actions/checkout@v4
48846
+ - uses: actions/setup-node@v4
48847
+ with:
48848
+ node-version: '20'
48849
+ - run: npm install -g oculum
48850
+ - run: oculum scan . --fail-on high
48851
+ env:
48852
+ OCULUM_API_KEY: \${{ secrets.OCULUM_API_KEY }}`));
48853
+ 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"));
48854
+ console.log(source_default.bold(" GitLab CI\n"));
48855
+ console.log(source_default.dim(" Add to .gitlab-ci.yml:\n"));
48856
+ console.log(source_default.cyan(` security-scan:
48857
+ image: node:20
48858
+ script:
48859
+ - npm install -g oculum
48860
+ - oculum scan . --fail-on high
48861
+ variables:
48862
+ OCULUM_API_KEY: $OCULUM_API_KEY`));
48863
+ console.log(source_default.dim("\n\u2500".repeat(60)));
48864
+ console.log(source_default.bold("\nTips:\n"));
48865
+ console.log(source_default.white(" - Use ") + source_default.cyan("--fail-on high") + source_default.white(" to fail builds on high/critical findings"));
48866
+ console.log(source_default.white(" - Use ") + source_default.cyan("--format sarif") + source_default.white(" for GitHub Code Scanning integration"));
48867
+ console.log(source_default.white(" - Use ") + source_default.cyan("-d cheap") + source_default.white(" for fastest scans (free, no API key needed)"));
48868
+ console.log(source_default.white(" - Store API key in secrets, not in code\n"));
48869
+ }
48870
+ function showConfig() {
48871
+ console.log(source_default.bold("\nConfiguration Files\n"));
48872
+ console.log(source_default.dim("\u2500".repeat(60) + "\n"));
48873
+ console.log(source_default.bold(" Project Config (oculum.config.json)\n"));
48874
+ console.log(source_default.dim(" Create in your project root:\n"));
48875
+ console.log(source_default.cyan(` {
48876
+ "depth": "validated",
48877
+ "failOn": "high",
48878
+ "format": "terminal",
48879
+ "ignore": [
48880
+ "**/node_modules/**",
48881
+ "**/dist/**",
48882
+ "**/*.test.ts"
48883
+ ],
48884
+ "include": [
48885
+ "src/**"
48886
+ ],
48887
+ "watch": {
48888
+ "debounce": 500,
48889
+ "clear": true
48890
+ }
48891
+ }`));
48892
+ 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"));
48893
+ console.log(source_default.bold(" Config Options\n"));
48894
+ console.log(source_default.white(" depth ") + source_default.dim("Scan depth: cheap | validated | deep"));
48895
+ console.log(source_default.white(" failOn ") + source_default.dim("Exit code 1 on: critical | high | medium | low | none"));
48896
+ console.log(source_default.white(" format ") + source_default.dim("Output format: terminal | json | sarif | markdown"));
48897
+ console.log(source_default.white(" output ") + source_default.dim("Output file path"));
48898
+ console.log(source_default.white(" quiet ") + source_default.dim("Only show findings (boolean)"));
48899
+ console.log(source_default.white(" ignore ") + source_default.dim("Glob patterns to exclude"));
48900
+ console.log(source_default.white(" include ") + source_default.dim("Glob patterns to include"));
48901
+ 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"));
48902
+ console.log(source_default.bold(" Config File Priority\n"));
48903
+ console.log(source_default.dim(" CLI flags > Project config > User config > Defaults\n"));
48904
+ console.log(source_default.white(" Supported filenames:"));
48905
+ console.log(source_default.dim(" - oculum.config.json"));
48906
+ console.log(source_default.dim(" - .oculumrc.json"));
48907
+ console.log(source_default.dim(" - .oculumrc\n"));
48908
+ }
48909
+ function showTroubleshooting() {
48910
+ console.log(source_default.bold("\nTroubleshooting\n"));
48911
+ console.log(source_default.dim("\u2500".repeat(60) + "\n"));
48912
+ console.log(source_default.red.bold(" Authentication Errors\n"));
48913
+ console.log(source_default.white(' "Authentication required"'));
48914
+ console.log(source_default.dim(" \u2192 Run `oculum login` to authenticate"));
48915
+ console.log(source_default.dim(" \u2192 Or use `--depth cheap` for free local scans\n"));
48916
+ console.log(source_default.white(' "API key invalid or expired"'));
48917
+ console.log(source_default.dim(" \u2192 Run `oculum login` to re-authenticate"));
48918
+ console.log(source_default.dim(" \u2192 Check key at https://oculum.dev/dashboard/api-keys\n"));
48919
+ console.log(source_default.white(' "Insufficient tier"'));
48920
+ console.log(source_default.dim(" \u2192 Validated/deep scans require Pro subscription"));
48921
+ console.log(source_default.dim(" \u2192 Visit https://oculum.dev/billing to upgrade\n"));
48922
+ 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"));
48923
+ console.log(source_default.yellow.bold(" Network Errors\n"));
48924
+ console.log(source_default.white(' "Connection refused" / "Network error"'));
48925
+ console.log(source_default.dim(" \u2192 Check your internet connection"));
48926
+ console.log(source_default.dim(" \u2192 Use `--depth cheap` for offline scans"));
48927
+ console.log(source_default.dim(" \u2192 Check https://status.oculum.dev\n"));
48928
+ console.log(source_default.white(' "SSL certificate error"'));
48929
+ console.log(source_default.dim(" \u2192 Corporate proxy may be intercepting HTTPS"));
48930
+ console.log(source_default.dim(" \u2192 Use `--depth cheap` for offline scans"));
48931
+ console.log(source_default.dim(" \u2192 Contact IT for proxy configuration\n"));
48932
+ 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"));
48933
+ console.log(source_default.blue.bold(" Scan Issues\n"));
48934
+ console.log(source_default.white(' "No scannable files found"'));
48935
+ console.log(source_default.dim(" \u2192 Check path exists: ls <path>"));
48936
+ console.log(source_default.dim(" \u2192 Ensure files have supported extensions"));
48937
+ console.log(source_default.dim(" \u2192 Check .gitignore isn't excluding files\n"));
48938
+ console.log(source_default.white(' "Request timed out"'));
48939
+ console.log(source_default.dim(" \u2192 Try scanning fewer files"));
48940
+ console.log(source_default.dim(" \u2192 Use `--depth cheap` for large codebases"));
48941
+ console.log(source_default.dim(" \u2192 Split into multiple scans\n"));
48942
+ console.log(source_default.white(' "Out of memory"'));
48943
+ console.log(source_default.dim(" \u2192 Scan a smaller directory"));
48944
+ console.log(source_default.dim(' \u2192 Increase Node memory: NODE_OPTIONS="--max-old-space-size=4096"\n'));
48945
+ console.log(source_default.dim("\u2500".repeat(60)));
48946
+ console.log(source_default.bold("\nStill stuck?\n"));
48947
+ console.log(source_default.white(" - Check docs: ") + source_default.cyan("https://oculum.dev/docs"));
48948
+ console.log(source_default.white(" - Report issues: ") + source_default.cyan("https://github.com/oculum-dev/oculum/issues"));
48949
+ console.log(source_default.white(" - Get support: ") + source_default.cyan("support@oculum.dev\n"));
48950
+ }
48951
+
48952
+ // src/utils/ci-detect.ts
48953
+ var CI_ENV_VARS = [
48954
+ "CI",
48955
+ // Generic CI flag (GitHub Actions, GitLab CI, etc.)
48956
+ "GITHUB_ACTIONS",
48957
+ // GitHub Actions
48958
+ "GITLAB_CI",
48959
+ // GitLab CI
48960
+ "JENKINS_URL",
48961
+ // Jenkins
48962
+ "CIRCLECI",
48963
+ // CircleCI
48964
+ "TRAVIS",
48965
+ // Travis CI
48966
+ "BUILDKITE",
48967
+ // Buildkite
48968
+ "DRONE",
48969
+ // Drone CI
48970
+ "TEAMCITY_VERSION",
48971
+ // TeamCity
48972
+ "BITBUCKET_COMMIT",
48973
+ // Bitbucket Pipelines
48974
+ "AZURE_HTTP_USER_AGENT",
48975
+ // Azure Pipelines
48976
+ "TF_BUILD",
48977
+ // Azure Pipelines (alternative)
48978
+ "CODEBUILD_BUILD_ID",
48979
+ // AWS CodeBuild
48980
+ "APPVEYOR",
48981
+ // AppVeyor
48982
+ "SEMAPHORE",
48983
+ // Semaphore CI
48984
+ "HEROKU_TEST_RUN_ID",
48985
+ // Heroku CI
48986
+ "RENDER",
48987
+ // Render
48988
+ "VERCEL",
48989
+ // Vercel
48990
+ "NETLIFY",
48991
+ // Netlify
48992
+ "CF_PAGES",
48993
+ // Cloudflare Pages
48994
+ "RAILWAY_ENVIRONMENT"
48995
+ // Railway
48996
+ ];
48997
+ function isCI() {
48998
+ return CI_ENV_VARS.some((envVar) => !!process.env[envVar]);
48999
+ }
49000
+ function isInteractiveTerminal() {
49001
+ return !!(process.stdin.isTTY && !isCI());
49002
+ }
49003
+
48400
49004
  // src/index.ts
49005
+ function shouldRunUI() {
49006
+ const programName = process.argv[1] || "";
49007
+ const args = process.argv.slice(2);
49008
+ const isOcAlias = programName.endsWith("oc") && !programName.endsWith("oculum");
49009
+ const isUICommand = args.length === 0 || args.length === 1 && args[0] === "ui";
49010
+ return isOcAlias || isUICommand;
49011
+ }
48401
49012
  var program2 = new Command();
48402
- program2.name("oculum").description("AI-native security scanner for detecting vulnerabilities in LLM-generated code").version("1.0.7").addHelpText("after", `
49013
+ program2.name("oculum").description("AI-native security scanner for detecting vulnerabilities in LLM-generated code").version("1.0.9").addHelpText("after", `
48403
49014
  Quick Start:
48404
49015
  $ oculum scan . Scan current directory (free)
48405
49016
  $ oculum ui Interactive mode with guided setup
@@ -48412,11 +49023,13 @@ Common Commands:
48412
49023
  login Authenticate with Oculum
48413
49024
  status Check authentication status
48414
49025
  usage View credits and quota
49026
+ help [topic] Get help on specific topics
48415
49027
 
48416
49028
  Learn More:
48417
49029
  $ oculum scan --help Detailed scan options
49030
+ $ oculum help scan-modes Compare scan depth options
48418
49031
  $ oculum --help All available commands
48419
-
49032
+
48420
49033
  Documentation: https://oculum.dev/docs
48421
49034
  `);
48422
49035
  program2.addCommand(scanCommand, { isDefault: true });
@@ -48427,7 +49040,17 @@ program2.addCommand(upgradeCommand);
48427
49040
  program2.addCommand(usageCommand);
48428
49041
  program2.addCommand(watchCommand);
48429
49042
  program2.addCommand(uiCommand);
48430
- program2.parse();
49043
+ program2.addCommand(helpCommand);
49044
+ async function main2() {
49045
+ const interactive = isInteractiveTerminal();
49046
+ if (interactive && shouldRunUI()) {
49047
+ process.argv = ["node", "oculum", "ui"];
49048
+ program2.parse();
49049
+ return;
49050
+ }
49051
+ program2.parse();
49052
+ }
49053
+ main2();
48431
49054
  /*! Bundled license information:
48432
49055
 
48433
49056
  chokidar/esm/index.js:
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "@oculum/cli",
3
- "version": "1.0.8",
3
+ "version": "1.0.10",
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": {
7
- "oculum": "./dist/index.js"
7
+ "oculum": "./dist/index.js",
8
+ "oc": "./dist/index.js"
8
9
  },
9
10
  "author": "Felix Westin <felix.lwestin@gmail.com>",
10
11
  "license": "MIT",
@@ -18,7 +19,7 @@
18
19
  "url": "https://github.com/flexipie/oculum/issues"
19
20
  },
20
21
  "scripts": {
21
- "build": "esbuild src/index.ts --bundle --platform=node --target=node18 --outfile=dist/index.js --banner:js=\"#!/usr/bin/env node\" --define:process.env.OCULUM_API_URL='undefined' --define:VERSION='\"1.0.8\"'",
22
+ "build": "esbuild src/index.ts --bundle --platform=node --target=node18 --outfile=dist/index.js --banner:js=\"#!/usr/bin/env node\" --define:process.env.OCULUM_API_URL='undefined' --define:VERSION='\"1.0.9\"'",
22
23
  "dev": "npm run build -- --watch",
23
24
  "test": "echo \"No tests configured yet\"",
24
25
  "lint": "eslint src/"