@oculum/cli 1.0.5 → 1.0.8

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 +151 -119
  2. package/package.json +4 -4
package/dist/index.js CHANGED
@@ -43131,103 +43131,6 @@ async function callBackendAPI(files, depth, apiKey, repoInfo) {
43131
43131
  }
43132
43132
  return data.result;
43133
43133
  }
43134
- async function callBackendAPIStream(files, depth, apiKey, repoInfo, onProgress) {
43135
- const baseUrl = getApiBaseUrl();
43136
- const streamUrl = `${baseUrl}/v1/scan/stream`;
43137
- try {
43138
- const response = await fetch(streamUrl, {
43139
- method: "POST",
43140
- headers: {
43141
- "Content-Type": "application/json",
43142
- "Authorization": `Bearer ${apiKey}`,
43143
- "X-Oculum-Client": "cli",
43144
- "X-Oculum-Version": "1.0.0"
43145
- },
43146
- body: JSON.stringify({
43147
- files,
43148
- depth,
43149
- repoName: repoInfo.name,
43150
- repoUrl: repoInfo.url,
43151
- branch: repoInfo.branch
43152
- })
43153
- });
43154
- if (!response.ok) {
43155
- console.warn(`[API] SSE endpoint failed with ${response.status}, falling back to regular POST`);
43156
- return await callBackendAPI(files, depth, apiKey, repoInfo);
43157
- }
43158
- if (!response.body) {
43159
- throw new APIError("Response body is empty", 500);
43160
- }
43161
- const reader = response.body.getReader();
43162
- const decoder = new TextDecoder();
43163
- let buffer = "";
43164
- let result = null;
43165
- while (true) {
43166
- const { done, value } = await reader.read();
43167
- if (done) break;
43168
- buffer += decoder.decode(value, { stream: true });
43169
- const lines = buffer.split("\n");
43170
- buffer = lines.pop() || "";
43171
- let currentEvent = null;
43172
- let currentData = null;
43173
- for (const line of lines) {
43174
- if (line.startsWith("event: ")) {
43175
- currentEvent = line.slice(7).trim();
43176
- } else if (line.startsWith("data: ")) {
43177
- currentData = line.slice(6).trim();
43178
- } else if (line === "" && currentEvent && currentData) {
43179
- try {
43180
- const data = JSON.parse(currentData);
43181
- switch (currentEvent) {
43182
- case "progress":
43183
- if (onProgress) {
43184
- onProgress({
43185
- status: data.status,
43186
- message: data.message || "",
43187
- filesProcessed: data.filesProcessed || 0,
43188
- totalFiles: data.totalFiles || 0,
43189
- vulnerabilitiesFound: data.vulnerabilitiesFound || 0
43190
- });
43191
- }
43192
- break;
43193
- case "complete":
43194
- if (!data.result) {
43195
- console.error("[API] Complete event received but no result in data:", data);
43196
- throw new APIError("Backend scan completed but returned no result data", 500);
43197
- }
43198
- result = data.result;
43199
- break;
43200
- case "error":
43201
- console.error("[API] Backend sent error event:", data);
43202
- throw new APIError(
43203
- data.message || data.error || "Scan failed",
43204
- data.status || 500,
43205
- data.error
43206
- );
43207
- }
43208
- } catch (parseError) {
43209
- if (parseError instanceof APIError) throw parseError;
43210
- console.error("[API] Failed to parse SSE event:", parseError);
43211
- }
43212
- currentEvent = null;
43213
- currentData = null;
43214
- }
43215
- }
43216
- }
43217
- if (!result) {
43218
- console.error("[API] Stream ended without receiving complete event");
43219
- throw new APIError("No result received from stream", 500);
43220
- }
43221
- console.log("[API] SSE stream completed successfully");
43222
- return result;
43223
- } catch (error) {
43224
- if (error instanceof APIError) {
43225
- throw error;
43226
- }
43227
- console.warn("[API] SSE streaming failed, falling back to regular POST:", error);
43228
- return await callBackendAPI(files, depth, apiKey, repoInfo);
43229
- }
43230
- }
43231
43134
  async function verifyApiKey(apiKey) {
43232
43135
  const baseUrl = getApiBaseUrl();
43233
43136
  const url = `${baseUrl}/v1/verify-key`;
@@ -44045,8 +43948,8 @@ async function runScanOnce(targetPath, options) {
44045
43948
  spinner.start("Starting scan...");
44046
43949
  const hasLocalAI = !!process.env.ANTHROPIC_API_KEY;
44047
43950
  if (options.depth !== "cheap" && isAuthenticated() && !hasLocalAI) {
44048
- spinner.text = `Backend ${options.depth} scan starting...`;
44049
- result = await callBackendAPIStream(
43951
+ spinner.text = `Backend ${options.depth} scan analyzing ${files.length} files...`;
43952
+ result = await callBackendAPI(
44050
43953
  files,
44051
43954
  options.depth,
44052
43955
  config.apiKey,
@@ -44054,9 +43957,7 @@ async function runScanOnce(targetPath, options) {
44054
43957
  name: (0, import_path3.basename)((0, import_path3.resolve)(targetPath)),
44055
43958
  url: "",
44056
43959
  branch: "local"
44057
- },
44058
- onProgress
44059
- // Stream real-time progress from backend
43960
+ }
44060
43961
  );
44061
43962
  spinner.succeed(`Backend ${options.depth} scan complete`);
44062
43963
  } else {
@@ -47005,8 +46906,7 @@ function recordScan() {
47005
46906
  }
47006
46907
 
47007
46908
  // src/ui/onboarding.ts
47008
- async function showWelcomeScreen() {
47009
- console.clear();
46909
+ function showLogo() {
47010
46910
  console.log(source_default.cyan(`
47011
46911
  \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2557
47012
46912
  \u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2551
@@ -47015,6 +46915,10 @@ async function showWelcomeScreen() {
47015
46915
  \u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u255A\u2550\u255D \u2588\u2588\u2551
47016
46916
  \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D
47017
46917
  `));
46918
+ }
46919
+ async function showWelcomeScreen() {
46920
+ console.clear();
46921
+ showLogo();
47018
46922
  console.log(source_default.bold.white(" AI-Native Security Scanner for Modern Codebases\n"));
47019
46923
  console.log(source_default.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n"));
47020
46924
  console.log(source_default.white(" Oculum detects security vulnerabilities in AI-generated"));
@@ -48071,11 +47975,136 @@ async function runHistoryFlow() {
48071
47975
  }
48072
47976
  }
48073
47977
  }
47978
+ function formatNumber(num) {
47979
+ if (num === -1) return "unlimited";
47980
+ return num.toLocaleString();
47981
+ }
47982
+ function createProgressBar(percentage, width = 20) {
47983
+ const filled = Math.round(percentage / 100 * width);
47984
+ const empty = width - filled;
47985
+ const bar = "\u2588".repeat(filled) + "\u2591".repeat(empty);
47986
+ if (percentage >= 90) return source_default.red(bar);
47987
+ if (percentage >= 70) return source_default.yellow(bar);
47988
+ return source_default.green(bar);
47989
+ }
47990
+ function formatDate(dateStr) {
47991
+ const date = new Date(dateStr);
47992
+ return date.toLocaleDateString("en-US", {
47993
+ month: "short",
47994
+ day: "numeric",
47995
+ year: "numeric"
47996
+ });
47997
+ }
47998
+ function getTimeAgo(date) {
47999
+ const now = /* @__PURE__ */ new Date();
48000
+ const diffMs = now.getTime() - date.getTime();
48001
+ const diffMins = Math.floor(diffMs / 6e4);
48002
+ const diffHours = Math.floor(diffMs / 36e5);
48003
+ const diffDays = Math.floor(diffMs / 864e5);
48004
+ if (diffMins < 1) return "just now";
48005
+ if (diffMins < 60) return `${diffMins}m ago`;
48006
+ if (diffHours < 24) return `${diffHours}h ago`;
48007
+ if (diffDays < 7) return `${diffDays}d ago`;
48008
+ return date.toLocaleDateString("en-US", { month: "short", day: "numeric" });
48009
+ }
48010
+ async function runUsageFlow() {
48011
+ const config = getConfig();
48012
+ if (!isAuthenticated()) {
48013
+ console.log("");
48014
+ M2.warn("Not logged in");
48015
+ console.log("");
48016
+ console.log(source_default.dim(" Login to view your usage and quota:"));
48017
+ console.log(source_default.cyan(" oculum login"));
48018
+ console.log("");
48019
+ await ve({
48020
+ message: "Press Enter to continue",
48021
+ options: [{ value: "back", label: "\u2190 Back to menu" }]
48022
+ });
48023
+ return;
48024
+ }
48025
+ const spinner = Y2();
48026
+ spinner.start("Fetching usage data...");
48027
+ try {
48028
+ const result = await getUsage(config.apiKey);
48029
+ if (!result.success || !result.usage || !result.plan) {
48030
+ spinner.stop("Failed to fetch usage data");
48031
+ console.log("");
48032
+ M2.error(result.error || "Unknown error");
48033
+ console.log("");
48034
+ await ve({
48035
+ message: "Press Enter to continue",
48036
+ options: [{ value: "back", label: "\u2190 Back to menu" }]
48037
+ });
48038
+ return;
48039
+ }
48040
+ spinner.stop("Usage data loaded");
48041
+ const { plan, usage: usageData } = result;
48042
+ console.clear();
48043
+ console.log("");
48044
+ console.log(source_default.bold(" \u{1F4CA} Oculum Usage"));
48045
+ console.log(source_default.dim(" " + "\u2500".repeat(38)));
48046
+ console.log("");
48047
+ const planBadge = plan.name === "pro" ? source_default.bgBlue.white(" PRO ") : plan.name === "enterprise" || plan.name === "max" ? source_default.bgMagenta.white(" MAX ") : plan.name === "starter" ? source_default.bgCyan.white(" STARTER ") : source_default.bgGray.white(" FREE ");
48048
+ console.log(source_default.dim(" Plan: ") + planBadge + source_default.white(` ${plan.displayName}`));
48049
+ console.log(source_default.dim(" Month: ") + source_default.white(usageData.month));
48050
+ console.log("");
48051
+ console.log(source_default.bold(" Credits Usage"));
48052
+ console.log("");
48053
+ const creditsDisplay = usageData.creditsLimit === -1 ? `${formatNumber(usageData.creditsUsed)} / unlimited` : `${formatNumber(usageData.creditsUsed)} / ${formatNumber(usageData.creditsLimit)}`;
48054
+ console.log(source_default.dim(" Used: ") + source_default.white(creditsDisplay));
48055
+ if (usageData.creditsLimit !== -1) {
48056
+ console.log(source_default.dim(" Remaining: ") + source_default.white(formatNumber(usageData.creditsRemaining)));
48057
+ console.log("");
48058
+ console.log(source_default.dim(" ") + createProgressBar(usageData.usagePercentage) + source_default.dim(` ${usageData.usagePercentage.toFixed(1)}%`));
48059
+ }
48060
+ console.log("");
48061
+ console.log(source_default.bold(" This Month"));
48062
+ console.log("");
48063
+ console.log(source_default.dim(" Scans: ") + source_default.white(formatNumber(usageData.totalScans)));
48064
+ console.log(source_default.dim(" Files: ") + source_default.white(formatNumber(usageData.totalFiles)));
48065
+ console.log("");
48066
+ console.log(source_default.dim(" Resets on: ") + source_default.white(formatDate(usageData.resetDate)));
48067
+ console.log("");
48068
+ if (result.recentScans && result.recentScans.length > 0) {
48069
+ console.log(source_default.bold(" Recent Scans"));
48070
+ console.log("");
48071
+ const recentToShow = result.recentScans.slice(0, 5);
48072
+ for (const scan of recentToShow) {
48073
+ const date = new Date(scan.createdAt);
48074
+ const timeAgo = getTimeAgo(date);
48075
+ console.log(source_default.dim(" \u2022 ") + source_default.white(scan.repoName || "unknown") + source_default.dim(` (${timeAgo})`));
48076
+ console.log(source_default.dim(" ") + source_default.dim(`${scan.filesScanned} files, ${scan.findingsCount} findings, ${scan.creditsUsed} credits`));
48077
+ }
48078
+ console.log("");
48079
+ }
48080
+ if (plan.name === "free" || plan.name === "starter") {
48081
+ console.log(source_default.dim(" " + "\u2500".repeat(38)));
48082
+ console.log("");
48083
+ console.log(source_default.dim(" Need more credits? Upgrade your plan"));
48084
+ console.log("");
48085
+ }
48086
+ await ve({
48087
+ message: "Press Enter to continue",
48088
+ options: [{ value: "back", label: "\u2190 Back to menu" }]
48089
+ });
48090
+ } catch (error) {
48091
+ spinner.stop("Failed to fetch usage data");
48092
+ console.log("");
48093
+ M2.error(String(error));
48094
+ console.log("");
48095
+ await ve({
48096
+ message: "Press Enter to continue",
48097
+ options: [{ value: "back", label: "\u2190 Back to menu" }]
48098
+ });
48099
+ }
48100
+ }
48074
48101
  async function runUI() {
48102
+ console.clear();
48103
+ showLogo();
48104
+ console.log();
48075
48105
  const onboardingResult = await handleOnboarding();
48076
48106
  if (onboardingResult.quickStartResult) {
48077
48107
  const { path: path2, depth } = onboardingResult.quickStartResult;
48078
- Ie(source_default.bold("Oculum"));
48079
48108
  try {
48080
48109
  const { output, exitCode, result } = await runScanOnce(path2, {
48081
48110
  depth,
@@ -48103,8 +48132,6 @@ async function runUI() {
48103
48132
  } catch (err) {
48104
48133
  M2.error(`Scan failed: ${String(err)}`);
48105
48134
  }
48106
- } else {
48107
- Ie(source_default.bold("Oculum"));
48108
48135
  }
48109
48136
  let lastScanEntry;
48110
48137
  const userState = getUserState();
@@ -48120,6 +48147,7 @@ async function runUI() {
48120
48147
  { value: "scan", label: "\u{1F50D} Custom Scan", hint: "Configure scan options" },
48121
48148
  { value: "history", label: "\u{1F4DC} Scan History", hint: `${listScanHistory().length} past scans` },
48122
48149
  { value: "auth", label: "\u{1F510} Auth", hint: isAuthenticated() ? `Logged in (${getConfig().tier || "free"})` : "Not logged in" },
48150
+ { value: "usage", label: "\u{1F4CA} Usage", hint: isAuthenticated() ? "View credits and quota" : "Requires login" },
48123
48151
  { value: "help", label: "\u2753 Help", hint: "Commands and tips" },
48124
48152
  { value: "exit", label: "\u{1F44B} Exit" }
48125
48153
  ];
@@ -48201,6 +48229,10 @@ async function runUI() {
48201
48229
  await runHistoryFlow();
48202
48230
  continue;
48203
48231
  }
48232
+ if (action === "usage") {
48233
+ await runUsageFlow();
48234
+ continue;
48235
+ }
48204
48236
  if (action === "help") {
48205
48237
  await showHelpScreen();
48206
48238
  continue;
@@ -48253,11 +48285,11 @@ var uiCommand = new Command("ui").description("Interactive terminal UI").action(
48253
48285
  });
48254
48286
 
48255
48287
  // src/commands/usage.ts
48256
- function formatNumber(num) {
48288
+ function formatNumber2(num) {
48257
48289
  if (num === -1) return "unlimited";
48258
48290
  return num.toLocaleString();
48259
48291
  }
48260
- function createProgressBar(percentage, width = 20) {
48292
+ function createProgressBar2(percentage, width = 20) {
48261
48293
  const filled = Math.round(percentage / 100 * width);
48262
48294
  const empty = width - filled;
48263
48295
  const bar = "\u2588".repeat(filled) + "\u2591".repeat(empty);
@@ -48265,7 +48297,7 @@ function createProgressBar(percentage, width = 20) {
48265
48297
  if (percentage >= 70) return source_default.yellow(bar);
48266
48298
  return source_default.green(bar);
48267
48299
  }
48268
- function formatDate(dateStr) {
48300
+ function formatDate2(dateStr) {
48269
48301
  const date = new Date(dateStr);
48270
48302
  return date.toLocaleDateString("en-US", {
48271
48303
  month: "short",
@@ -48310,20 +48342,20 @@ async function usage(options) {
48310
48342
  console.log("");
48311
48343
  console.log(source_default.bold(" Credits Usage"));
48312
48344
  console.log("");
48313
- const creditsDisplay = usageData.creditsLimit === -1 ? `${formatNumber(usageData.creditsUsed)} / unlimited` : `${formatNumber(usageData.creditsUsed)} / ${formatNumber(usageData.creditsLimit)}`;
48345
+ const creditsDisplay = usageData.creditsLimit === -1 ? `${formatNumber2(usageData.creditsUsed)} / unlimited` : `${formatNumber2(usageData.creditsUsed)} / ${formatNumber2(usageData.creditsLimit)}`;
48314
48346
  console.log(source_default.dim(" Used: ") + source_default.white(creditsDisplay));
48315
48347
  if (usageData.creditsLimit !== -1) {
48316
- console.log(source_default.dim(" Remaining: ") + source_default.white(formatNumber(usageData.creditsRemaining)));
48348
+ console.log(source_default.dim(" Remaining: ") + source_default.white(formatNumber2(usageData.creditsRemaining)));
48317
48349
  console.log("");
48318
- console.log(source_default.dim(" ") + createProgressBar(usageData.usagePercentage) + source_default.dim(` ${usageData.usagePercentage.toFixed(1)}%`));
48350
+ console.log(source_default.dim(" ") + createProgressBar2(usageData.usagePercentage) + source_default.dim(` ${usageData.usagePercentage.toFixed(1)}%`));
48319
48351
  }
48320
48352
  console.log("");
48321
48353
  console.log(source_default.bold(" This Month"));
48322
48354
  console.log("");
48323
- console.log(source_default.dim(" Scans: ") + source_default.white(formatNumber(usageData.totalScans)));
48324
- console.log(source_default.dim(" Files: ") + source_default.white(formatNumber(usageData.totalFiles)));
48355
+ console.log(source_default.dim(" Scans: ") + source_default.white(formatNumber2(usageData.totalScans)));
48356
+ console.log(source_default.dim(" Files: ") + source_default.white(formatNumber2(usageData.totalFiles)));
48325
48357
  console.log("");
48326
- console.log(source_default.dim(" Resets on: ") + source_default.white(formatDate(usageData.resetDate)));
48358
+ console.log(source_default.dim(" Resets on: ") + source_default.white(formatDate2(usageData.resetDate)));
48327
48359
  console.log("");
48328
48360
  if (result.recentScans && result.recentScans.length > 0) {
48329
48361
  console.log(source_default.bold(" Recent Scans"));
@@ -48331,7 +48363,7 @@ async function usage(options) {
48331
48363
  const recentToShow = result.recentScans.slice(0, 5);
48332
48364
  for (const scan of recentToShow) {
48333
48365
  const date = new Date(scan.createdAt);
48334
- const timeAgo = getTimeAgo(date);
48366
+ const timeAgo = getTimeAgo2(date);
48335
48367
  console.log(source_default.dim(" \u2022 ") + source_default.white(scan.repoName || "unknown") + source_default.dim(` (${timeAgo})`));
48336
48368
  console.log(source_default.dim(" ") + source_default.dim(`${scan.filesScanned} files, ${scan.findingsCount} findings, ${scan.creditsUsed} credits`));
48337
48369
  }
@@ -48351,7 +48383,7 @@ async function usage(options) {
48351
48383
  process.exit(1);
48352
48384
  }
48353
48385
  }
48354
- function getTimeAgo(date) {
48386
+ function getTimeAgo2(date) {
48355
48387
  const now = /* @__PURE__ */ new Date();
48356
48388
  const diffMs = now.getTime() - date.getTime();
48357
48389
  const diffMins = Math.floor(diffMs / 6e4);
@@ -48367,7 +48399,7 @@ var usageCommand = new Command("usage").description("Show current usage and quot
48367
48399
 
48368
48400
  // src/index.ts
48369
48401
  var program2 = new Command();
48370
- program2.name("oculum").description("AI-native security scanner for detecting vulnerabilities in LLM-generated code").version("1.0.5").addHelpText("after", `
48402
+ program2.name("oculum").description("AI-native security scanner for detecting vulnerabilities in LLM-generated code").version("1.0.7").addHelpText("after", `
48371
48403
  Quick Start:
48372
48404
  $ oculum scan . Scan current directory (free)
48373
48405
  $ oculum ui Interactive mode with guided setup
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oculum/cli",
3
- "version": "1.0.5",
3
+ "version": "1.0.8",
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": {
@@ -18,14 +18,14 @@
18
18
  "url": "https://github.com/flexipie/oculum/issues"
19
19
  },
20
20
  "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.5\"'",
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
22
  "dev": "npm run build -- --watch",
23
23
  "test": "echo \"No tests configured yet\"",
24
24
  "lint": "eslint src/"
25
25
  },
26
26
  "dependencies": {
27
- "@oculum/scanner": "^1.0.2",
28
- "@oculum/shared": "^1.0.0",
27
+ "@oculum/scanner": "^1.0.3",
28
+ "@oculum/shared": "^1.0.2",
29
29
  "commander": "^12.1.0",
30
30
  "chalk": "^5.3.0",
31
31
  "ora": "^8.1.1",