securevibe 0.1.7 → 0.1.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.
package/README.md CHANGED
@@ -34,6 +34,8 @@ midnight). The `--staged` commit guard is never limited. See [`LICENSE`](LICENSE
34
34
  | `securevibe protect [path]` | Remediation-first view: the fix for every finding (doc 05) |
35
35
  | `securevibe attack-map [path]` | Exploit paths from reachable findings + choke point (doc 06) |
36
36
  | `securevibe score [path]` | Just the score + deployment-readiness verdict (doc 08) |
37
+ | `securevibe config <show\|set-key\|unset-key>` | Manage local Groq/Anthropic keys for the AI-powered fixer |
38
+ | `securevibe repl [path]` | Interactive session — run `scan`/`fix`/etc without re-invoking the CLI each time |
37
39
 
38
40
  Flags: `--json`, `--sarif` (GitHub code-scanning / CI interop), `--no-color`,
39
41
  `--ci` (exit `2` on BLOCK, `1` on WARN, `0` on SHIP — for build gates),
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Shared helpers used by both the one-shot CLI commands (index.ts) and the
3
+ * interactive REPL (repl.ts), so the two entry points never duplicate logic.
4
+ */
5
+ /**
6
+ * If `fix` is about to run with no AI-fixer provider key available (and we're
7
+ * in an interactive terminal), offer to set one up on the spot — a free Groq
8
+ * key takes under a minute. Never prompts under --json/non-TTY/--no-llm, and
9
+ * never touches process.env if the user declines or a key is already set.
10
+ */
11
+ export async function maybeOfferAiFixerSetup() {
12
+ if (process.env.GROQ_API_KEY || process.env.ANTHROPIC_API_KEY)
13
+ return;
14
+ const { isInteractive, askLine } = await import("./ui/prompt.js");
15
+ if (!isInteractive())
16
+ return;
17
+ process.stderr.write("\n AI auto-fix (RCE, AI tool-hijack, IDOR, complex SQLi) needs a provider key — you don't have one set.\n");
18
+ const ans = (await askLine(" Enable it now with a free Groq key? [y/N]: ")).trim().toLowerCase();
19
+ if (ans !== "y" && ans !== "yes") {
20
+ process.stderr.write(" Skipping — those findings will show as manual review. Run `securevibe config set-key groq` anytime.\n\n");
21
+ return;
22
+ }
23
+ const key = (await askLine(" Paste your Groq key (get one free at console.groq.com/keys): ")).trim();
24
+ if (!key) {
25
+ process.stderr.write(" No key entered — skipping.\n\n");
26
+ return;
27
+ }
28
+ const { setKey, configFilePath } = await import("./config.js");
29
+ await setKey("groq", key);
30
+ process.env.GROQ_API_KEY = key;
31
+ process.stderr.write(` Saved to ${configFilePath()}. This run will use it.\n\n`);
32
+ }
package/dist/index.js CHANGED
@@ -92,34 +92,6 @@ async function maybeNotifyUpdate(opts) {
92
92
  if (latest)
93
93
  process.stderr.write("\n" + updateNotice(VERSION, latest) + "\n");
94
94
  }
95
- /**
96
- * If `fix` is about to run with no AI-fixer provider key available (and we're
97
- * in an interactive terminal), offer to set one up on the spot — a free Groq
98
- * key takes under a minute. Never prompts under --json/non-TTY/--no-llm, and
99
- * never touches process.env if the user declines or a key is already set.
100
- */
101
- async function maybeOfferAiFixerSetup() {
102
- if (process.env.GROQ_API_KEY || process.env.ANTHROPIC_API_KEY)
103
- return;
104
- const { isInteractive, askLine } = await import("./ui/prompt.js");
105
- if (!isInteractive())
106
- return;
107
- process.stderr.write("\n AI auto-fix (RCE, AI tool-hijack, IDOR, complex SQLi) needs a provider key — you don't have one set.\n");
108
- const ans = (await askLine(" Enable it now with a free Groq key? [y/N]: ")).trim().toLowerCase();
109
- if (ans !== "y" && ans !== "yes") {
110
- process.stderr.write(" Skipping — those findings will show as manual review. Run `securevibe config set-key groq` anytime.\n\n");
111
- return;
112
- }
113
- const key = (await askLine(" Paste your Groq key (get one free at console.groq.com/keys): ")).trim();
114
- if (!key) {
115
- process.stderr.write(" No key entered — skipping.\n\n");
116
- return;
117
- }
118
- const { setKey, configFilePath } = await import("./config.js");
119
- await setKey("groq", key);
120
- process.env.GROQ_API_KEY = key;
121
- process.stderr.write(` Saved to ${configFilePath()}. This run will use it.\n\n`);
122
- }
123
95
  /** Exit code policy for CI gating (doc 08/12). */
124
96
  function ciExit(result, opts) {
125
97
  if (!opts.ci)
@@ -175,8 +147,10 @@ program
175
147
  const decision = decideApply(opts, isInteractive());
176
148
  // Only worth asking on a run that will actually write something — a dry-run
177
149
  // preview has no use for a key yet, and asking anyway reads as nagging.
178
- if (opts.llm !== false && !opts.json && decision.apply)
150
+ if (opts.llm !== false && !opts.json && decision.apply) {
151
+ const { maybeOfferAiFixerSetup } = await import("./index-support.js");
179
152
  await maybeOfferAiFixerSetup();
153
+ }
180
154
  const usage = await meter();
181
155
  const { runFix } = await import("./engine/fix/session.js");
182
156
  const { renderFixSession, fixToJson, renderApprovalRequest } = await import("./ui/fix.js");
@@ -352,6 +326,14 @@ program
352
326
  process.stderr.write(` unknown config action: ${action} (use: show | set-key | unset-key)\n`);
353
327
  process.exit(64);
354
328
  });
329
+ program
330
+ .command("repl")
331
+ .description("Interactive session: run scan/fix/etc without re-invoking the CLI each time")
332
+ .argument("[path]", "path to the repository", ".")
333
+ .action(async (pathArg) => {
334
+ const { runRepl } = await import("./repl.js");
335
+ await runRepl(pathArg ?? ".");
336
+ });
355
337
  program.parseAsync(process.argv).catch((err) => {
356
338
  process.stderr.write(`securevibe: ${err?.message ?? err}\n`);
357
339
  process.exit(70);
package/dist/repl.js ADDED
@@ -0,0 +1,252 @@
1
+ /**
2
+ * Interactive REPL (`securevibe repl`). A persistent session that holds the
3
+ * last scan result in memory and reuses the exact same underlying functions
4
+ * the one-shot commands use (scan(), runFix(), the ui/* renderers) — never
5
+ * the commander actions, since several of those call process.exit() on
6
+ * error/limit paths, which would tear down the whole session, not just the
7
+ * failed command.
8
+ *
9
+ * Only ever one readline interface alive at a time (via askLine, which opens
10
+ * and closes its own per call): the REPL's own prompt and any nested
11
+ * interactive flow (fix --apply confirmations, config set-key) are always
12
+ * sequential, never concurrent, so there's no stdin contention.
13
+ *
14
+ * Streams are injectable (runReplSession) so the whole loop — including
15
+ * fix --apply's confirm prompts — is unit-testable without a real TTY; the
16
+ * public runRepl() just wraps it with the real stdio + the TTY gate.
17
+ */
18
+ import path from "node:path";
19
+ import { askLine, isInteractive, decideApply, makeFixConfirm } from "./ui/prompt.js";
20
+ function tokenize(line) {
21
+ const out = [];
22
+ const re = /"([^"]*)"|'([^']*)'|(\S+)/g;
23
+ let m;
24
+ while ((m = re.exec(line)))
25
+ out.push(m[1] ?? m[2] ?? m[3]);
26
+ return out;
27
+ }
28
+ function hasFlag(tokens, ...names) {
29
+ return tokens.some((t) => names.includes(t));
30
+ }
31
+ function write(io, text) {
32
+ io.output.write(text);
33
+ }
34
+ async function meterOrPrint(io) {
35
+ const { recordUse, limitMessage } = await import("./usage.js");
36
+ const r = await recordUse();
37
+ if (!r.allowed) {
38
+ write(io, limitMessage() + "\n");
39
+ return undefined;
40
+ }
41
+ return r;
42
+ }
43
+ const HELP = [
44
+ " Commands:",
45
+ " scan [--json] full scan",
46
+ " fix [--apply] [--yes] [--no-llm] [--json] fix findings",
47
+ " ai-audit | protect | attack-map | score | deps | ready [--json]",
48
+ " init [--force] wire up the pre-commit guard + CI",
49
+ " config show | set-key <groq|anthropic> | unset-key <groq|anthropic>",
50
+ " db update | status manage the local OSV database",
51
+ " cd <path> change the scan target",
52
+ " pwd show the current scan target",
53
+ " help this message",
54
+ " exit | quit leave the session",
55
+ ].join("\n");
56
+ /** Public entry point: real stdio, refuses to start without a TTY. */
57
+ export async function runRepl(initialPath) {
58
+ if (!isInteractive()) {
59
+ process.stderr.write(" repl needs an interactive terminal.\n");
60
+ process.exitCode = 64;
61
+ return;
62
+ }
63
+ await runReplSession(initialPath, { input: process.stdin, output: process.stdout });
64
+ }
65
+ /** The actual session loop, over injectable streams — testable without a TTY. */
66
+ export async function runReplSession(initialPath, io) {
67
+ const { applyStoredKeysToEnv } = await import("./config.js");
68
+ await applyStoredKeysToEnv();
69
+ const state = { root: path.resolve(initialPath) };
70
+ const { renderBanner } = await import("./ui/banner.js");
71
+ write(io, renderBanner({ subtitle: "interactive session — type `help`" }) + "\n\n");
72
+ write(io, ` target: ${state.root}\n\n`);
73
+ while (true) {
74
+ const raw = await askLine(`securevibe (${path.basename(state.root)})> `, io.input, io.output);
75
+ const line = raw.trim();
76
+ if (!line)
77
+ continue;
78
+ const tokens = tokenize(line);
79
+ const cmd = tokens[0];
80
+ const rest = tokens.slice(1);
81
+ try {
82
+ if (cmd === "exit" || cmd === "quit" || cmd === "q")
83
+ break;
84
+ if (cmd === "help" || cmd === "?") {
85
+ write(io, HELP + "\n\n");
86
+ continue;
87
+ }
88
+ if (cmd === "pwd") {
89
+ write(io, ` ${state.root}\n\n`);
90
+ continue;
91
+ }
92
+ if (cmd === "cd") {
93
+ if (!rest[0]) {
94
+ write(io, " usage: cd <path>\n\n");
95
+ continue;
96
+ }
97
+ state.root = path.resolve(state.root, rest[0]);
98
+ write(io, ` target: ${state.root}\n\n`);
99
+ continue;
100
+ }
101
+ await dispatch(cmd, rest, state, io);
102
+ }
103
+ catch (err) {
104
+ write(io, ` error: ${err?.message ?? err}\n\n`);
105
+ }
106
+ }
107
+ }
108
+ async function dispatch(cmd, tokens, state, io) {
109
+ const asJson = hasFlag(tokens, "--json");
110
+ if (["scan", "ai-audit", "protect", "attack-map", "score", "deps", "ready"].includes(cmd)) {
111
+ const { scan } = await import("./engine/scan.js");
112
+ const usage = await meterOrPrint(io);
113
+ const result = await scan(state.root, {
114
+ aiOnly: cmd === "ai-audit",
115
+ onProgress: asJson ? undefined : (m) => write(io, ` · ${m}\n`),
116
+ });
117
+ state.lastResult = result;
118
+ if (asJson) {
119
+ const { toJson } = await import("./ui/emit.js");
120
+ write(io, toJson(result) + "\n\n");
121
+ return;
122
+ }
123
+ write(io, (await renderFor(cmd, result, usage)) + "\n");
124
+ return;
125
+ }
126
+ if (cmd === "fix") {
127
+ const { runFix } = await import("./engine/fix/session.js");
128
+ const { renderFixSession, fixToJson, renderApprovalRequest } = await import("./ui/fix.js");
129
+ const opts = { apply: hasFlag(tokens, "--apply"), yes: hasFlag(tokens, "--yes"), json: asJson };
130
+ const decision = decideApply(opts, true);
131
+ if (decision.apply && !hasFlag(tokens, "--no-llm")) {
132
+ const { maybeOfferAiFixerSetup } = await import("./index-support.js");
133
+ await maybeOfferAiFixerSetup();
134
+ }
135
+ const usage = await meterOrPrint(io);
136
+ const result = await runFix(state.root, {
137
+ apply: decision.apply,
138
+ noLlm: hasFlag(tokens, "--no-llm"),
139
+ confirm: decision.prompt ? makeFixConfirm(renderApprovalRequest, io.input, io.output) : undefined,
140
+ onProgress: asJson ? undefined : (m) => write(io, ` · ${m}\n`),
141
+ });
142
+ if (asJson) {
143
+ write(io, fixToJson(result) + "\n\n");
144
+ return;
145
+ }
146
+ write(io, renderFixSession(result, usage) + "\n");
147
+ if (decision.notice) {
148
+ write(io, " Not applied: re-run with `fix --apply --yes` to apply non-interactively.\n\n");
149
+ }
150
+ return;
151
+ }
152
+ if (cmd === "init") {
153
+ const { runInit } = await import("./engine/init.js");
154
+ const { renderInit } = await import("./ui/init.js");
155
+ const result = await runInit(state.root, { force: hasFlag(tokens, "--force") });
156
+ write(io, renderInit(result) + "\n");
157
+ return;
158
+ }
159
+ if (cmd === "config") {
160
+ await handleConfig(tokens, io);
161
+ return;
162
+ }
163
+ if (cmd === "db") {
164
+ await handleDb(tokens, io);
165
+ return;
166
+ }
167
+ write(io, ` unknown command: ${cmd} (type \`help\`)\n\n`);
168
+ }
169
+ async function renderFor(cmd, result, usage) {
170
+ if (cmd === "scan" || cmd === "ai-audit") {
171
+ const { renderReport } = await import("./ui/report.js");
172
+ return renderReport(result, { aiFocus: cmd === "ai-audit", usage });
173
+ }
174
+ if (cmd === "protect") {
175
+ const { renderRemediations } = await import("./ui/protect.js");
176
+ return renderRemediations(result, usage);
177
+ }
178
+ if (cmd === "attack-map") {
179
+ const { renderAttackMap } = await import("./ui/attackmap.js");
180
+ return renderAttackMap(result, usage);
181
+ }
182
+ if (cmd === "score") {
183
+ const { renderScoreOnly } = await import("./ui/protect.js");
184
+ return renderScoreOnly(result, usage);
185
+ }
186
+ if (cmd === "deps") {
187
+ const { renderDeps } = await import("./ui/deps.js");
188
+ return renderDeps(result, usage);
189
+ }
190
+ const { renderScorecard } = await import("./ui/readiness.js");
191
+ return renderScorecard(result, usage);
192
+ }
193
+ async function handleConfig(tokens, io) {
194
+ const [action, provider] = tokens;
195
+ const { readConfig, setKey, unsetKey, maskKey, configFilePath, ENV_VAR } = await import("./config.js");
196
+ if (action === "show" || !action) {
197
+ const cfg = await readConfig();
198
+ const line = (label, stored, envSet) => ` ${label}:`.padEnd(13) + (stored ? maskKey(stored) : "not set") + (envSet ? " (env var active)" : "");
199
+ write(io, " SecureVibe AI-fixer keys\n");
200
+ write(io, ` file: ${configFilePath()}\n`);
201
+ write(io, line("groq", cfg.groqApiKey, !!process.env.GROQ_API_KEY) + "\n");
202
+ write(io, line("anthropic", cfg.anthropicApiKey, !!process.env.ANTHROPIC_API_KEY) + "\n\n");
203
+ return;
204
+ }
205
+ if ((action === "set-key" || action === "unset-key") && provider !== "groq" && provider !== "anthropic") {
206
+ write(io, ` usage: config ${action} <groq|anthropic>\n\n`);
207
+ return;
208
+ }
209
+ if (action === "set-key") {
210
+ const hint = provider === "groq" ? " (free — console.groq.com/keys)" : " (console.anthropic.com)";
211
+ const key = (await askLine(` Paste your ${provider} API key${hint}: `, io.input, io.output)).trim();
212
+ if (!key) {
213
+ write(io, " No key entered — nothing saved.\n\n");
214
+ return;
215
+ }
216
+ await setKey(provider, key);
217
+ process.env[ENV_VAR[provider]] = key;
218
+ write(io, ` Saved to ${configFilePath()}. This session will use it.\n\n`);
219
+ return;
220
+ }
221
+ if (action === "unset-key") {
222
+ await unsetKey(provider);
223
+ write(io, ` Removed the stored ${provider} key.\n\n`);
224
+ return;
225
+ }
226
+ write(io, " usage: config show | set-key <groq|anthropic> | unset-key <groq|anthropic>\n\n");
227
+ }
228
+ async function handleDb(tokens, io) {
229
+ const [action] = tokens;
230
+ if (action === "status") {
231
+ const { loadDepDb, userDbDir } = await import("./engine/deps/db.js");
232
+ const db = await loadDepDb();
233
+ const ecos = Object.entries(db.ecosystems)
234
+ .map(([e, pkgs]) => `${e}: ${Object.keys(pkgs).length} packages`)
235
+ .join(", ");
236
+ write(io, ` SecureVibe OSV database\n date: ${db.date ?? "(bundled seed only)"}\n cache: ${userDbDir()}\n loaded: ${ecos || "(empty)"}\n\n`);
237
+ return;
238
+ }
239
+ if (action === "update") {
240
+ const { updateDb } = await import("./engine/deps/osv.js");
241
+ write(io, " · fetching OSV dumps (npm, PyPI)…\n");
242
+ try {
243
+ const r = await updateDb();
244
+ write(io, ` Updated local OSV db (${r.date}): ${r.count} packages → ${r.destDir}\n\n`);
245
+ }
246
+ catch (err) {
247
+ write(io, ` db update failed: ${err?.message ?? err}\n\n`);
248
+ }
249
+ return;
250
+ }
251
+ write(io, " usage: db update | status\n\n");
252
+ }
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.7";
5
+ export const VERSION = "0.1.8";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "securevibe",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "description": "Autonomous AI security engineer for AI generated apps: scan, fix, verify, and gate every commit before you launch",
5
5
  "type": "module",
6
6
  "license": "SEE LICENSE IN LICENSE",