securevibe 0.1.13 → 0.1.15

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.
package/README.md CHANGED
@@ -17,8 +17,9 @@ securevibe fix <path> --apply # apply + back up originals (asks per change)
17
17
  From a clone of this repo instead: `pnpm install && pnpm build`, then
18
18
  `node packages/cli/dist/index.js scan <path>` (`pnpm demo` scans the bundled sample).
19
19
 
20
- The free beta includes a daily usage limit (50 analyses per day, reset at local
21
- midnight). The `--staged` commit guard is never limited. See [`LICENSE`](LICENSE).
20
+ The free beta includes a daily usage limit, reset at local midnight (the current count and
21
+ limit are shown after each run; the limit itself is remotely adjustable and not a fixed
22
+ number, see below). The `--staged` commit guard is never limited. See [`LICENSE`](LICENSE).
22
23
 
23
24
  ## Commands
24
25
 
@@ -31,6 +32,7 @@ midnight). The `--staged` commit guard is never limited. See [`LICENSE`](LICENSE
31
32
  | `securevibe pr-comment [path]` | Post/update a PR findings summary (used by the GitHub Actions workflow) |
32
33
  | `securevibe deps [path]` | Audit dependencies for known CVEs against the local OSV database (SCA) |
33
34
  | `securevibe db <update\|status>` | Manage the local OSV database — `update` is networked |
35
+ | `securevibe update` | Update securevibe itself to the latest published version (also networked) |
34
36
  | `securevibe ready [path]` | Launch readiness scorecard: pass/fail gates + go/no-go verdict |
35
37
  | `securevibe ai-audit [path]` | Focus on the AI-agent attack surface (doc 04) |
36
38
  | `securevibe protect [path]` | Remediation-first view: the fix for every finding (doc 05) |
@@ -71,7 +73,8 @@ is sent to your provider. Needs the same key as `fix` and `explain`
71
73
  - a **GitHub Actions workflow** (`.github/workflows/securevibe.yml`) that scans every PR and uploads
72
74
  SARIF to GitHub code scanning, gates the build on deployment-readiness, and posts a PR comment
73
75
  summarizing findings in the *changed* files only (updated in place on new commits, not reposted —
74
- and the only other command besides `db update` that talks to the network, and only to post a
76
+ and one of the few other commands (besides `db update` and `update`) that talks to the network,
77
+ and only to post a
75
78
  findings summary already computed locally, never your source code);
76
79
  - gitignores `.env` / backups and drops a minimal `securevibe.config.json`.
77
80
 
@@ -142,9 +145,14 @@ against a local copy of the [OSV](https://osv.dev) advisory database, and report
142
145
  CVE still blocks deployment, and `securevibe fix` bumps the package to the lowest non vulnerable
143
146
  version while preserving your range prefix (`^`, `~`, `==`, ...).
144
147
  - **Offline first.** A bundled seed (`data/osv-seed.json`) ships with the CLI, so `deps` works with
145
- no network. `db update` is the only command that **downloads** anything (the public OSV npm and
146
- PyPI dumps, into `~/.securevibe/db`). `pr-comment` is the only command that **uploads** anything,
147
- and only a findings summary already computed locally — never your code or package list.
148
+ no network. `db update` and `update` are the only commands that **download** anything (OSV
149
+ advisory dumps, or the CLI's own next version, respectively). `pr-comment` is the only command
150
+ that **uploads** anything, and only a findings summary already computed locally — never your
151
+ code or package list. Most other commands also make one small background check: a newer
152
+ version notice (the npm registry, cached a day, exempt under `--json`/`--sarif`/`--staged`)
153
+ and the current daily usage limit (a public read only endpoint, cached an hour, exempt under
154
+ `--staged`/`--diff`). Both are tiny GET requests for a version string or a number, and never
155
+ send your code, your findings, or anything else about your project.
148
156
  - **Honest framing.** A clean result reads "no known advisories as of `<db date>`", not "secure" —
149
157
  it means nothing matched the local database on that date, which is not a proof of safety.
150
158
 
package/dist/index.js CHANGED
@@ -38,7 +38,7 @@ async function meter(opts = {}) {
38
38
  const { recordUse, limitMessage } = await import("./usage.js");
39
39
  const r = await recordUse();
40
40
  if (!r.allowed) {
41
- process.stderr.write(limitMessage() + "\n");
41
+ process.stderr.write(limitMessage(r.limit) + "\n");
42
42
  process.exit(1);
43
43
  }
44
44
  return r;
@@ -353,6 +353,18 @@ program
353
353
  process.stderr.write(` unknown db action: ${action} (use: update | status)\n`);
354
354
  process.exit(64);
355
355
  });
356
+ program
357
+ .command("update")
358
+ .description("Update securevibe to the latest published version (npm global installs only)")
359
+ .option("--no-color", "disable coloured output")
360
+ .action(async (opts) => {
361
+ if (opts.color === false)
362
+ process.env.NO_COLOR = "1";
363
+ const { runUpdateCommand } = await import("./update-command.js");
364
+ const code = await runUpdateCommand();
365
+ if (code !== 0)
366
+ process.exit(code);
367
+ });
356
368
  program
357
369
  .command("config")
358
370
  .description("Manage local AI-fixer provider keys (Groq/Anthropic), stored in ~/.securevibe/config.json")
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Remote, admin editable override for the daily usage limit. Same
3
+ * fetch/cache/TTL/fail-open shape as update-check.ts, checking a small
4
+ * public endpoint instead of the npm registry. Never throws — offline, a
5
+ * down endpoint, an invalid value, or a corrupt cache all resolve to the
6
+ * last known good cached value, or ultimately to the caller supplied
7
+ * fallback (usage.ts's DAILY_LIMIT).
8
+ */
9
+ import { promises as fs } from "node:fs";
10
+ import os from "node:os";
11
+ import path from "node:path";
12
+ const LIMITS_URL = "https://limits-config.vercel.app/api/limits";
13
+ const CACHE_TTL_MS = 60 * 60 * 1000;
14
+ const FETCH_TIMEOUT_MS = 1500;
15
+ export function cacheFilePath() {
16
+ return path.join(os.homedir(), ".securevibe", "remote-limit.json");
17
+ }
18
+ /** A remote value is only trusted if it's "unlimited" or a finite positive number. */
19
+ function isValidLimit(value) {
20
+ return value === "unlimited" || (typeof value === "number" && Number.isFinite(value) && value > 0);
21
+ }
22
+ async function readCache(file) {
23
+ try {
24
+ const raw = JSON.parse(await fs.readFile(file, "utf8"));
25
+ if (typeof raw?.checkedAt === "number" && isValidLimit(raw?.dailyLimit))
26
+ return raw;
27
+ }
28
+ catch {
29
+ /* missing or corrupt → treated as no cache */
30
+ }
31
+ return null;
32
+ }
33
+ async function writeCache(file, state) {
34
+ try {
35
+ await fs.mkdir(path.dirname(file), { recursive: true });
36
+ await fs.writeFile(file, JSON.stringify(state) + "\n", "utf8");
37
+ }
38
+ catch {
39
+ /* best effort — a failed write must never break the command */
40
+ }
41
+ }
42
+ export async function fetchRemoteLimit(fetchImpl) {
43
+ const controller = new AbortController();
44
+ const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
45
+ try {
46
+ const res = await fetchImpl(LIMITS_URL, { signal: controller.signal });
47
+ if (!res.ok)
48
+ return null;
49
+ const json = await res.json();
50
+ return isValidLimit(json?.dailyLimit) ? json.dailyLimit : null;
51
+ }
52
+ catch {
53
+ return null;
54
+ }
55
+ finally {
56
+ clearTimeout(timer);
57
+ }
58
+ }
59
+ /**
60
+ * Returns the effective daily limit: a fresh or cached remote override when
61
+ * one is known and valid, else the caller supplied fallback. Never throws.
62
+ */
63
+ export async function resolveDailyLimit(opts) {
64
+ const file = opts.file ?? cacheFilePath();
65
+ const now = opts.now ?? Date.now();
66
+ const fetchImpl = opts.fetchImpl ?? fetch;
67
+ const fallback = opts.fallback;
68
+ try {
69
+ const cached = await readCache(file);
70
+ if (cached && now - cached.checkedAt < CACHE_TTL_MS) {
71
+ return cached.dailyLimit;
72
+ }
73
+ const fetched = await fetchRemoteLimit(fetchImpl);
74
+ if (fetched !== null) {
75
+ await writeCache(file, { checkedAt: now, dailyLimit: fetched });
76
+ return fetched;
77
+ }
78
+ return cached ? cached.dailyLimit : fallback;
79
+ }
80
+ catch {
81
+ return fallback;
82
+ }
83
+ }
package/dist/repl.js CHANGED
@@ -35,7 +35,7 @@ async function meterOrPrint(io) {
35
35
  const { recordUse, limitMessage } = await import("./usage.js");
36
36
  const r = await recordUse();
37
37
  if (!r.allowed) {
38
- write(io, limitMessage() + "\n");
38
+ write(io, limitMessage(r.limit) + "\n");
39
39
  return undefined;
40
40
  }
41
41
  return r;
package/dist/ui/usage.js CHANGED
@@ -7,6 +7,6 @@ function bar(fraction, width = 20) {
7
7
  return color("█".repeat(filled)) + pc.dim("░".repeat(width - filled));
8
8
  }
9
9
  export function renderUsageLine(usage) {
10
- const fraction = usage.limit > 0 ? usage.used / usage.limit : 0;
10
+ const fraction = usage.limit === "unlimited" || usage.limit <= 0 ? 0 : usage.used / usage.limit;
11
11
  return pc.dim(" Free beta usage today ") + bar(fraction) + pc.dim(` ${usage.used}/${usage.limit}`);
12
12
  }
@@ -28,6 +28,16 @@ export function isNewer(current, latest) {
28
28
  }
29
29
  return false;
30
30
  }
31
+ /**
32
+ * True when the running script is npm's npx cache copy rather than a
33
+ * persistent global install — npx always resolves to the latest published
34
+ * version on every invocation, so there is nothing for `securevibe update`
35
+ * to do in that case. The npx cache directory is named `_npx` on both Unix
36
+ * (~/.npm/_npx/...) and Windows (%LocalAppData%\npm-cache\_npx\...).
37
+ */
38
+ export function isRunningViaNpx(scriptPath = process.argv[1] ?? "") {
39
+ return scriptPath.split(/[\\/]/).includes("_npx");
40
+ }
31
41
  async function readCache(file) {
32
42
  try {
33
43
  const raw = JSON.parse(await fs.readFile(file, "utf8"));
@@ -48,7 +58,7 @@ async function writeCache(file, state) {
48
58
  /* best effort — a failed write must never break the command */
49
59
  }
50
60
  }
51
- async function fetchLatest(fetchImpl) {
61
+ export async function fetchLatest(fetchImpl) {
52
62
  const controller = new AbortController();
53
63
  const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
54
64
  try {
@@ -96,6 +106,6 @@ export async function checkForUpdate(currentVersion, opts = {}) {
96
106
  export function updateNotice(currentVersion, latestVersion) {
97
107
  return [
98
108
  ` A new version of securevibe is available: ${latestVersion} (you have ${currentVersion}).`,
99
- " Update: npm install -g securevibe",
109
+ " Update: securevibe update (or: npm install -g securevibe)",
100
110
  ].join("\n");
101
111
  }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * `securevibe update` — an explicit, user-triggered self-update for npm
3
+ * global installs. Reuses the same registry check as the passive daily
4
+ * notice (update-check.ts) but always fetches fresh: "check right now" is
5
+ * the whole point of running this command instead of waiting for tomorrow's
6
+ * cached check.
7
+ */
8
+ import { isNewer, isRunningViaNpx, fetchLatest as defaultFetchLatest } from "./update-check.js";
9
+ import { VERSION } from "./version.js";
10
+ async function defaultSpawnNpmInstall(write) {
11
+ const { spawn } = await import("node:child_process");
12
+ return new Promise((resolve) => {
13
+ const child = spawn("npm", ["install", "-g", "securevibe@latest"], {
14
+ stdio: ["ignore", "pipe", "pipe"],
15
+ shell: process.platform === "win32",
16
+ });
17
+ let output = "";
18
+ const onChunk = (chunk) => {
19
+ const text = chunk.toString("utf8");
20
+ output += text;
21
+ write(text);
22
+ };
23
+ child.stdout?.on("data", onChunk);
24
+ child.stderr?.on("data", onChunk);
25
+ child.on("error", (err) => resolve({ code: 1, output: output + String(err?.message ?? err) }));
26
+ child.on("close", (code) => resolve({ code, output }));
27
+ });
28
+ }
29
+ export function defaultUpdateCommandDeps() {
30
+ return {
31
+ currentVersion: VERSION,
32
+ platform: process.platform,
33
+ isNpx: () => isRunningViaNpx(),
34
+ fetchLatest: defaultFetchLatest,
35
+ fetchImpl: fetch,
36
+ spawnNpmInstall: defaultSpawnNpmInstall,
37
+ write: (text) => process.stderr.write(text),
38
+ };
39
+ }
40
+ /**
41
+ * True when npm's own output looks like a permission error and a sudo
42
+ * re-run would plausibly help. Never true on Windows, where elevation isn't
43
+ * a one-line fix.
44
+ */
45
+ export function shouldShowSudoHint(output, platform = process.platform) {
46
+ return platform !== "win32" && /eacces|permission denied/i.test(output);
47
+ }
48
+ /**
49
+ * Runs the `securevibe update` flow. Returns the process exit code (0
50
+ * success, 1 failure) rather than calling process.exit itself, so callers
51
+ * (and tests) control process lifecycle. Never throws.
52
+ */
53
+ export async function runUpdateCommand(deps = {}) {
54
+ const d = { ...defaultUpdateCommandDeps(), ...deps };
55
+ if (d.isNpx()) {
56
+ d.write(" You're running securevibe via npx, which always uses the latest published version — nothing to update.\n");
57
+ return 0;
58
+ }
59
+ const latest = await d.fetchLatest(d.fetchImpl);
60
+ if (latest === null) {
61
+ d.write(" Could not reach the npm registry to check for updates. Check your network and try again.\n");
62
+ return 1;
63
+ }
64
+ if (!isNewer(d.currentVersion, latest)) {
65
+ d.write(` Already on the latest version (${d.currentVersion}).\n`);
66
+ return 0;
67
+ }
68
+ d.write(` Updating securevibe ${d.currentVersion} -> ${latest}...\n`);
69
+ const result = await d.spawnNpmInstall(d.write);
70
+ if (result.code === 0) {
71
+ d.write(` Updated to ${latest}.\n`);
72
+ return 0;
73
+ }
74
+ d.write(" Update failed.\n");
75
+ if (shouldShowSudoHint(result.output, d.platform)) {
76
+ d.write(" Try: sudo npm install -g securevibe@latest\n");
77
+ }
78
+ return 1;
79
+ }
package/dist/usage.js CHANGED
@@ -4,10 +4,14 @@
4
4
  * design — there is no account system yet — so this establishes the metering
5
5
  * UX honestly without pretending to be tamper-proof. The --staged commit guard
6
6
  * is never metered: a quota must not block someone's commit.
7
+ *
8
+ * The enforced limit is resolved through remote-limit.ts on every call: an
9
+ * admin editable value if one is known and valid, else this file's DAILY_LIMIT.
7
10
  */
8
11
  import { promises as fs } from "node:fs";
9
12
  import os from "node:os";
10
13
  import path from "node:path";
14
+ import { resolveDailyLimit } from "./remote-limit.js";
11
15
  export const DAILY_LIMIT = 200;
12
16
  export function usageFilePath() {
13
17
  return path.join(os.homedir(), ".securevibe", "usage.json");
@@ -19,10 +23,10 @@ export function localDay(now = new Date()) {
19
23
  const d = String(now.getDate()).padStart(2, "0");
20
24
  return `${y}-${m}-${d}`;
21
25
  }
22
- /** Pure state transition: same day increments, a new day resets, limit blocks. */
26
+ /** Pure state transition: same day increments, a new day resets, limit blocks — "unlimited" never blocks. */
23
27
  export function nextUsage(state, today, limit = DAILY_LIMIT) {
24
28
  const base = state && state.date === today ? state : { date: today, count: 0 };
25
- if (base.count >= limit) {
29
+ if (limit !== "unlimited" && base.count >= limit) {
26
30
  return { state: base, result: { allowed: false, used: base.count, limit } };
27
31
  }
28
32
  const next = { date: today, count: base.count + 1 };
@@ -42,12 +46,13 @@ async function readState(file) {
42
46
  }
43
47
  /**
44
48
  * Record one use against today's quota. Returns whether the command may run.
45
- * Metering failures (unwritable home, etc.) never break the tool: on any IO
46
- * error the use is allowed.
49
+ * Metering failures (unwritable home, unreachable remote limit, etc.) never
50
+ * break the tool: on any IO error the use is allowed.
47
51
  */
48
- export async function recordUse(file = usageFilePath(), now = new Date()) {
52
+ export async function recordUse(file = usageFilePath(), now = new Date(), resolveLimit = () => resolveDailyLimit({ fallback: DAILY_LIMIT })) {
49
53
  try {
50
- const { state, result } = nextUsage(await readState(file), localDay(now));
54
+ const limit = await resolveLimit();
55
+ const { state, result } = nextUsage(await readState(file), localDay(now), limit);
51
56
  if (result.allowed) {
52
57
  await fs.mkdir(path.dirname(file), { recursive: true });
53
58
  await fs.writeFile(file, JSON.stringify(state) + "\n", "utf8");
package/dist/version.js CHANGED
@@ -2,4 +2,4 @@
2
2
  // Single source of truth for the CLI's own version, so it can't drift from
3
3
  // what commander reports and what the update-check compares against.
4
4
  // Kept in sync with the "version" field in package.json by hand at release time.
5
- export const VERSION = "0.1.13";
5
+ export const VERSION = "0.1.15";
package/package.json CHANGED
@@ -1,71 +1,71 @@
1
- {
2
- "name": "securevibe",
3
- "version": "0.1.13",
4
- "description": "Autonomous AI security engineer for AI generated apps: scan, fix, verify, and gate every commit before you launch",
5
- "type": "module",
6
- "license": "SEE LICENSE IN LICENSE",
7
- "author": "Benedict Patrick <benedictroger2006@gmail.com>",
8
- "homepage": "https://github.com/Benedictpatrick/secure-vibe#readme",
9
- "repository": {
10
- "type": "git",
11
- "url": "git+https://github.com/Benedictpatrick/secure-vibe.git",
12
- "directory": "packages/cli"
13
- },
14
- "bugs": {
15
- "url": "https://github.com/Benedictpatrick/secure-vibe/issues"
16
- },
17
- "keywords": [
18
- "security",
19
- "scanner",
20
- "sast",
21
- "ai security",
22
- "prompt injection",
23
- "vulnerability",
24
- "secrets",
25
- "sca",
26
- "cve",
27
- "launch readiness",
28
- "vibe coding",
29
- "cli"
30
- ],
31
- "engines": {
32
- "node": ">=20"
33
- },
34
- "bin": {
35
- "securevibe": "./dist/index.js"
36
- },
37
- "files": [
38
- "dist",
39
- "data",
40
- "LICENSE"
41
- ],
42
- "scripts": {
43
- "build": "tsc -p tsconfig.json",
44
- "dev": "tsx src/index.ts",
45
- "start": "node dist/index.js",
46
- "test": "node run-tests.mjs"
47
- },
48
- "dependencies": {
49
- "commander": "^12.1.0",
50
- "diff": "^5.2.0",
51
- "ink": "^6.8.0",
52
- "ink-text-input": "^6.0.0",
53
- "picocolors": "^1.1.1",
54
- "react": "^19.2.7",
55
- "tree-sitter-wasms": "^0.1.12",
56
- "web-tree-sitter": "^0.25.10",
57
- "yauzl": "^3.2.0"
58
- },
59
- "optionalDependencies": {
60
- "@anthropic-ai/sdk": "^0.39.0"
61
- },
62
- "devDependencies": {
63
- "@types/diff": "^5.2.0",
64
- "@types/node": "^20.14.0",
65
- "@types/react": "^19.2.17",
66
- "@types/yauzl": "^2.10.3",
67
- "ink-testing-library": "^4.0.0",
68
- "tsx": "^4.19.0",
69
- "typescript": "^5.6.0"
70
- }
71
- }
1
+ {
2
+ "name": "securevibe",
3
+ "version": "0.1.15",
4
+ "description": "Autonomous AI security engineer for AI generated apps: scan, fix, verify, and gate every commit before you launch",
5
+ "type": "module",
6
+ "license": "SEE LICENSE IN LICENSE",
7
+ "author": "Benedict Patrick <benedictroger2006@gmail.com>",
8
+ "homepage": "https://github.com/Benedictpatrick/secure-vibe#readme",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/Benedictpatrick/secure-vibe.git",
12
+ "directory": "packages/cli"
13
+ },
14
+ "bugs": {
15
+ "url": "https://github.com/Benedictpatrick/secure-vibe/issues"
16
+ },
17
+ "keywords": [
18
+ "security",
19
+ "scanner",
20
+ "sast",
21
+ "ai security",
22
+ "prompt injection",
23
+ "vulnerability",
24
+ "secrets",
25
+ "sca",
26
+ "cve",
27
+ "launch readiness",
28
+ "vibe coding",
29
+ "cli"
30
+ ],
31
+ "engines": {
32
+ "node": ">=20"
33
+ },
34
+ "bin": {
35
+ "securevibe": "./dist/index.js"
36
+ },
37
+ "files": [
38
+ "dist",
39
+ "data",
40
+ "LICENSE"
41
+ ],
42
+ "scripts": {
43
+ "build": "tsc -p tsconfig.json",
44
+ "dev": "tsx src/index.ts",
45
+ "start": "node dist/index.js",
46
+ "test": "node run-tests.mjs"
47
+ },
48
+ "dependencies": {
49
+ "commander": "^12.1.0",
50
+ "diff": "^5.2.0",
51
+ "ink": "^6.8.0",
52
+ "ink-text-input": "^6.0.0",
53
+ "picocolors": "^1.1.1",
54
+ "react": "^19.2.7",
55
+ "tree-sitter-wasms": "^0.1.12",
56
+ "web-tree-sitter": "^0.25.10",
57
+ "yauzl": "^3.2.0"
58
+ },
59
+ "optionalDependencies": {
60
+ "@anthropic-ai/sdk": "^0.39.0"
61
+ },
62
+ "devDependencies": {
63
+ "@types/diff": "^5.2.0",
64
+ "@types/node": "^20.14.0",
65
+ "@types/react": "^19.2.17",
66
+ "@types/yauzl": "^2.10.3",
67
+ "ink-testing-library": "^4.0.0",
68
+ "tsx": "^4.19.0",
69
+ "typescript": "^5.6.0"
70
+ }
71
+ }