securevibe 0.1.12 → 0.1.13
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 +7 -0
- package/dist/engine/chat.js +76 -0
- package/dist/engine/fix/llm.js +15 -11
- package/dist/repl.js +104 -13
- package/dist/ui/fix.js +1 -1
- package/dist/ui/prompt.js +6 -3
- package/dist/ui/repl-tui/App.js +3 -1
- package/dist/ui/repl-tui/commandRegistry.js +2 -0
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -54,6 +54,13 @@ before (`scan`, `fix --apply`, `help`, etc. — the `/` prefix is optional, not
|
|
|
54
54
|
Up/down arrow cycles through your command history. `scan`/`fix`/`explain` show a single
|
|
55
55
|
live progress spinner instead of scrolling status lines.
|
|
56
56
|
|
|
57
|
+
Inside the REPL, anything you type that is not a command is a question for the AI about
|
|
58
|
+
your last scan: "why is finding 3 critical?", "which of these should I fix first?".
|
|
59
|
+
Answers are grounded in the scan results only; the assistant never sees your files and
|
|
60
|
+
never claims your code is secure. The first question each session asks before anything
|
|
61
|
+
is sent to your provider. Needs the same key as `fix` and `explain`
|
|
62
|
+
(`securevibe config set-key groq`).
|
|
63
|
+
|
|
57
64
|
## Always-on (doc 12)
|
|
58
65
|
|
|
59
66
|
`securevibe init` makes the tool continuous so insecure code can't slip through:
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { SEVERITY_ORDER } from "./types.js";
|
|
2
|
+
import { llmChatMessages } from "./fix/llm.js";
|
|
3
|
+
/** Findings included in the grounding table, top N by severity then confidence. */
|
|
4
|
+
export const CHAT_MAX_FINDINGS = 30;
|
|
5
|
+
/** Prior exchanges (user + assistant pairs) kept and re-sent per question. */
|
|
6
|
+
export const CHAT_MAX_EXCHANGES = 10;
|
|
7
|
+
const CHAT_SYSTEM = [
|
|
8
|
+
"You are the assistant inside SecureVibe, a security scanner CLI. The user just scanned their repository; the scan results are below. Answer their questions about those results.",
|
|
9
|
+
"Rules you must never break:",
|
|
10
|
+
"- Answer ONLY from the scan results provided. You cannot see the repository's files, only the findings listed. If asked about anything not in them, say you can only see the scan results.",
|
|
11
|
+
"- Never call the code \"secure\", \"safe\", or \"unhackable\". A clean scan means our detectors found nothing, not that the code is secure.",
|
|
12
|
+
"- When you give remediation advice, end with: review the diff and run your tests.",
|
|
13
|
+
"- The user can run these commands: scan, fix (--apply), explain, ai-audit, protect, attack-map, score, deps, ready. Recommend those when relevant. You cannot fix, read files, or run anything yourself; never claim otherwise.",
|
|
14
|
+
"- Findings are numbered; \"finding 3\" means number 3 in the list below.",
|
|
15
|
+
"- Keep answers short and concrete: plain prose, no markdown headings, no bullet spam.",
|
|
16
|
+
].join("\n");
|
|
17
|
+
/** The compact, numbered findings table the assistant is grounded in. */
|
|
18
|
+
export function buildGrounding(result) {
|
|
19
|
+
const sorted = [...result.findings].sort((a, b) => SEVERITY_ORDER[b.severity] - SEVERITY_ORDER[a.severity] || b.confidence - a.confidence);
|
|
20
|
+
const shown = sorted.slice(0, CHAT_MAX_FINDINGS);
|
|
21
|
+
const lines = [
|
|
22
|
+
`Target: ${result.root}`,
|
|
23
|
+
`Score: grade ${result.score.grade} (composite ${result.score.composite}), readiness: ${result.score.readiness}`,
|
|
24
|
+
];
|
|
25
|
+
if (result.launchReadiness) {
|
|
26
|
+
lines.push(`Launch verdict: ${result.launchReadiness.verdict} (${result.launchReadiness.blockers} blockers, ${result.launchReadiness.warnings} warnings)`);
|
|
27
|
+
}
|
|
28
|
+
if (shown.length === 0) {
|
|
29
|
+
lines.push("Findings: none (no findings — our detectors found nothing; that does not prove the code is secure).");
|
|
30
|
+
return lines.join("\n");
|
|
31
|
+
}
|
|
32
|
+
lines.push(shown.length < sorted.length
|
|
33
|
+
? `Findings (top ${shown.length} of ${sorted.length}, by severity):`
|
|
34
|
+
: `Findings (${shown.length}):`);
|
|
35
|
+
shown.forEach((f, i) => {
|
|
36
|
+
lines.push(`${i + 1}. [${f.severity}] ${f.detector} ${f.file}:${f.line} - ${f.title}`);
|
|
37
|
+
if (f.evidence)
|
|
38
|
+
lines.push(` evidence: ${f.evidence.trim()}`);
|
|
39
|
+
});
|
|
40
|
+
return lines.join("\n");
|
|
41
|
+
}
|
|
42
|
+
export class ChatSession {
|
|
43
|
+
callLlm;
|
|
44
|
+
history = [];
|
|
45
|
+
grounding = null;
|
|
46
|
+
constructor(callLlm = llmChatMessages) {
|
|
47
|
+
this.callLlm = callLlm;
|
|
48
|
+
}
|
|
49
|
+
/** New grounding means a fresh conversation: the old answers described old results. */
|
|
50
|
+
setScanResult(result) {
|
|
51
|
+
this.grounding = buildGrounding(result);
|
|
52
|
+
this.history = [];
|
|
53
|
+
}
|
|
54
|
+
reset() {
|
|
55
|
+
this.grounding = null;
|
|
56
|
+
this.history = [];
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Ask one question. Returns null (and leaves history untouched) when there
|
|
60
|
+
* is no grounding or the call fails — the caller owns the user-facing
|
|
61
|
+
* degradation message, same contract as llmChat.
|
|
62
|
+
*/
|
|
63
|
+
async ask(question) {
|
|
64
|
+
if (this.grounding === null)
|
|
65
|
+
return null;
|
|
66
|
+
const messages = [...this.history, { role: "user", content: question }];
|
|
67
|
+
const answer = await this.callLlm(`${CHAT_SYSTEM}\n\nScan results:\n${this.grounding}`, messages);
|
|
68
|
+
if (answer == null)
|
|
69
|
+
return null;
|
|
70
|
+
this.history.push({ role: "user", content: question }, { role: "assistant", content: answer });
|
|
71
|
+
const max = CHAT_MAX_EXCHANGES * 2;
|
|
72
|
+
if (this.history.length > max)
|
|
73
|
+
this.history = this.history.slice(-max);
|
|
74
|
+
return answer;
|
|
75
|
+
}
|
|
76
|
+
}
|
package/dist/engine/fix/llm.js
CHANGED
|
@@ -102,22 +102,29 @@ export async function llmFix(input) {
|
|
|
102
102
|
/**
|
|
103
103
|
* Generic single-turn chat call against whichever provider is configured.
|
|
104
104
|
* Shared by llmFix (full-file rewrites) and engine/explain.ts (plain-language
|
|
105
|
-
* finding explanations)
|
|
106
|
-
*
|
|
107
|
-
* gracefully and must never throw because of this.
|
|
105
|
+
* finding explanations). Returns null on no-provider / network / auth / quota
|
|
106
|
+
* failure — callers degrade gracefully and must never throw because of this.
|
|
108
107
|
*/
|
|
109
108
|
export async function llmChat(system, user) {
|
|
109
|
+
return llmChatMessages(system, [{ role: "user", content: user }]);
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Multi turn variant: same provider selection and same null-on-failure
|
|
113
|
+
* contract as llmChat, but takes the full message history. Used by
|
|
114
|
+
* engine/chat.ts (the REPL's free text chat).
|
|
115
|
+
*/
|
|
116
|
+
export async function llmChatMessages(system, messages) {
|
|
110
117
|
const provider = selectProvider();
|
|
111
118
|
if (!provider)
|
|
112
119
|
return null;
|
|
113
120
|
try {
|
|
114
|
-
return provider === "groq" ? await callGroq(system,
|
|
121
|
+
return provider === "groq" ? await callGroq(system, messages) : await callAnthropic(system, messages);
|
|
115
122
|
}
|
|
116
123
|
catch {
|
|
117
124
|
return null;
|
|
118
125
|
}
|
|
119
126
|
}
|
|
120
|
-
async function callAnthropic(system,
|
|
127
|
+
async function callAnthropic(system, messages) {
|
|
121
128
|
let Anthropic;
|
|
122
129
|
try {
|
|
123
130
|
({ default: Anthropic } = await import("@anthropic-ai/sdk"));
|
|
@@ -130,12 +137,12 @@ async function callAnthropic(system, user) {
|
|
|
130
137
|
model: modelFor("anthropic"),
|
|
131
138
|
max_tokens: 8192,
|
|
132
139
|
system,
|
|
133
|
-
messages
|
|
140
|
+
messages,
|
|
134
141
|
});
|
|
135
142
|
return extractAnthropicText(msg);
|
|
136
143
|
}
|
|
137
144
|
/** Groq's chat-completions endpoint is OpenAI-compatible — plain fetch, no SDK. */
|
|
138
|
-
async function callGroq(system,
|
|
145
|
+
async function callGroq(system, messages) {
|
|
139
146
|
const res = await fetch("https://api.groq.com/openai/v1/chat/completions", {
|
|
140
147
|
method: "POST",
|
|
141
148
|
headers: {
|
|
@@ -145,10 +152,7 @@ async function callGroq(system, user) {
|
|
|
145
152
|
body: JSON.stringify({
|
|
146
153
|
model: modelFor("groq"),
|
|
147
154
|
max_tokens: 8192,
|
|
148
|
-
messages: [
|
|
149
|
-
{ role: "system", content: system },
|
|
150
|
-
{ role: "user", content: user },
|
|
151
|
-
],
|
|
155
|
+
messages: [{ role: "system", content: system }, ...messages],
|
|
152
156
|
}),
|
|
153
157
|
});
|
|
154
158
|
if (!res.ok)
|
package/dist/repl.js
CHANGED
|
@@ -70,27 +70,29 @@ export async function processLine(line, state, io) {
|
|
|
70
70
|
const cmd = tokens[0];
|
|
71
71
|
const rest = tokens.slice(1);
|
|
72
72
|
try {
|
|
73
|
-
if (cmd === "exit" || cmd === "quit" || cmd === "q")
|
|
73
|
+
if ((cmd === "exit" || cmd === "quit" || cmd === "q") && rest.length === 0)
|
|
74
74
|
return true;
|
|
75
|
-
if (cmd === "help" || cmd === "?") {
|
|
75
|
+
if ((cmd === "help" || cmd === "?") && rest.length === 0) {
|
|
76
76
|
const { renderHelp } = await import("./ui/repl-tui/commandRegistry.js");
|
|
77
77
|
write(io, renderHelp() + "\n\n");
|
|
78
78
|
return false;
|
|
79
79
|
}
|
|
80
|
-
if (cmd === "pwd") {
|
|
80
|
+
if (cmd === "pwd" && rest.length === 0) {
|
|
81
81
|
write(io, ` ${state.root}\n\n`);
|
|
82
82
|
return false;
|
|
83
83
|
}
|
|
84
|
-
if (cmd === "cd") {
|
|
84
|
+
if (cmd === "cd" && rest.length <= 1) {
|
|
85
85
|
if (!rest[0]) {
|
|
86
86
|
write(io, " usage: cd <path>\n\n");
|
|
87
87
|
return false;
|
|
88
88
|
}
|
|
89
89
|
state.root = path.resolve(state.root, rest[0]);
|
|
90
|
+
state.lastResult = undefined; // stale results would ground chat in the wrong repo
|
|
91
|
+
state.chat?.reset();
|
|
90
92
|
write(io, ` target: ${state.root}\n\n`);
|
|
91
93
|
return false;
|
|
92
94
|
}
|
|
93
|
-
await dispatch(cmd, rest, state, io);
|
|
95
|
+
await dispatch(cmd, rest, state, io, trimmed);
|
|
94
96
|
}
|
|
95
97
|
catch (err) {
|
|
96
98
|
write(io, ` error: ${err?.message ?? err}\n\n`);
|
|
@@ -118,9 +120,42 @@ function progressCallback(io, asJson) {
|
|
|
118
120
|
return undefined;
|
|
119
121
|
return io.onProgress ?? ((m) => write(io, ` · ${m}\n`));
|
|
120
122
|
}
|
|
121
|
-
|
|
123
|
+
/**
|
|
124
|
+
* True when `rest` (the tokens after the command word) is a plausible
|
|
125
|
+
* argument list for `cmd`'s actual grammar -- known flags/subcommands only.
|
|
126
|
+
* A command WORD matching a real command but followed by tokens outside its
|
|
127
|
+
* grammar is natural language that happens to start with that word (e.g.
|
|
128
|
+
* "score seems too low, why?"), not an invocation -- dispatch() falls
|
|
129
|
+
* through to chat instead of running the command. Every dispatch-level name
|
|
130
|
+
* here must also appear in ui/repl-tui/commandRegistry.ts's COMMANDS list;
|
|
131
|
+
* repl-command-collision.test.ts cross-checks the two so they can't drift.
|
|
132
|
+
*/
|
|
133
|
+
export function matchesCommandGrammar(cmd, rest) {
|
|
134
|
+
const jsonOnly = new Set(["scan", "ai-audit", "protect", "attack-map", "score", "deps", "ready", "explain"]);
|
|
135
|
+
if (jsonOnly.has(cmd))
|
|
136
|
+
return rest.every((t) => t === "--json");
|
|
137
|
+
if (cmd === "fix") {
|
|
138
|
+
const known = new Set(["--apply", "--yes", "--no-llm", "--json"]);
|
|
139
|
+
return rest.every((t) => known.has(t));
|
|
140
|
+
}
|
|
141
|
+
if (cmd === "init")
|
|
142
|
+
return rest.every((t) => t === "--force");
|
|
143
|
+
if (cmd === "config") {
|
|
144
|
+
if (rest.length === 0)
|
|
145
|
+
return true;
|
|
146
|
+
if (rest[0] === "show")
|
|
147
|
+
return rest.length === 1;
|
|
148
|
+
if (rest[0] === "set-key" || rest[0] === "unset-key")
|
|
149
|
+
return rest.length <= 2;
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
if (cmd === "db")
|
|
153
|
+
return rest.length === 0 || (rest.length === 1 && (rest[0] === "update" || rest[0] === "status"));
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
async function dispatch(cmd, tokens, state, io, rawLine) {
|
|
122
157
|
const asJson = hasFlag(tokens, "--json");
|
|
123
|
-
if (["scan", "ai-audit", "protect", "attack-map", "score", "deps", "ready"].includes(cmd)) {
|
|
158
|
+
if (["scan", "ai-audit", "protect", "attack-map", "score", "deps", "ready"].includes(cmd) && matchesCommandGrammar(cmd, tokens)) {
|
|
124
159
|
const { scan } = await import("./engine/scan.js");
|
|
125
160
|
const usage = await meterOrPrint(io);
|
|
126
161
|
const result = await scan(state.root, {
|
|
@@ -128,6 +163,7 @@ async function dispatch(cmd, tokens, state, io) {
|
|
|
128
163
|
onProgress: progressCallback(io, asJson),
|
|
129
164
|
});
|
|
130
165
|
state.lastResult = result;
|
|
166
|
+
state.chat?.setScanResult(result);
|
|
131
167
|
if (asJson) {
|
|
132
168
|
const { toJson } = await import("./ui/emit.js");
|
|
133
169
|
write(io, toJson(result) + "\n\n");
|
|
@@ -136,7 +172,7 @@ async function dispatch(cmd, tokens, state, io) {
|
|
|
136
172
|
write(io, (await renderFor(cmd, result, usage)) + "\n");
|
|
137
173
|
return;
|
|
138
174
|
}
|
|
139
|
-
if (cmd === "fix") {
|
|
175
|
+
if (cmd === "fix" && matchesCommandGrammar(cmd, tokens)) {
|
|
140
176
|
const { runFix } = await import("./engine/fix/session.js");
|
|
141
177
|
const { renderFixSession, fixToJson, renderApprovalRequest } = await import("./ui/fix.js");
|
|
142
178
|
const opts = { apply: hasFlag(tokens, "--apply"), yes: hasFlag(tokens, "--yes"), json: asJson };
|
|
@@ -175,7 +211,7 @@ async function dispatch(cmd, tokens, state, io) {
|
|
|
175
211
|
}
|
|
176
212
|
return;
|
|
177
213
|
}
|
|
178
|
-
if (cmd === "explain") {
|
|
214
|
+
if (cmd === "explain" && matchesCommandGrammar(cmd, tokens)) {
|
|
179
215
|
if (!asJson) {
|
|
180
216
|
if (io.inkMode) {
|
|
181
217
|
const { noKeyNudgeMessage } = await import("./index-support.js");
|
|
@@ -205,7 +241,7 @@ async function dispatch(cmd, tokens, state, io) {
|
|
|
205
241
|
write(io, renderExplain(report, usage) + "\n");
|
|
206
242
|
return;
|
|
207
243
|
}
|
|
208
|
-
if (cmd === "init") {
|
|
244
|
+
if (cmd === "init" && matchesCommandGrammar(cmd, tokens)) {
|
|
209
245
|
const { runInit } = await import("./engine/init.js");
|
|
210
246
|
const { renderInit } = await import("./ui/init.js");
|
|
211
247
|
const result = await runInit(state.root, { force: hasFlag(tokens, "--force") });
|
|
@@ -223,15 +259,70 @@ async function dispatch(cmd, tokens, state, io) {
|
|
|
223
259
|
: undefined);
|
|
224
260
|
return;
|
|
225
261
|
}
|
|
226
|
-
if (cmd === "config") {
|
|
262
|
+
if (cmd === "config" && matchesCommandGrammar(cmd, tokens)) {
|
|
227
263
|
await handleConfig(tokens, io);
|
|
228
264
|
return;
|
|
229
265
|
}
|
|
230
|
-
if (cmd === "db") {
|
|
266
|
+
if (cmd === "db" && matchesCommandGrammar(cmd, tokens)) {
|
|
231
267
|
await handleDb(tokens, io);
|
|
232
268
|
return;
|
|
233
269
|
}
|
|
234
|
-
|
|
270
|
+
await handleChat(rawLine, state, io);
|
|
271
|
+
}
|
|
272
|
+
/** Two-space indent so answers sit inside the REPL's output gutter. */
|
|
273
|
+
function indentAnswer(text) {
|
|
274
|
+
return text
|
|
275
|
+
.split("\n")
|
|
276
|
+
.map((l) => " " + l)
|
|
277
|
+
.join("\n");
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Free text = a question about the last scan (spec 2026-07-12). Order of
|
|
281
|
+
* gates: scan first (nothing to talk about), then key (consent would be
|
|
282
|
+
* pointless without one), then once-per-session consent, then the ask.
|
|
283
|
+
* NOT metered: no scan runs here and the call spends the user's own key,
|
|
284
|
+
* the same reasoning that exempts --staged/--diff.
|
|
285
|
+
*/
|
|
286
|
+
async function handleChat(question, state, io) {
|
|
287
|
+
if (!state.lastResult) {
|
|
288
|
+
write(io, " Run `scan` first so I have findings to talk about. (Free text asks the AI about your last scan; type `help` for commands.)\n\n");
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
const { llmAvailability } = await import("./engine/fix/llm.js");
|
|
292
|
+
const avail = llmAvailability(false);
|
|
293
|
+
if (!avail.available) {
|
|
294
|
+
write(io, " Chat needs an AI key. Run `securevibe config set-key groq` (outside the REPL) to enable it; `explain` and `fix` use the same key.\n\n");
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
if (state.chatConsent === false) {
|
|
298
|
+
write(io, " Chat is off for this session (you declined earlier). Commands still work; type `help`.\n\n");
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
if (state.chatConsent === undefined) {
|
|
302
|
+
const render = (req) => req.kind === "chat-consent"
|
|
303
|
+
? ` First question this session: send your questions and the scan findings to ${req.provider}?`
|
|
304
|
+
: "";
|
|
305
|
+
const confirm = io.makeConfirm ? io.makeConfirm(render) : makeFixConfirm(render, io.input, io.output);
|
|
306
|
+
const answer = await confirm({ kind: "chat-consent", provider: avail.provider });
|
|
307
|
+
state.chatConsent = answer === "yes";
|
|
308
|
+
if (!state.chatConsent) {
|
|
309
|
+
write(io, " Understood, chat stays off for this session. Commands still work; type `help`.\n\n");
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
if (!state.chat) {
|
|
314
|
+
const { ChatSession } = await import("./engine/chat.js");
|
|
315
|
+
state.chat = new ChatSession();
|
|
316
|
+
state.chat.setScanResult(state.lastResult);
|
|
317
|
+
}
|
|
318
|
+
const progress = progressCallback(io, false);
|
|
319
|
+
progress?.(`asking ${avail.provider}…`);
|
|
320
|
+
const answer = await state.chat.ask(question);
|
|
321
|
+
if (answer == null) {
|
|
322
|
+
write(io, " The AI call failed; nothing was answered. Try again, or use `explain` for per finding context.\n\n");
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
write(io, indentAnswer(answer) + "\n\n");
|
|
235
326
|
}
|
|
236
327
|
async function renderFor(cmd, result, usage) {
|
|
237
328
|
if (cmd === "scan" || cmd === "ai-audit") {
|
package/dist/ui/fix.js
CHANGED
|
@@ -131,7 +131,7 @@ export function renderApprovalRequest(req) {
|
|
|
131
131
|
L.push(renderDiff(p.relPath, p.before, p.after));
|
|
132
132
|
L.push(" " + pc.dim("verified: finding clears · no new issue · file still parses"));
|
|
133
133
|
}
|
|
134
|
-
else {
|
|
134
|
+
else if (req.kind === "side-effects") {
|
|
135
135
|
L.push("");
|
|
136
136
|
L.push(" " + pc.bold("Supporting changes") + pc.dim(" (additive, no break risk)"));
|
|
137
137
|
for (const fx of req.effects) {
|
package/dist/ui/prompt.js
CHANGED
|
@@ -26,10 +26,11 @@ export function decideApply(opts, interactive) {
|
|
|
26
26
|
return { apply: false, prompt: false, notice: true };
|
|
27
27
|
return { apply: true, prompt: true, notice: false };
|
|
28
28
|
}
|
|
29
|
-
/** Map a raw keypress/line to an answer. Empty / unknown → the safe default "no".
|
|
29
|
+
/** Map a raw keypress/line to an answer. Empty / unknown → the safe default "no".
|
|
30
|
+
* Only "patch" offers all/quit; side-effects and chat-consent are yes/no. */
|
|
30
31
|
export function mapAnswer(raw, kind) {
|
|
31
32
|
const c = raw.trim().toLowerCase().charAt(0);
|
|
32
|
-
if (kind
|
|
33
|
+
if (kind !== "patch")
|
|
33
34
|
return c === "y" ? "yes" : "no";
|
|
34
35
|
if (c === "y")
|
|
35
36
|
return "yes";
|
|
@@ -55,7 +56,9 @@ export function makeFixConfirm(render, input = process.stdin, output = process.s
|
|
|
55
56
|
output.write(render(req) + "\n");
|
|
56
57
|
const prompt = req.kind === "patch"
|
|
57
58
|
? " Apply this change? [y]es / [n]o / [a]ll / [q]uit: "
|
|
58
|
-
:
|
|
59
|
+
: req.kind === "side-effects"
|
|
60
|
+
? " Apply these supporting changes? [y]es / [n]o: "
|
|
61
|
+
: " Send to your AI provider? [y]es / [n]o: ";
|
|
59
62
|
const raw = await askLine(prompt, input, output);
|
|
60
63
|
return mapAnswer(raw, req.kind);
|
|
61
64
|
};
|
package/dist/ui/repl-tui/App.js
CHANGED
|
@@ -118,7 +118,9 @@ export function App({ initialPath }) {
|
|
|
118
118
|
}), h(Box, { flexDirection: "column" }, h(StatusLine, { targetRoot: stateRef.current.root }), busy && progressMessage ? h(Spinner, { message: progressMessage }) : null, confirmKind
|
|
119
119
|
? h(Text, { dimColor: true }, confirmKind === "patch"
|
|
120
120
|
? " Apply this change? [y]es / [n]o / [a]ll / [q]uit"
|
|
121
|
-
:
|
|
121
|
+
: confirmKind === "side-effects"
|
|
122
|
+
? " Apply these supporting changes? [y]es / [n]o"
|
|
123
|
+
: " Send to your AI provider? [y]es / [n]o")
|
|
122
124
|
: null, menuOpen ? h(SlashMenu, { items: filteredCommands, selectedIndex: clampedIndex }) : null, h(InputBox, {
|
|
123
125
|
value,
|
|
124
126
|
onChange: handleChange,
|
|
@@ -32,5 +32,7 @@ export function renderHelp() {
|
|
|
32
32
|
const left = ` ${c.name}${c.args ? " " + c.args : ""}`;
|
|
33
33
|
lines.push(left.length < 44 ? left.padEnd(44) + c.description : left + " " + c.description);
|
|
34
34
|
}
|
|
35
|
+
lines.push("");
|
|
36
|
+
lines.push(" Anything else you type is a question for the AI about your last scan (needs a key: `config set-key groq`).");
|
|
35
37
|
return lines.join("\n");
|
|
36
38
|
}
|
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.
|
|
5
|
+
export const VERSION = "0.1.13";
|
package/package.json
CHANGED