@oculum/cli 1.0.10 → 1.0.11

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 +462 -135
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -40915,8 +40915,8 @@ var {
40915
40915
  } = import_index.default;
40916
40916
 
40917
40917
  // src/commands/scan.ts
40918
- var import_path3 = require("path");
40919
- var import_fs4 = require("fs");
40918
+ var import_path4 = require("path");
40919
+ var import_fs5 = require("fs");
40920
40920
  init_esm7();
40921
40921
 
40922
40922
  // node_modules/chalk/source/vendor/ansi-styles/index.js
@@ -43882,8 +43882,83 @@ function validateConfig(config) {
43882
43882
  validated.watch.clear = config.watch.clear;
43883
43883
  }
43884
43884
  }
43885
+ if (config.profiles && typeof config.profiles === "object") {
43886
+ validated.profiles = config.profiles;
43887
+ }
43888
+ if (typeof config.defaultProfile === "string") {
43889
+ validated.defaultProfile = config.defaultProfile;
43890
+ }
43885
43891
  return validated;
43886
43892
  }
43893
+ function getProfileConfig(projectConfig, profileName) {
43894
+ if (!projectConfig) return null;
43895
+ const name = profileName || projectConfig.defaultProfile;
43896
+ if (!name) return null;
43897
+ const profile = projectConfig.profiles?.[name];
43898
+ if (!profile) {
43899
+ if (profileName) {
43900
+ console.warn(source_default.yellow(`Warning: Profile "${profileName}" not found in config`));
43901
+ }
43902
+ return null;
43903
+ }
43904
+ return profile;
43905
+ }
43906
+
43907
+ // src/utils/history.ts
43908
+ var import_fs4 = require("fs");
43909
+ var import_path3 = require("path");
43910
+ var import_os2 = require("os");
43911
+ var import_crypto = require("crypto");
43912
+ var CONFIG_DIR2 = (0, import_path3.join)((0, import_os2.homedir)(), ".oculum");
43913
+ var HISTORY_FILE = (0, import_path3.join)(CONFIG_DIR2, "history.json");
43914
+ var MAX_ENTRIES = 25;
43915
+ function ensureConfigDir2() {
43916
+ if (!(0, import_fs4.existsSync)(CONFIG_DIR2)) {
43917
+ (0, import_fs4.mkdirSync)(CONFIG_DIR2, { recursive: true });
43918
+ }
43919
+ }
43920
+ function readHistoryFile() {
43921
+ try {
43922
+ if (!(0, import_fs4.existsSync)(HISTORY_FILE)) return [];
43923
+ const content = (0, import_fs4.readFileSync)(HISTORY_FILE, "utf-8");
43924
+ const parsed = JSON.parse(content);
43925
+ if (!Array.isArray(parsed)) return [];
43926
+ return parsed;
43927
+ } catch {
43928
+ return [];
43929
+ }
43930
+ }
43931
+ function writeHistoryFile(entries) {
43932
+ ensureConfigDir2();
43933
+ (0, import_fs4.writeFileSync)(HISTORY_FILE, JSON.stringify(entries, null, 2));
43934
+ }
43935
+ function listScanHistory() {
43936
+ return readHistoryFile();
43937
+ }
43938
+ function getScanHistoryEntry(id) {
43939
+ return readHistoryFile().find((e2) => e2.id === id);
43940
+ }
43941
+ function addScanHistoryEntry(input) {
43942
+ const entry = {
43943
+ id: input.id ?? (0, import_crypto.randomUUID)(),
43944
+ createdAt: input.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
43945
+ targetPath: input.targetPath,
43946
+ options: input.options,
43947
+ result: input.result
43948
+ };
43949
+ const current = readHistoryFile();
43950
+ const updated = [entry, ...current].slice(0, MAX_ENTRIES);
43951
+ writeHistoryFile(updated);
43952
+ return entry;
43953
+ }
43954
+ function deleteScanHistoryEntry(id) {
43955
+ const current = readHistoryFile();
43956
+ const updated = current.filter((e2) => e2.id !== id);
43957
+ writeHistoryFile(updated);
43958
+ }
43959
+ function clearScanHistory() {
43960
+ writeHistoryFile([]);
43961
+ }
43887
43962
 
43888
43963
  // src/commands/scan.ts
43889
43964
  function getChangedFiles(absolutePath, diff) {
@@ -43900,22 +43975,22 @@ function getChangedFiles(absolutePath, diff) {
43900
43975
  args = ["diff", "--name-only", "HEAD"];
43901
43976
  }
43902
43977
  const out = (0, import_child_process.execFileSync)("git", args, { cwd: gitRoot, stdio: ["ignore", "pipe", "ignore"] }).toString();
43903
- const files = out.split("\n").map((s) => s.trim()).filter(Boolean).map((p2) => (0, import_path3.resolve)(gitRoot, p2)).filter((p2) => p2.startsWith(absolutePath));
43978
+ const files = out.split("\n").map((s) => s.trim()).filter(Boolean).map((p2) => (0, import_path4.resolve)(gitRoot, p2)).filter((p2) => p2.startsWith(absolutePath));
43904
43979
  return files;
43905
43980
  } catch {
43906
43981
  return [];
43907
43982
  }
43908
43983
  }
43909
43984
  function isScannableFile(filePath) {
43910
- const ext2 = (0, import_path3.extname)(filePath).toLowerCase();
43911
- const fileName = (0, import_path3.basename)(filePath);
43985
+ const ext2 = (0, import_path4.extname)(filePath).toLowerCase();
43986
+ const fileName = (0, import_path4.basename)(filePath);
43912
43987
  if (import_scanner.SPECIAL_FILES.includes(fileName)) {
43913
43988
  return true;
43914
43989
  }
43915
43990
  return import_scanner.SCANNABLE_EXTENSIONS.includes(ext2);
43916
43991
  }
43917
43992
  function getLanguage(filePath) {
43918
- const ext2 = (0, import_path3.extname)(filePath).toLowerCase();
43993
+ const ext2 = (0, import_path4.extname)(filePath).toLowerCase();
43919
43994
  const langMap = {
43920
43995
  ".js": "javascript",
43921
43996
  ".jsx": "javascript",
@@ -43940,15 +44015,15 @@ function getLanguage(filePath) {
43940
44015
  return langMap[ext2] || "text";
43941
44016
  }
43942
44017
  async function collectFiles(targetPath) {
43943
- const absolutePath = (0, import_path3.resolve)(targetPath);
43944
- const stats = (0, import_fs4.statSync)(absolutePath);
44018
+ const absolutePath = (0, import_path4.resolve)(targetPath);
44019
+ const stats = (0, import_fs5.statSync)(absolutePath);
43945
44020
  const files = [];
43946
44021
  if (stats.isFile()) {
43947
44022
  if (isScannableFile(absolutePath)) {
43948
- const content = (0, import_fs4.readFileSync)(absolutePath, "utf-8");
44023
+ const content = (0, import_fs5.readFileSync)(absolutePath, "utf-8");
43949
44024
  if (content.length <= import_scanner.MAX_FILE_SIZE) {
43950
44025
  files.push({
43951
- path: (0, import_path3.relative)(process.cwd(), absolutePath),
44026
+ path: (0, import_path4.relative)(process.cwd(), absolutePath),
43952
44027
  content,
43953
44028
  language: getLanguage(absolutePath),
43954
44029
  size: content.length
@@ -44017,8 +44092,8 @@ async function collectFiles(targetPath) {
44017
44092
  "**/.env.*.local",
44018
44093
  "**/**.env"
44019
44094
  ];
44020
- const gitignorePath = (0, import_path3.join)(absolutePath, ".gitignore");
44021
- const hasGitignore = (0, import_fs4.existsSync)(gitignorePath);
44095
+ const gitignorePath = (0, import_path4.join)(absolutePath, ".gitignore");
44096
+ const hasGitignore = (0, import_fs5.existsSync)(gitignorePath);
44022
44097
  const globOptions = {
44023
44098
  cwd: absolutePath,
44024
44099
  ignore: ignorePatterns,
@@ -44032,11 +44107,11 @@ async function collectFiles(targetPath) {
44032
44107
  for (const file of foundFiles) {
44033
44108
  const filePath = String(file);
44034
44109
  try {
44035
- const fileStats = (0, import_fs4.statSync)(filePath);
44110
+ const fileStats = (0, import_fs5.statSync)(filePath);
44036
44111
  if (fileStats.size > import_scanner.MAX_FILE_SIZE) continue;
44037
- const content = (0, import_fs4.readFileSync)(filePath, "utf-8");
44112
+ const content = (0, import_fs5.readFileSync)(filePath, "utf-8");
44038
44113
  files.push({
44039
- path: (0, import_path3.relative)(process.cwd(), filePath),
44114
+ path: (0, import_path4.relative)(process.cwd(), filePath),
44040
44115
  content,
44041
44116
  language: getLanguage(filePath),
44042
44117
  size: content.length
@@ -44048,8 +44123,8 @@ async function collectFiles(targetPath) {
44048
44123
  return files;
44049
44124
  }
44050
44125
  async function collectFilesForScan(targetPath, options) {
44051
- const absolutePath = (0, import_path3.resolve)(targetPath);
44052
- const stats = (0, import_fs4.statSync)(absolutePath);
44126
+ const absolutePath = (0, import_path4.resolve)(targetPath);
44127
+ const stats = (0, import_fs5.statSync)(absolutePath);
44053
44128
  if (stats.isFile()) {
44054
44129
  return collectFiles(targetPath);
44055
44130
  }
@@ -44058,13 +44133,13 @@ async function collectFilesForScan(targetPath, options) {
44058
44133
  const files = [];
44059
44134
  for (const filePath of changed) {
44060
44135
  try {
44061
- if (!(0, import_fs4.existsSync)(filePath)) continue;
44136
+ if (!(0, import_fs5.existsSync)(filePath)) continue;
44062
44137
  if (!isScannableFile(filePath)) continue;
44063
- const fileStats = (0, import_fs4.statSync)(filePath);
44138
+ const fileStats = (0, import_fs5.statSync)(filePath);
44064
44139
  if (fileStats.size > import_scanner.MAX_FILE_SIZE) continue;
44065
- const content = (0, import_fs4.readFileSync)(filePath, "utf-8");
44140
+ const content = (0, import_fs5.readFileSync)(filePath, "utf-8");
44066
44141
  files.push({
44067
- path: (0, import_path3.relative)(process.cwd(), filePath),
44142
+ path: (0, import_path4.relative)(process.cwd(), filePath),
44068
44143
  content,
44069
44144
  language: getLanguage(filePath),
44070
44145
  size: content.length
@@ -44078,7 +44153,7 @@ async function collectFilesForScan(targetPath, options) {
44078
44153
  }
44079
44154
  function createEmptyResult(targetPath) {
44080
44155
  return {
44081
- repoName: (0, import_path3.basename)((0, import_path3.resolve)(targetPath)),
44156
+ repoName: (0, import_path4.basename)((0, import_path4.resolve)(targetPath)),
44082
44157
  repoUrl: "",
44083
44158
  branch: "local",
44084
44159
  filesScanned: 0,
@@ -44300,7 +44375,7 @@ async function runScanOnce(targetPath, options) {
44300
44375
  options.depth,
44301
44376
  config.apiKey,
44302
44377
  {
44303
- name: (0, import_path3.basename)((0, import_path3.resolve)(targetPath)),
44378
+ name: (0, import_path4.basename)((0, import_path4.resolve)(targetPath)),
44304
44379
  url: "",
44305
44380
  branch: "local"
44306
44381
  }
@@ -44312,7 +44387,7 @@ async function runScanOnce(targetPath, options) {
44312
44387
  result = await (0, import_scanner.runScan)(
44313
44388
  files,
44314
44389
  {
44315
- name: (0, import_path3.basename)((0, import_path3.resolve)(targetPath)),
44390
+ name: (0, import_path4.basename)((0, import_path4.resolve)(targetPath)),
44316
44391
  url: "",
44317
44392
  branch: "local"
44318
44393
  },
@@ -44375,32 +44450,33 @@ Results written to ${options.output}`));
44375
44450
  }
44376
44451
  async function runScan(targetPath, cliOptions) {
44377
44452
  const projectConfig = loadProjectConfig(targetPath, cliOptions.quiet);
44378
- let scanDepth = cliOptions.mode || cliOptions.depth || "cheap";
44453
+ const profileConfig = getProfileConfig(projectConfig, cliOptions.profile);
44454
+ let scanDepth = cliOptions.mode || cliOptions.depth;
44379
44455
  if (scanDepth === "quick") {
44380
44456
  scanDepth = "cheap";
44381
44457
  }
44382
- const baseOptions = {
44383
- depth: scanDepth,
44384
- format: cliOptions.format ?? "terminal",
44385
- failOn: cliOptions.failOn ?? "high",
44458
+ const options = {
44459
+ depth: scanDepth ?? profileConfig?.depth ?? projectConfig?.depth ?? "cheap",
44460
+ format: cliOptions.format ?? profileConfig?.format ?? projectConfig?.format ?? "terminal",
44461
+ failOn: cliOptions.failOn ?? profileConfig?.failOn ?? projectConfig?.failOn ?? "high",
44386
44462
  color: cliOptions.color,
44387
- quiet: cliOptions.quiet,
44463
+ quiet: cliOptions.quiet ?? profileConfig?.quiet ?? projectConfig?.quiet,
44388
44464
  verbose: cliOptions.verbose,
44389
44465
  incremental: cliOptions.incremental,
44390
44466
  diff: cliOptions.diff,
44391
- output: cliOptions.output
44392
- };
44393
- const options = {
44394
- ...baseOptions,
44395
- depth: baseOptions.depth ?? projectConfig?.depth ?? "cheap",
44396
- format: baseOptions.format ?? projectConfig?.format ?? "terminal",
44397
- failOn: baseOptions.failOn ?? projectConfig?.failOn ?? "high",
44398
- quiet: baseOptions.quiet ?? projectConfig?.quiet,
44399
- verbose: baseOptions.verbose,
44400
- output: baseOptions.output ?? projectConfig?.output
44467
+ output: cliOptions.output ?? profileConfig?.output ?? projectConfig?.output,
44468
+ profile: cliOptions.profile
44401
44469
  };
44402
44470
  try {
44403
- const { output, exitCode } = await runScanOnce(targetPath, options);
44471
+ const { result, output, exitCode } = await runScanOnce(targetPath, options);
44472
+ try {
44473
+ addScanHistoryEntry({
44474
+ targetPath,
44475
+ options: { depth: options.depth, format: options.format, failOn: options.failOn },
44476
+ result
44477
+ });
44478
+ } catch {
44479
+ }
44404
44480
  console.log(output);
44405
44481
  if (exitCode !== 0) {
44406
44482
  process.exit(exitCode);
@@ -44411,7 +44487,7 @@ async function runScan(targetPath, cliOptions) {
44411
44487
  process.exit(1);
44412
44488
  }
44413
44489
  }
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", `
44490
+ 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").option("-m, --mode <mode>", "alias for --depth (quick, validated, deep)").option("-f, --format <format>", "output format: terminal, json, sarif, markdown").option("--fail-on <severity>", "exit with error code if findings at severity").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("-p, --profile <name>", "use named profile from oculum.config.json").option("--incremental", "only scan changed files (requires git)").option("--diff <ref>", "diff against branch/commit for incremental scan").addHelpText("after", `
44415
44491
  Scan Modes:
44416
44492
  cheap Free Fast pattern matching, runs locally
44417
44493
  Best for: Quick checks, CI/CD pipelines
@@ -44436,9 +44512,14 @@ Configuration:
44436
44512
  {
44437
44513
  "depth": "validated",
44438
44514
  "failOn": "high",
44439
- "ignore": ["**/test/**"]
44515
+ "profiles": {
44516
+ "ci": { "depth": "validated", "quiet": true, "format": "sarif" },
44517
+ "strict": { "depth": "validated", "failOn": "medium" }
44518
+ }
44440
44519
  }
44441
44520
 
44521
+ Use profiles: oculum scan . --profile ci
44522
+
44442
44523
  More Help:
44443
44524
  $ oculum help scan-modes Detailed mode comparison
44444
44525
  $ oculum help ci-setup CI/CD integration examples
@@ -44615,11 +44696,12 @@ var statusCommand = new Command("status").description("Show current authenticati
44615
44696
  var upgradeCommand = new Command("upgrade").description("Upgrade your subscription").action(upgrade);
44616
44697
 
44617
44698
  // src/commands/watch.ts
44618
- var import_path4 = require("path");
44619
- var import_fs7 = require("fs");
44699
+ var import_path5 = require("path");
44700
+ var import_fs8 = require("fs");
44701
+ var readline = __toESM(require("readline"));
44620
44702
 
44621
44703
  // ../../node_modules/chokidar/esm/index.js
44622
- var import_fs6 = require("fs");
44704
+ var import_fs7 = require("fs");
44623
44705
  var import_promises4 = require("fs/promises");
44624
44706
  var import_events = require("events");
44625
44707
  var sysPath2 = __toESM(require("path"), 1);
@@ -44844,10 +44926,10 @@ function readdirp(root, options = {}) {
44844
44926
  }
44845
44927
 
44846
44928
  // ../../node_modules/chokidar/esm/handler.js
44847
- var import_fs5 = require("fs");
44929
+ var import_fs6 = require("fs");
44848
44930
  var import_promises3 = require("fs/promises");
44849
44931
  var sysPath = __toESM(require("path"), 1);
44850
- var import_os2 = require("os");
44932
+ var import_os3 = require("os");
44851
44933
  var STR_DATA = "data";
44852
44934
  var STR_END = "end";
44853
44935
  var STR_CLOSE = "close";
@@ -44858,7 +44940,7 @@ var isWindows = pl === "win32";
44858
44940
  var isMacos = pl === "darwin";
44859
44941
  var isLinux = pl === "linux";
44860
44942
  var isFreeBSD = pl === "freebsd";
44861
- var isIBMi = (0, import_os2.type)() === "OS400";
44943
+ var isIBMi = (0, import_os3.type)() === "OS400";
44862
44944
  var EVENTS = {
44863
44945
  ALL: "all",
44864
44946
  READY: "ready",
@@ -45182,7 +45264,7 @@ function createFsWatchInstance(path2, options, listener, errHandler, emitRaw) {
45182
45264
  }
45183
45265
  };
45184
45266
  try {
45185
- return (0, import_fs5.watch)(path2, {
45267
+ return (0, import_fs6.watch)(path2, {
45186
45268
  persistent: options.persistent
45187
45269
  }, handleEvent);
45188
45270
  } catch (error) {
@@ -45265,7 +45347,7 @@ var setFsWatchFileListener = (path2, fullPath, options, handlers) => {
45265
45347
  let cont = FsWatchFileInstances.get(fullPath);
45266
45348
  const copts = cont && cont.options;
45267
45349
  if (copts && (copts.persistent < options.persistent || copts.interval > options.interval)) {
45268
- (0, import_fs5.unwatchFile)(fullPath);
45350
+ (0, import_fs6.unwatchFile)(fullPath);
45269
45351
  cont = void 0;
45270
45352
  }
45271
45353
  if (cont) {
@@ -45276,7 +45358,7 @@ var setFsWatchFileListener = (path2, fullPath, options, handlers) => {
45276
45358
  listeners: listener,
45277
45359
  rawEmitters: rawEmitter,
45278
45360
  options,
45279
- watcher: (0, import_fs5.watchFile)(fullPath, options, (curr, prev) => {
45361
+ watcher: (0, import_fs6.watchFile)(fullPath, options, (curr, prev) => {
45280
45362
  foreach(cont.rawEmitters, (rawEmitter2) => {
45281
45363
  rawEmitter2(EV.CHANGE, fullPath, { curr, prev });
45282
45364
  });
@@ -45293,7 +45375,7 @@ var setFsWatchFileListener = (path2, fullPath, options, handlers) => {
45293
45375
  delFromSet(cont, KEY_RAW, rawEmitter);
45294
45376
  if (isEmptySet(cont.listeners)) {
45295
45377
  FsWatchFileInstances.delete(fullPath);
45296
- (0, import_fs5.unwatchFile)(fullPath);
45378
+ (0, import_fs6.unwatchFile)(fullPath);
45297
45379
  cont.options = cont.watcher = void 0;
45298
45380
  Object.freeze(cont);
45299
45381
  }
@@ -46136,7 +46218,7 @@ var FSWatcher = class extends import_events.EventEmitter {
46136
46218
  const now = /* @__PURE__ */ new Date();
46137
46219
  const writes = this._pendingWrites;
46138
46220
  function awaitWriteFinishFn(prevStat) {
46139
- (0, import_fs6.stat)(fullPath, (err, curStat) => {
46221
+ (0, import_fs7.stat)(fullPath, (err, curStat) => {
46140
46222
  if (err || !writes.has(path2)) {
46141
46223
  if (err && err.code !== "ENOENT")
46142
46224
  awfEmit(err);
@@ -46313,13 +46395,13 @@ var esm_default = { watch, FSWatcher };
46313
46395
  var import_scanner2 = __toESM(require_dist());
46314
46396
  var import_formatters2 = __toESM(require_formatters());
46315
46397
  function isScannableFile2(filePath) {
46316
- const ext2 = (0, import_path4.extname)(filePath).toLowerCase();
46317
- const fileName = (0, import_path4.basename)(filePath);
46398
+ const ext2 = (0, import_path5.extname)(filePath).toLowerCase();
46399
+ const fileName = (0, import_path5.basename)(filePath);
46318
46400
  if (import_scanner2.SPECIAL_FILES.includes(fileName)) return true;
46319
46401
  return import_scanner2.SCANNABLE_EXTENSIONS.includes(ext2);
46320
46402
  }
46321
46403
  function getLanguage2(filePath) {
46322
- const ext2 = (0, import_path4.extname)(filePath).toLowerCase();
46404
+ const ext2 = (0, import_path5.extname)(filePath).toLowerCase();
46323
46405
  const langMap = {
46324
46406
  ".js": "javascript",
46325
46407
  ".jsx": "javascript",
@@ -46338,11 +46420,11 @@ function getLanguage2(filePath) {
46338
46420
  }
46339
46421
  function readScanFile(filePath) {
46340
46422
  try {
46341
- const stats = (0, import_fs7.statSync)(filePath);
46423
+ const stats = (0, import_fs8.statSync)(filePath);
46342
46424
  if (stats.size > import_scanner2.MAX_FILE_SIZE) return null;
46343
- const content = (0, import_fs7.readFileSync)(filePath, "utf-8");
46425
+ const content = (0, import_fs8.readFileSync)(filePath, "utf-8");
46344
46426
  return {
46345
- path: (0, import_path4.relative)(process.cwd(), filePath),
46427
+ path: (0, import_path5.relative)(process.cwd(), filePath),
46346
46428
  content,
46347
46429
  language: getLanguage2(filePath),
46348
46430
  size: content.length
@@ -46359,7 +46441,7 @@ function debounce(fn, delay) {
46359
46441
  };
46360
46442
  }
46361
46443
  async function watch2(targetPath, options) {
46362
- const absolutePath = (0, import_path4.resolve)(targetPath);
46444
+ const absolutePath = (0, import_path5.resolve)(targetPath);
46363
46445
  const config = getConfig();
46364
46446
  if ((options.depth === "validated" || options.depth === "deep") && !isAuthenticated()) {
46365
46447
  if (!options.quiet) {
@@ -46369,31 +46451,40 @@ async function watch2(targetPath, options) {
46369
46451
  }
46370
46452
  options.depth = "cheap";
46371
46453
  }
46372
- if (!options.quiet) {
46454
+ const changedFiles = /* @__PURE__ */ new Set();
46455
+ let isPaused = false;
46456
+ let scanCount = 0;
46457
+ let totalIssues = 0;
46458
+ const showBanner = () => {
46459
+ if (options.quiet) return;
46373
46460
  console.log("");
46374
46461
  console.log(source_default.bold(" \u{1F441}\uFE0F Oculum Watch Mode"));
46375
- console.log(source_default.dim(" " + "\u2500".repeat(38)));
46462
+ console.log(source_default.dim(" " + "\u2500".repeat(50)));
46376
46463
  console.log("");
46377
46464
  console.log(source_default.dim(" Watching: ") + source_default.white(absolutePath));
46378
46465
  console.log(source_default.dim(" Depth: ") + source_default.white(options.depth === "cheap" ? "Quick (pattern matching)" : options.depth));
46379
- console.log(source_default.dim(" Debounce: ") + source_default.white(`${options.debounce}ms`));
46466
+ console.log(source_default.dim(" Status: ") + (isPaused ? source_default.yellow("Paused") : source_default.green("Active")));
46467
+ if (scanCount > 0) {
46468
+ console.log(source_default.dim(" Scans: ") + source_default.white(`${scanCount} (${totalIssues} issues found)`));
46469
+ }
46380
46470
  console.log("");
46381
- console.log(source_default.dim(" Press ") + source_default.white("Ctrl+C") + source_default.dim(" to stop watching"));
46471
+ console.log(source_default.dim(" Keyboard: ") + source_default.white("[r]") + source_default.dim(" rescan ") + source_default.white("[c]") + source_default.dim(" clear ") + source_default.white("[p]") + source_default.dim(" pause ") + source_default.white("[q]") + source_default.dim(" quit"));
46472
+ console.log(source_default.dim(" " + "\u2500".repeat(50)));
46382
46473
  console.log("");
46383
- }
46384
- const changedFiles = /* @__PURE__ */ new Set();
46474
+ };
46475
+ showBanner();
46385
46476
  let isScanning = false;
46386
46477
  const runScanOnChanges = debounce(async () => {
46387
- if (isScanning || changedFiles.size === 0) return;
46478
+ if (isScanning || changedFiles.size === 0 || isPaused) return;
46388
46479
  isScanning = true;
46389
46480
  const filesToScan = Array.from(changedFiles);
46390
46481
  changedFiles.clear();
46391
46482
  if (options.clearOnScan && !options.quiet) {
46392
46483
  console.clear();
46484
+ showBanner();
46393
46485
  }
46394
46486
  if (!options.quiet) {
46395
46487
  const fileText = filesToScan.length === 1 ? "file" : "files";
46396
- console.log("");
46397
46488
  console.log(source_default.cyan(` \u27F3 [${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] Scanning ${filesToScan.length} changed ${fileText}...`));
46398
46489
  }
46399
46490
  const scanFiles = [];
@@ -46403,7 +46494,7 @@ async function watch2(targetPath, options) {
46403
46494
  }
46404
46495
  if (scanFiles.length === 0) {
46405
46496
  if (!options.quiet) {
46406
- console.log(source_default.dim("No scannable files found."));
46497
+ console.log(source_default.dim(" No scannable files found."));
46407
46498
  }
46408
46499
  isScanning = false;
46409
46500
  return;
@@ -46412,24 +46503,36 @@ async function watch2(targetPath, options) {
46412
46503
  const result = await (0, import_scanner2.runScan)(
46413
46504
  scanFiles,
46414
46505
  {
46415
- name: (0, import_path4.basename)(absolutePath),
46506
+ name: (0, import_path5.basename)(absolutePath),
46416
46507
  url: "",
46417
46508
  branch: "watch"
46418
46509
  },
46419
46510
  {
46420
46511
  enableAI: options.depth !== "cheap" && isAuthenticated(),
46421
46512
  scanDepth: options.depth,
46422
- scanMode: "incremental"
46513
+ scanMode: "incremental",
46514
+ quiet: true
46515
+ // Suppress internal scanner logs
46423
46516
  }
46424
46517
  );
46518
+ scanCount++;
46519
+ totalIssues += result.vulnerabilities.length;
46520
+ try {
46521
+ addScanHistoryEntry({
46522
+ targetPath: absolutePath,
46523
+ options: { depth: options.depth, format: "terminal", failOn: "high" },
46524
+ result
46525
+ });
46526
+ } catch {
46527
+ }
46425
46528
  if (result.vulnerabilities.length === 0) {
46426
46529
  if (!options.quiet) {
46427
- console.log(source_default.green(" \u2713 No issues found"));
46530
+ console.log(source_default.green(" \u2713 No issues found"));
46428
46531
  }
46429
46532
  } else {
46430
46533
  const issueCount = result.vulnerabilities.length;
46431
46534
  const issueText = issueCount === 1 ? "issue" : "issues";
46432
- console.log(source_default.yellow(` \u26A0 Found ${issueCount} ${issueText}:`));
46535
+ console.log(source_default.yellow(` \u26A0 Found ${issueCount} ${issueText}:`));
46433
46536
  console.log((0, import_formatters2.formatTerminalOutput)(result, {
46434
46537
  maxFindingsPerGroup: 5
46435
46538
  }));
@@ -46440,6 +46543,47 @@ async function watch2(targetPath, options) {
46440
46543
  }
46441
46544
  isScanning = false;
46442
46545
  }, options.debounce);
46546
+ const triggerFullRescan = async () => {
46547
+ if (isScanning) return;
46548
+ if (!options.quiet) {
46549
+ console.log(source_default.cyan(`
46550
+ \u27F3 [${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] Triggering full rescan...`));
46551
+ }
46552
+ const { glob: glob3 } = await Promise.resolve().then(() => (init_esm7(), esm_exports));
46553
+ const patterns2 = [
46554
+ "**/*.js",
46555
+ "**/*.jsx",
46556
+ "**/*.ts",
46557
+ "**/*.tsx",
46558
+ "**/*.py",
46559
+ "**/*.go",
46560
+ "**/*.java",
46561
+ "**/*.rb",
46562
+ "**/*.php",
46563
+ "**/*.cs",
46564
+ "**/package.json"
46565
+ ];
46566
+ const ignorePatterns2 = [
46567
+ "**/node_modules/**",
46568
+ "**/dist/**",
46569
+ "**/build/**",
46570
+ "**/.git/**",
46571
+ "**/vendor/**",
46572
+ "**/__pycache__/**"
46573
+ ];
46574
+ const allFiles2 = await glob3(patterns2, {
46575
+ cwd: absolutePath,
46576
+ ignore: ignorePatterns2,
46577
+ nodir: true,
46578
+ absolute: true
46579
+ });
46580
+ changedFiles.clear();
46581
+ for (const filePath of allFiles2) {
46582
+ changedFiles.add(filePath);
46583
+ }
46584
+ isScanning = false;
46585
+ runScanOnChanges();
46586
+ };
46443
46587
  const watcher = esm_default.watch(absolutePath, {
46444
46588
  ignored: [
46445
46589
  "**/node_modules/**",
@@ -46459,17 +46603,17 @@ async function watch2(targetPath, options) {
46459
46603
  });
46460
46604
  watcher.on("change", (filePath) => {
46461
46605
  if (!isScannableFile2(filePath)) return;
46462
- changedFiles.add((0, import_path4.resolve)(filePath));
46606
+ changedFiles.add((0, import_path5.resolve)(filePath));
46463
46607
  if (!options.quiet) {
46464
- console.log(source_default.dim(`Changed: ${(0, import_path4.relative)(process.cwd(), filePath)}`));
46608
+ console.log(source_default.dim(`Changed: ${(0, import_path5.relative)(process.cwd(), filePath)}`));
46465
46609
  }
46466
46610
  runScanOnChanges();
46467
46611
  });
46468
46612
  watcher.on("add", (filePath) => {
46469
46613
  if (!isScannableFile2(filePath)) return;
46470
- changedFiles.add((0, import_path4.resolve)(filePath));
46614
+ changedFiles.add((0, import_path5.resolve)(filePath));
46471
46615
  if (!options.quiet) {
46472
- console.log(source_default.dim(`Added: ${(0, import_path4.relative)(process.cwd(), filePath)}`));
46616
+ console.log(source_default.dim(`Added: ${(0, import_path5.relative)(process.cwd(), filePath)}`));
46473
46617
  }
46474
46618
  runScanOnChanges();
46475
46619
  });
@@ -46477,13 +46621,47 @@ async function watch2(targetPath, options) {
46477
46621
  const enhanced = enhanceError(error);
46478
46622
  console.error(formatError(enhanced));
46479
46623
  });
46480
- process.on("SIGINT", () => {
46624
+ const cleanup = () => {
46481
46625
  if (!options.quiet) {
46482
- console.log(source_default.dim("\n\nStopping watch mode..."));
46626
+ console.log(source_default.dim("\n\n Stopping watch mode..."));
46627
+ if (scanCount > 0) {
46628
+ console.log(source_default.dim(` Completed ${scanCount} scans, found ${totalIssues} total issues.`));
46629
+ }
46630
+ console.log("");
46631
+ }
46632
+ if (process.stdin.isTTY) {
46633
+ process.stdin.setRawMode(false);
46483
46634
  }
46484
46635
  watcher.close();
46485
46636
  process.exit(0);
46486
- });
46637
+ };
46638
+ if (process.stdin.isTTY && !options.quiet) {
46639
+ readline.emitKeypressEvents(process.stdin);
46640
+ process.stdin.setRawMode(true);
46641
+ process.stdin.on("keypress", (ch, key) => {
46642
+ if (!key) return;
46643
+ if (key.name === "r") {
46644
+ triggerFullRescan();
46645
+ } else if (key.name === "c") {
46646
+ console.clear();
46647
+ showBanner();
46648
+ } else if (key.name === "p") {
46649
+ isPaused = !isPaused;
46650
+ if (isPaused) {
46651
+ console.log(source_default.yellow("\n \u23F8 Watch paused. Press [p] to resume.\n"));
46652
+ } else {
46653
+ console.log(source_default.green("\n \u25B6 Watch resumed.\n"));
46654
+ if (changedFiles.size > 0) {
46655
+ runScanOnChanges();
46656
+ }
46657
+ }
46658
+ } else if (key.name === "q" || key.ctrl && key.name === "c") {
46659
+ cleanup();
46660
+ }
46661
+ });
46662
+ } else {
46663
+ process.on("SIGINT", cleanup);
46664
+ }
46487
46665
  if (!options.quiet) {
46488
46666
  console.log(source_default.dim("Performing initial scan..."));
46489
46667
  }
@@ -46527,9 +46705,15 @@ Examples:
46527
46705
  $ oculum watch . --clear # Clear console between scans
46528
46706
  $ oculum watch . --debounce 1000 # Wait 1s after changes
46529
46707
 
46708
+ Keyboard Controls:
46709
+ [r] Rescan all files
46710
+ [c] Clear console
46711
+ [p] Pause/resume watching
46712
+ [q] Quit watch mode
46713
+
46530
46714
  Tips:
46531
46715
  \u2022 Watch mode uses 'cheap' depth by default for fast feedback
46532
- \u2022 Press Ctrl+C to stop watching
46716
+ \u2022 Scans are recorded to history (view with: oculum history)
46533
46717
  \u2022 Combine with oculum.config.json for project-specific settings
46534
46718
  `).action((path2, cliOptions) => {
46535
46719
  const projectConfig = loadProjectConfig(path2, cliOptions.quiet);
@@ -47152,59 +47336,6 @@ var Y2 = ({ indicator: t = "dots" } = {}) => {
47152
47336
  // src/commands/ui.ts
47153
47337
  var import_fs11 = require("fs");
47154
47338
 
47155
- // src/utils/history.ts
47156
- var import_fs8 = require("fs");
47157
- var import_path5 = require("path");
47158
- var import_os3 = require("os");
47159
- var import_crypto = require("crypto");
47160
- var CONFIG_DIR2 = (0, import_path5.join)((0, import_os3.homedir)(), ".oculum");
47161
- var HISTORY_FILE = (0, import_path5.join)(CONFIG_DIR2, "history.json");
47162
- var MAX_ENTRIES = 25;
47163
- function ensureConfigDir2() {
47164
- if (!(0, import_fs8.existsSync)(CONFIG_DIR2)) {
47165
- (0, import_fs8.mkdirSync)(CONFIG_DIR2, { recursive: true });
47166
- }
47167
- }
47168
- function readHistoryFile() {
47169
- try {
47170
- if (!(0, import_fs8.existsSync)(HISTORY_FILE)) return [];
47171
- const content = (0, import_fs8.readFileSync)(HISTORY_FILE, "utf-8");
47172
- const parsed = JSON.parse(content);
47173
- if (!Array.isArray(parsed)) return [];
47174
- return parsed;
47175
- } catch {
47176
- return [];
47177
- }
47178
- }
47179
- function writeHistoryFile(entries) {
47180
- ensureConfigDir2();
47181
- (0, import_fs8.writeFileSync)(HISTORY_FILE, JSON.stringify(entries, null, 2));
47182
- }
47183
- function listScanHistory() {
47184
- return readHistoryFile();
47185
- }
47186
- function addScanHistoryEntry(input) {
47187
- const entry = {
47188
- id: input.id ?? (0, import_crypto.randomUUID)(),
47189
- createdAt: input.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
47190
- targetPath: input.targetPath,
47191
- options: input.options,
47192
- result: input.result
47193
- };
47194
- const current = readHistoryFile();
47195
- const updated = [entry, ...current].slice(0, MAX_ENTRIES);
47196
- writeHistoryFile(updated);
47197
- return entry;
47198
- }
47199
- function deleteScanHistoryEntry(id) {
47200
- const current = readHistoryFile();
47201
- const updated = current.filter((e2) => e2.id !== id);
47202
- writeHistoryFile(updated);
47203
- }
47204
- function clearScanHistory() {
47205
- writeHistoryFile([]);
47206
- }
47207
-
47208
47339
  // src/utils/first-run.ts
47209
47340
  var import_fs9 = require("fs");
47210
47341
  var import_path6 = require("path");
@@ -48949,6 +49080,200 @@ function showTroubleshooting() {
48949
49080
  console.log(source_default.white(" - Get support: ") + source_default.cyan("support@oculum.dev\n"));
48950
49081
  }
48951
49082
 
49083
+ // src/commands/history.ts
49084
+ function formatDate3(isoString) {
49085
+ const date = new Date(isoString);
49086
+ return date.toLocaleString("en-US", {
49087
+ month: "short",
49088
+ day: "numeric",
49089
+ hour: "2-digit",
49090
+ minute: "2-digit"
49091
+ });
49092
+ }
49093
+ function truncate(str, maxLen) {
49094
+ if (str.length <= maxLen) return str;
49095
+ return str.slice(0, maxLen - 2) + "..";
49096
+ }
49097
+ function getStatusBadge(entry) {
49098
+ const { severityCounts } = entry.result;
49099
+ if (severityCounts.critical > 0 || severityCounts.high > 0) {
49100
+ return source_default.red("FAIL");
49101
+ }
49102
+ return source_default.green("PASS");
49103
+ }
49104
+ function getTotalIssues(entry) {
49105
+ const { severityCounts } = entry.result;
49106
+ return severityCounts.critical + severityCounts.high + severityCounts.medium + severityCounts.low + severityCounts.info;
49107
+ }
49108
+ function showHistoryList() {
49109
+ const entries = listScanHistory();
49110
+ if (entries.length === 0) {
49111
+ console.log(source_default.dim("\nNo scan history found."));
49112
+ console.log(source_default.dim("Run a scan with: oculum scan .\n"));
49113
+ return;
49114
+ }
49115
+ console.log(source_default.bold("\nRecent Scans"));
49116
+ console.log(source_default.dim("\u2500".repeat(70)));
49117
+ console.log();
49118
+ console.log(
49119
+ source_default.dim(" ID ") + source_default.dim("Date ") + source_default.dim("Path ") + source_default.dim("Issues ") + source_default.dim("Status")
49120
+ );
49121
+ console.log(source_default.dim(" " + "\u2500".repeat(66)));
49122
+ for (const entry of entries) {
49123
+ const id = entry.id.slice(0, 8);
49124
+ const date = formatDate3(entry.createdAt);
49125
+ const path2 = truncate(entry.targetPath, 22);
49126
+ const issues = getTotalIssues(entry);
49127
+ const status2 = getStatusBadge(entry);
49128
+ console.log(
49129
+ ` ${source_default.cyan(id)} ${source_default.white(date.padEnd(16))} ${source_default.dim(path2.padEnd(22))} ${String(issues).padEnd(6)} ` + status2
49130
+ );
49131
+ }
49132
+ console.log();
49133
+ console.log(source_default.dim("\u2500".repeat(70)));
49134
+ console.log(source_default.dim("Run 'oculum history show <id>' for details"));
49135
+ console.log(source_default.dim("Run 'oculum history clear' to clear all history\n"));
49136
+ }
49137
+ function showScanDetails(id) {
49138
+ const entry = getScanHistoryEntry(id);
49139
+ if (!entry) {
49140
+ const entries = listScanHistory();
49141
+ const partial = entries.find((e2) => e2.id.startsWith(id));
49142
+ if (partial) {
49143
+ showScanDetailsForEntry(partial);
49144
+ return;
49145
+ }
49146
+ console.log(source_default.red(`
49147
+ Scan not found: ${id}`));
49148
+ console.log(source_default.dim("Run 'oculum history' to see available scans\n"));
49149
+ return;
49150
+ }
49151
+ showScanDetailsForEntry(entry);
49152
+ }
49153
+ function showScanDetailsForEntry(entry) {
49154
+ const { result, options, targetPath, createdAt, id } = entry;
49155
+ const { severityCounts, vulnerabilities, filesScanned, scanDuration } = result;
49156
+ console.log(source_default.bold("\nScan Details"));
49157
+ console.log(source_default.dim("\u2500".repeat(50)));
49158
+ console.log();
49159
+ console.log(source_default.white(" ID: ") + source_default.cyan(id.slice(0, 8)));
49160
+ console.log(source_default.white(" Date: ") + formatDate3(createdAt));
49161
+ console.log(source_default.white(" Path: ") + targetPath);
49162
+ console.log(source_default.white(" Depth: ") + options.depth);
49163
+ console.log(source_default.white(" Files: ") + filesScanned);
49164
+ console.log(source_default.white(" Duration: ") + `${(scanDuration / 1e3).toFixed(1)}s`);
49165
+ console.log();
49166
+ console.log(source_default.bold(" Findings"));
49167
+ console.log(source_default.dim(" " + "\u2500".repeat(30)));
49168
+ const severities = [
49169
+ { name: "Critical", count: severityCounts.critical, color: source_default.red },
49170
+ { name: "High", count: severityCounts.high, color: source_default.yellow },
49171
+ { name: "Medium", count: severityCounts.medium, color: source_default.magenta },
49172
+ { name: "Low", count: severityCounts.low, color: source_default.blue },
49173
+ { name: "Info", count: severityCounts.info, color: source_default.gray }
49174
+ ];
49175
+ for (const { name, count, color } of severities) {
49176
+ if (count > 0) {
49177
+ console.log(` ${color(name.padEnd(10))} ${count}`);
49178
+ }
49179
+ }
49180
+ const total = getTotalIssues(entry);
49181
+ if (total === 0) {
49182
+ console.log(source_default.green(" No issues found"));
49183
+ }
49184
+ if (vulnerabilities.length > 0) {
49185
+ console.log();
49186
+ console.log(source_default.bold(" Top Findings"));
49187
+ console.log(source_default.dim(" " + "\u2500".repeat(30)));
49188
+ const topFindings = vulnerabilities.slice(0, 5);
49189
+ for (const finding of topFindings) {
49190
+ const severityColor = finding.severity === "critical" ? source_default.red : finding.severity === "high" ? source_default.yellow : finding.severity === "medium" ? source_default.magenta : finding.severity === "low" ? source_default.blue : source_default.gray;
49191
+ console.log(
49192
+ ` ${severityColor(finding.severity.toUpperCase().padEnd(8))} ` + source_default.white(truncate(finding.title, 40))
49193
+ );
49194
+ console.log(source_default.dim(` ${finding.filePath}:${finding.lineNumber}`));
49195
+ }
49196
+ if (vulnerabilities.length > 5) {
49197
+ console.log(source_default.dim(` ... and ${vulnerabilities.length - 5} more`));
49198
+ }
49199
+ }
49200
+ console.log();
49201
+ console.log(source_default.dim("\u2500".repeat(50)));
49202
+ console.log(source_default.dim(`Run 'oculum history delete ${id.slice(0, 8)}' to remove this scan
49203
+ `));
49204
+ }
49205
+ function clearHistory() {
49206
+ const entries = listScanHistory();
49207
+ if (entries.length === 0) {
49208
+ console.log(source_default.dim("\nNo history to clear.\n"));
49209
+ return;
49210
+ }
49211
+ clearScanHistory();
49212
+ console.log(source_default.green(`
49213
+ Cleared ${entries.length} scan entries.
49214
+ `));
49215
+ }
49216
+ function deleteEntry(id) {
49217
+ const entry = getScanHistoryEntry(id);
49218
+ if (!entry) {
49219
+ const entries = listScanHistory();
49220
+ const partial = entries.find((e2) => e2.id.startsWith(id));
49221
+ if (partial) {
49222
+ deleteScanHistoryEntry(partial.id);
49223
+ console.log(source_default.green(`
49224
+ Deleted scan ${partial.id.slice(0, 8)}
49225
+ `));
49226
+ return;
49227
+ }
49228
+ console.log(source_default.red(`
49229
+ Scan not found: ${id}
49230
+ `));
49231
+ return;
49232
+ }
49233
+ deleteScanHistoryEntry(entry.id);
49234
+ console.log(source_default.green(`
49235
+ Deleted scan ${entry.id.slice(0, 8)}
49236
+ `));
49237
+ }
49238
+ var historyCommand = new Command("history").description("View and manage scan history").argument("[subcommand]", "show, clear, or delete").argument("[id]", "scan ID for show/delete").action((subcommand, id) => {
49239
+ if (!subcommand) {
49240
+ showHistoryList();
49241
+ return;
49242
+ }
49243
+ switch (subcommand) {
49244
+ case "show":
49245
+ if (!id) {
49246
+ console.log(source_default.red("\nPlease provide a scan ID"));
49247
+ console.log(source_default.dim("Usage: oculum history show <id>\n"));
49248
+ return;
49249
+ }
49250
+ showScanDetails(id);
49251
+ break;
49252
+ case "clear":
49253
+ clearHistory();
49254
+ break;
49255
+ case "delete":
49256
+ if (!id) {
49257
+ console.log(source_default.red("\nPlease provide a scan ID"));
49258
+ console.log(source_default.dim("Usage: oculum history delete <id>\n"));
49259
+ return;
49260
+ }
49261
+ deleteEntry(id);
49262
+ break;
49263
+ default:
49264
+ showScanDetails(subcommand);
49265
+ }
49266
+ }).addHelpText("after", `
49267
+ Examples:
49268
+ $ oculum history List recent scans
49269
+ $ oculum history show abc123 View scan details
49270
+ $ oculum history abc123 Quick access (same as show)
49271
+ $ oculum history delete abc Delete a scan by ID
49272
+ $ oculum history clear Clear all history
49273
+
49274
+ Note: History stores up to 25 recent scans locally.
49275
+ `);
49276
+
48952
49277
  // src/utils/ci-detect.ts
48953
49278
  var CI_ENV_VARS = [
48954
49279
  "CI",
@@ -49019,6 +49344,7 @@ Quick Start:
49019
49344
  Common Commands:
49020
49345
  scan [path] Scan files for security vulnerabilities
49021
49346
  watch [path] Watch files and scan on changes
49347
+ history View and manage scan history
49022
49348
  ui Interactive terminal UI
49023
49349
  login Authenticate with Oculum
49024
49350
  status Check authentication status
@@ -49041,6 +49367,7 @@ program2.addCommand(usageCommand);
49041
49367
  program2.addCommand(watchCommand);
49042
49368
  program2.addCommand(uiCommand);
49043
49369
  program2.addCommand(helpCommand);
49370
+ program2.addCommand(historyCommand);
49044
49371
  async function main2() {
49045
49372
  const interactive = isInteractiveTerminal();
49046
49373
  if (interactive && shouldRunUI()) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oculum/cli",
3
- "version": "1.0.10",
3
+ "version": "1.0.11",
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": {