@pentoshi/clai 0.6.0 → 0.7.2
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 +18 -15
- package/dist/agent/context-manager.d.ts +27 -0
- package/dist/agent/context-manager.js +75 -0
- package/dist/agent/context-manager.js.map +1 -0
- package/dist/agent/runner.d.ts +21 -1
- package/dist/agent/runner.js +185 -74
- package/dist/agent/runner.js.map +1 -1
- package/dist/commands/doctor.js +21 -2
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/update.js +11 -2
- package/dist/commands/update.js.map +1 -1
- package/dist/index.js +172 -5
- package/dist/index.js.map +1 -1
- package/dist/llm/anthropic.js +29 -38
- package/dist/llm/anthropic.js.map +1 -1
- package/dist/llm/gemini.d.ts +2 -0
- package/dist/llm/gemini.js +40 -43
- package/dist/llm/gemini.js.map +1 -1
- package/dist/llm/http.d.ts +23 -1
- package/dist/llm/http.js +197 -13
- package/dist/llm/http.js.map +1 -1
- package/dist/llm/nvidia.js +1 -0
- package/dist/llm/nvidia.js.map +1 -1
- package/dist/llm/ollama.js +18 -27
- package/dist/llm/ollama.js.map +1 -1
- package/dist/llm/router.d.ts +7 -0
- package/dist/llm/router.js +16 -23
- package/dist/llm/router.js.map +1 -1
- package/dist/modes/agent.d.ts +4 -2
- package/dist/modes/agent.js +2 -2
- package/dist/modes/agent.js.map +1 -1
- package/dist/modes/ask.js +3 -4
- package/dist/modes/ask.js.map +1 -1
- package/dist/os/pkgmgr.d.ts +7 -1
- package/dist/os/pkgmgr.js +97 -18
- package/dist/os/pkgmgr.js.map +1 -1
- package/dist/prompts/index.d.ts +7 -0
- package/dist/prompts/index.js +12 -4
- package/dist/prompts/index.js.map +1 -1
- package/dist/repl.d.ts +1 -0
- package/dist/repl.js +363 -45
- package/dist/repl.js.map +1 -1
- package/dist/safety/classifier.d.ts +7 -1
- package/dist/safety/classifier.js +260 -86
- package/dist/safety/classifier.js.map +1 -1
- package/dist/safety/patterns.d.ts +48 -1
- package/dist/safety/patterns.js +140 -7
- package/dist/safety/patterns.js.map +1 -1
- package/dist/store/config.d.ts +23 -3
- package/dist/store/config.js +31 -11
- package/dist/store/config.js.map +1 -1
- package/dist/store/history.d.ts +9 -0
- package/dist/store/history.js +58 -1
- package/dist/store/history.js.map +1 -1
- package/dist/store/keys.d.ts +2 -1
- package/dist/store/keys.js +7 -3
- package/dist/store/keys.js.map +1 -1
- package/dist/store/logs.d.ts +7 -0
- package/dist/store/logs.js +39 -1
- package/dist/store/logs.js.map +1 -1
- package/dist/store/project.d.ts +1 -0
- package/dist/store/project.js +34 -9
- package/dist/store/project.js.map +1 -1
- package/dist/store/scope.d.ts +32 -0
- package/dist/store/scope.js +161 -0
- package/dist/store/scope.js.map +1 -0
- package/dist/tools/fs.d.ts +6 -2
- package/dist/tools/fs.js +99 -87
- package/dist/tools/fs.js.map +1 -1
- package/dist/tools/http.d.ts +5 -3
- package/dist/tools/http.js +170 -31
- package/dist/tools/http.js.map +1 -1
- package/dist/tools/policies/output-policy.d.ts +13 -0
- package/dist/tools/policies/output-policy.js +56 -0
- package/dist/tools/policies/output-policy.js.map +1 -0
- package/dist/tools/reducers/ffuf.d.ts +6 -0
- package/dist/tools/reducers/ffuf.js +74 -0
- package/dist/tools/reducers/ffuf.js.map +1 -0
- package/dist/tools/reducers/generic.d.ts +2 -0
- package/dist/tools/reducers/generic.js +60 -0
- package/dist/tools/reducers/generic.js.map +1 -0
- package/dist/tools/reducers/gobuster.d.ts +2 -0
- package/dist/tools/reducers/gobuster.js +36 -0
- package/dist/tools/reducers/gobuster.js.map +1 -0
- package/dist/tools/reducers/httpx.d.ts +2 -0
- package/dist/tools/reducers/httpx.js +38 -0
- package/dist/tools/reducers/httpx.js.map +1 -0
- package/dist/tools/reducers/nmap.d.ts +7 -0
- package/dist/tools/reducers/nmap.js +82 -0
- package/dist/tools/reducers/nmap.js.map +1 -0
- package/dist/tools/reducers/nuclei.d.ts +2 -0
- package/dist/tools/reducers/nuclei.js +51 -0
- package/dist/tools/reducers/nuclei.js.map +1 -0
- package/dist/tools/reducers/sqlmap.d.ts +2 -0
- package/dist/tools/reducers/sqlmap.js +39 -0
- package/dist/tools/reducers/sqlmap.js.map +1 -0
- package/dist/tools/reducers/subdomains.d.ts +6 -0
- package/dist/tools/reducers/subdomains.js +31 -0
- package/dist/tools/reducers/subdomains.js.map +1 -0
- package/dist/tools/reducers/types.d.ts +14 -0
- package/dist/tools/reducers/types.js +2 -0
- package/dist/tools/reducers/types.js.map +1 -0
- package/dist/tools/registry.d.ts +1 -1
- package/dist/tools/registry.js +223 -79
- package/dist/tools/registry.js.map +1 -1
- package/dist/tools/shell.d.ts +45 -4
- package/dist/tools/shell.js +419 -88
- package/dist/tools/shell.js.map +1 -1
- package/dist/tools/validate.d.ts +37 -0
- package/dist/tools/validate.js +144 -0
- package/dist/tools/validate.js.map +1 -0
- package/dist/types.d.ts +7 -15
- package/dist/ui/keys.d.ts +21 -0
- package/dist/ui/keys.js +13 -0
- package/dist/ui/keys.js.map +1 -0
- package/dist/ui/output-pane.d.ts +31 -0
- package/dist/ui/output-pane.js +81 -0
- package/dist/ui/output-pane.js.map +1 -0
- package/package.json +1 -1
package/dist/repl.js
CHANGED
|
@@ -3,18 +3,20 @@ import { stdin as input, stdout as output } from "node:process";
|
|
|
3
3
|
import chalk from "chalk";
|
|
4
4
|
import { search } from "@inquirer/prompts";
|
|
5
5
|
import { runAskStream } from "./modes/ask.js";
|
|
6
|
-
import { runAgent } from "./modes/agent.js";
|
|
6
|
+
import { runAgent, createSessionPolicy, } from "./modes/agent.js";
|
|
7
7
|
import { providerSwitcher, printProviderKeys, setProviderKey, unsetProviderKey, } from "./commands/providers.js";
|
|
8
8
|
import { getConfig, getProviderModel, setDefaultMode, setProviderModel, setThinking, updateConfig, } from "./store/config.js";
|
|
9
9
|
import { listSessions, saveSession } from "./store/history.js";
|
|
10
10
|
import { assertProvider, defaultModels } from "./llm/provider.js";
|
|
11
|
-
import { runUpdate, checkForUpdateSilent, getCurrentVersion } from "./commands/update.js";
|
|
11
|
+
import { runUpdate, checkForUpdateSilent, getCurrentVersion, } from "./commands/update.js";
|
|
12
12
|
import { renderBanner, renderSessionInfo, renderSuggestions, renderModeSwitch, renderProviderSwitch, PROMPT, } from "./ui/banner.js";
|
|
13
13
|
import { clearThinking, createThinkingStreamParser, getLastThinking, rememberThinkingFromText, renderThinkingBlock, renderThinkingSummary, renderThinkingToggleMessage, } from "./ui/thinking.js";
|
|
14
14
|
import { createMarkdownStreamWriter, renderMarkdown } from "./ui/markdown.js";
|
|
15
15
|
import { startThinkingSpinner } from "./ui/spinner.js";
|
|
16
16
|
import { modelSupportsThinking } from "./llm/capabilities.js";
|
|
17
|
-
import {
|
|
17
|
+
import { getLastViewport, getViewport, listViewports, toggleViewport, } from "./ui/output-pane.js";
|
|
18
|
+
import { compactMessages, estimateMessagesTokens, } from "./agent/context-manager.js";
|
|
19
|
+
import { isCtrlC, isCtrlO, isCtrlT, isEscape } from "./ui/keys.js";
|
|
18
20
|
const slashCommands = [
|
|
19
21
|
{ command: "/ask", description: "switch to ask mode" },
|
|
20
22
|
{ command: "/agent", description: "switch to agent mode" },
|
|
@@ -50,10 +52,45 @@ const slashCommands = [
|
|
|
50
52
|
{ command: "/history", description: "show past sessions" },
|
|
51
53
|
{ command: "/save", usage: "<name>", description: "save session" },
|
|
52
54
|
{ command: "/cwd", usage: "<path>", description: "change working directory" },
|
|
53
|
-
{
|
|
55
|
+
{
|
|
56
|
+
command: "/allow",
|
|
57
|
+
usage: "<tool>|list",
|
|
58
|
+
description: "allow a tool for this session (not persisted)",
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
command: "/disallow",
|
|
62
|
+
usage: "<tool>",
|
|
63
|
+
description: "revoke a session allow",
|
|
64
|
+
},
|
|
54
65
|
{ command: "/think", description: "show thinking from last response" },
|
|
55
66
|
{ command: "/thinking", description: "alias for /think" },
|
|
56
|
-
{
|
|
67
|
+
{
|
|
68
|
+
command: "/output",
|
|
69
|
+
usage: "[last|<id>|list]",
|
|
70
|
+
description: "toggle full tool output (same as Ctrl+O)",
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
command: "/freeonly",
|
|
74
|
+
usage: "[on|off]",
|
|
75
|
+
description: "skip paid providers when fallback is enabled",
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
command: "/fallback",
|
|
79
|
+
usage: "[on|off]",
|
|
80
|
+
description: "try other configured providers after a failure (off by default)",
|
|
81
|
+
},
|
|
82
|
+
{ command: "/compact", description: "compact session history now" },
|
|
83
|
+
{ command: "/context", description: "show estimated context size" },
|
|
84
|
+
{
|
|
85
|
+
command: "/scope",
|
|
86
|
+
usage: "[show|clear|new|add <targets>]",
|
|
87
|
+
description: "manage pentest engagement scope",
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
command: "/privacy",
|
|
91
|
+
usage: "[status|clear-history|clear-logs|clear-artifacts|clear-all|on|off]",
|
|
92
|
+
description: "control retention and private mode (in-memory only)",
|
|
93
|
+
},
|
|
57
94
|
{ command: "/update", description: "check for updates" },
|
|
58
95
|
{ command: "/exit", description: "quit" },
|
|
59
96
|
{ command: "/quit", description: "alias for /exit" },
|
|
@@ -186,7 +223,9 @@ function isAbortLikeError(error) {
|
|
|
186
223
|
return false;
|
|
187
224
|
}
|
|
188
225
|
function slashCommandLabel(command) {
|
|
189
|
-
return command.usage
|
|
226
|
+
return command.usage
|
|
227
|
+
? `${command.command} ${command.usage}`
|
|
228
|
+
: command.command;
|
|
190
229
|
}
|
|
191
230
|
function slashCommandFilter(line) {
|
|
192
231
|
if (!line.startsWith("/") || /\s/.test(line))
|
|
@@ -268,6 +307,18 @@ async function readPromptLine(options) {
|
|
|
268
307
|
if (input.isTTY)
|
|
269
308
|
input.setRawMode(false);
|
|
270
309
|
};
|
|
310
|
+
const clearPromptDisplay = () => {
|
|
311
|
+
const previousMenuLines = renderedMenuLines;
|
|
312
|
+
cursorTo(output, 0);
|
|
313
|
+
clearLine(output, 0);
|
|
314
|
+
for (let i = 0; i < previousMenuLines; i += 1) {
|
|
315
|
+
output.write("\n");
|
|
316
|
+
clearLine(output, 0);
|
|
317
|
+
}
|
|
318
|
+
if (previousMenuLines > 0)
|
|
319
|
+
moveCursor(output, 0, -previousMenuLines);
|
|
320
|
+
renderedMenuLines = 0;
|
|
321
|
+
};
|
|
271
322
|
const submit = (submittedLine) => {
|
|
272
323
|
const previousMenuLines = renderedMenuLines;
|
|
273
324
|
line = submittedLine;
|
|
@@ -294,7 +345,7 @@ async function readPromptLine(options) {
|
|
|
294
345
|
// selecting + copying never breaks the REPL.
|
|
295
346
|
if (key.meta && !key.ctrl && key.name === "c")
|
|
296
347
|
return;
|
|
297
|
-
if (key
|
|
348
|
+
if (isCtrlC(key)) {
|
|
298
349
|
// First press: clear the current line. Second press within 1s: exit.
|
|
299
350
|
// This mirrors bash / Claude Code and avoids killing the REPL by
|
|
300
351
|
// accident when users habitually press Ctrl+C to copy in some
|
|
@@ -317,15 +368,15 @@ async function readPromptLine(options) {
|
|
|
317
368
|
renderedMenuLines = 0;
|
|
318
369
|
return;
|
|
319
370
|
}
|
|
320
|
-
if (key
|
|
371
|
+
if (isCtrlT(key)) {
|
|
321
372
|
options.onThinkingShortcut();
|
|
322
373
|
refresh();
|
|
323
374
|
return;
|
|
324
375
|
}
|
|
325
|
-
if (key
|
|
376
|
+
if (isCtrlO(key)) {
|
|
377
|
+
clearPromptDisplay();
|
|
326
378
|
output.write("\n");
|
|
327
|
-
|
|
328
|
-
options.onOutputToggle();
|
|
379
|
+
void options.onOutputShortcut().finally(refresh);
|
|
329
380
|
return;
|
|
330
381
|
}
|
|
331
382
|
if (key.name === "return" || key.name === "enter") {
|
|
@@ -342,7 +393,7 @@ async function readPromptLine(options) {
|
|
|
342
393
|
}
|
|
343
394
|
return;
|
|
344
395
|
}
|
|
345
|
-
if (key
|
|
396
|
+
if (isEscape(key)) {
|
|
346
397
|
if (menu.visible) {
|
|
347
398
|
dismissedSlashLine = line;
|
|
348
399
|
refresh();
|
|
@@ -352,7 +403,8 @@ async function readPromptLine(options) {
|
|
|
352
403
|
if (key.name === "up") {
|
|
353
404
|
if (menu.visible && menu.suggestions.length > 0) {
|
|
354
405
|
selectedIndex =
|
|
355
|
-
(selectedIndex - 1 + menu.suggestions.length) %
|
|
406
|
+
(selectedIndex - 1 + menu.suggestions.length) %
|
|
407
|
+
menu.suggestions.length;
|
|
356
408
|
refresh();
|
|
357
409
|
return;
|
|
358
410
|
}
|
|
@@ -517,7 +569,8 @@ async function withAbortableInput(run) {
|
|
|
517
569
|
return await run(ac.signal);
|
|
518
570
|
}
|
|
519
571
|
catch (error) {
|
|
520
|
-
if (ac.signal.aborted ||
|
|
572
|
+
if (ac.signal.aborted ||
|
|
573
|
+
(error instanceof Error && error.name === "AbortError")) {
|
|
521
574
|
throw new AbortRunError();
|
|
522
575
|
}
|
|
523
576
|
throw error;
|
|
@@ -536,7 +589,8 @@ function help() {
|
|
|
536
589
|
return ` ${chalk.cyan(label)}${chalk.dim(command.description)}`;
|
|
537
590
|
})
|
|
538
591
|
.join("\n");
|
|
539
|
-
return lines +
|
|
592
|
+
return (lines +
|
|
593
|
+
chalk.dim("\n\n ESC abort │ Ctrl+C clears input (twice to exit) │ Ctrl+T toggle thinking │ Ctrl+O / /output last toggle tool output"));
|
|
540
594
|
}
|
|
541
595
|
async function pickModelInteractively(provider, currentModel) {
|
|
542
596
|
const models = knownModels[provider] ?? [];
|
|
@@ -771,17 +825,263 @@ async function handleSlash(line, state) {
|
|
|
771
825
|
}
|
|
772
826
|
case "/allow": {
|
|
773
827
|
const tool = args[0];
|
|
774
|
-
if (!tool)
|
|
775
|
-
console.log(chalk.dim("usage: /allow <tool
|
|
828
|
+
if (!tool) {
|
|
829
|
+
console.log(chalk.dim("usage: /allow <tool>|list"));
|
|
830
|
+
return true;
|
|
831
|
+
}
|
|
832
|
+
if (tool === "list" || tool === "ls") {
|
|
833
|
+
if (state.session.allow.size === 0) {
|
|
834
|
+
console.log(chalk.dim(" no session allows"));
|
|
835
|
+
}
|
|
836
|
+
else {
|
|
837
|
+
for (const allowed of state.session.allow) {
|
|
838
|
+
console.log(chalk.dim(` ✓ ${allowed}`));
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
return true;
|
|
842
|
+
}
|
|
843
|
+
state.session.allow.add(tool);
|
|
844
|
+
console.log(chalk.dim(` allowed ${tool} for this session only ✓`));
|
|
845
|
+
return true;
|
|
846
|
+
}
|
|
847
|
+
case "/disallow": {
|
|
848
|
+
const tool = args[0];
|
|
849
|
+
if (!tool) {
|
|
850
|
+
console.log(chalk.dim("usage: /disallow <tool>"));
|
|
851
|
+
return true;
|
|
852
|
+
}
|
|
853
|
+
if (state.session.allow.delete(tool)) {
|
|
854
|
+
console.log(chalk.dim(` revoked ${tool} ✓`));
|
|
855
|
+
}
|
|
776
856
|
else {
|
|
777
|
-
|
|
778
|
-
updateConfig({
|
|
779
|
-
allowAlwaysTools: Array.from(new Set([...config.allowAlwaysTools, tool])),
|
|
780
|
-
});
|
|
781
|
-
console.log(chalk.dim(` allowed ${tool} ✓`));
|
|
857
|
+
console.log(chalk.dim(` ${tool} was not in the session allow list`));
|
|
782
858
|
}
|
|
783
859
|
return true;
|
|
784
860
|
}
|
|
861
|
+
case "/context": {
|
|
862
|
+
const tokens = estimateMessagesTokens(state.messages);
|
|
863
|
+
console.log(chalk.dim(` ${state.messages.length} message(s), ~${tokens.toLocaleString()} tokens estimated`));
|
|
864
|
+
return true;
|
|
865
|
+
}
|
|
866
|
+
case "/compact": {
|
|
867
|
+
const before = state.messages.length;
|
|
868
|
+
const compacted = compactMessages(state.messages, { budgetTokens: 0 });
|
|
869
|
+
state.messages.splice(0, state.messages.length, ...compacted);
|
|
870
|
+
console.log(chalk.dim(` compacted ${before} → ${state.messages.length} messages (~${estimateMessagesTokens(state.messages).toLocaleString()} tokens)`));
|
|
871
|
+
return true;
|
|
872
|
+
}
|
|
873
|
+
case "/scope": {
|
|
874
|
+
const sub = (args[0] ?? "show").toLowerCase();
|
|
875
|
+
const { loadScope, saveScope, addScopeTargets, clearScope, isScopeActive, getScopePath, resetScopeCache, } = await import("./store/scope.js");
|
|
876
|
+
if (sub === "show" || sub === "ls" || sub === "list") {
|
|
877
|
+
resetScopeCache();
|
|
878
|
+
const scope = await loadScope();
|
|
879
|
+
if (!scope) {
|
|
880
|
+
console.log(chalk.dim(" no engagement scope configured"));
|
|
881
|
+
console.log(chalk.dim(` expected at: ${getScopePath()}`));
|
|
882
|
+
console.log(chalk.dim(" create one with: /scope add domain1,domain2 or `clai scope add --targets ...`"));
|
|
883
|
+
return true;
|
|
884
|
+
}
|
|
885
|
+
const status = isScopeActive(scope)
|
|
886
|
+
? chalk.green("active")
|
|
887
|
+
: chalk.yellow("expired or empty");
|
|
888
|
+
console.log(chalk.dim(` scope: ${scope.name ?? "(unnamed)"} [${status}]`));
|
|
889
|
+
console.log(chalk.dim(` authorized: ${scope.authorizedTargets.join(", ")}`));
|
|
890
|
+
if (scope.excludedTargets && scope.excludedTargets.length > 0) {
|
|
891
|
+
console.log(chalk.dim(` excluded: ${scope.excludedTargets.join(", ")}`));
|
|
892
|
+
}
|
|
893
|
+
if (scope.expiresAt) {
|
|
894
|
+
console.log(chalk.dim(` expires: ${scope.expiresAt}`));
|
|
895
|
+
}
|
|
896
|
+
return true;
|
|
897
|
+
}
|
|
898
|
+
if (sub === "clear" || sub === "reset" || sub === "off") {
|
|
899
|
+
await clearScope();
|
|
900
|
+
console.log(chalk.dim(" engagement scope cleared"));
|
|
901
|
+
return true;
|
|
902
|
+
}
|
|
903
|
+
if (sub === "add") {
|
|
904
|
+
const rest = args.slice(1).join(" ").trim();
|
|
905
|
+
if (!rest) {
|
|
906
|
+
console.log(chalk.dim(" usage: /scope add <target1,target2,...>"));
|
|
907
|
+
return true;
|
|
908
|
+
}
|
|
909
|
+
const targets = rest
|
|
910
|
+
.split(/\s+/)[0]
|
|
911
|
+
.split(",")
|
|
912
|
+
.map((t) => t.trim())
|
|
913
|
+
.filter(Boolean);
|
|
914
|
+
if (targets.length === 0) {
|
|
915
|
+
console.log(chalk.dim(" no targets parsed"));
|
|
916
|
+
return true;
|
|
917
|
+
}
|
|
918
|
+
const scope = await addScopeTargets(targets);
|
|
919
|
+
console.log(chalk.dim(` added ${targets.length} target(s); scope now has ${scope.authorizedTargets.length}`));
|
|
920
|
+
return true;
|
|
921
|
+
}
|
|
922
|
+
if (sub === "new" || sub === "set") {
|
|
923
|
+
const rest = args.slice(1).join(" ").trim();
|
|
924
|
+
if (!rest) {
|
|
925
|
+
console.log(chalk.dim(" usage: /scope new <target1,target2,...> [name=<engagement>] [expires=<iso>]"));
|
|
926
|
+
return true;
|
|
927
|
+
}
|
|
928
|
+
// Parse: first whitespace-delimited token is the targets list,
|
|
929
|
+
// remaining `key=value` pairs configure name/expires/note.
|
|
930
|
+
const tokens = rest.split(/\s+/);
|
|
931
|
+
const targetsRaw = tokens[0] ?? "";
|
|
932
|
+
const targets = targetsRaw
|
|
933
|
+
.split(",")
|
|
934
|
+
.map((t) => t.trim())
|
|
935
|
+
.filter(Boolean);
|
|
936
|
+
if (targets.length === 0) {
|
|
937
|
+
console.log(chalk.dim(" no targets parsed"));
|
|
938
|
+
return true;
|
|
939
|
+
}
|
|
940
|
+
let name;
|
|
941
|
+
let expires;
|
|
942
|
+
let note;
|
|
943
|
+
let exclude;
|
|
944
|
+
for (const token of tokens.slice(1)) {
|
|
945
|
+
const eq = token.indexOf("=");
|
|
946
|
+
if (eq < 0)
|
|
947
|
+
continue;
|
|
948
|
+
const key = token.slice(0, eq).toLowerCase();
|
|
949
|
+
const value = token.slice(eq + 1);
|
|
950
|
+
if (key === "name")
|
|
951
|
+
name = value;
|
|
952
|
+
else if (key === "expires")
|
|
953
|
+
expires = value;
|
|
954
|
+
else if (key === "note")
|
|
955
|
+
note = value;
|
|
956
|
+
else if (key === "exclude")
|
|
957
|
+
exclude = value
|
|
958
|
+
.split(",")
|
|
959
|
+
.map((t) => t.trim())
|
|
960
|
+
.filter(Boolean);
|
|
961
|
+
}
|
|
962
|
+
const scope = {
|
|
963
|
+
name,
|
|
964
|
+
authorizedTargets: targets,
|
|
965
|
+
excludedTargets: exclude,
|
|
966
|
+
authorizationNote: note,
|
|
967
|
+
createdAt: new Date().toISOString(),
|
|
968
|
+
expiresAt: expires,
|
|
969
|
+
};
|
|
970
|
+
await saveScope(scope);
|
|
971
|
+
console.log(chalk.dim(` saved scope${name ? ` "${name}"` : ""} with ${targets.length} target(s)`));
|
|
972
|
+
return true;
|
|
973
|
+
}
|
|
974
|
+
console.log(chalk.dim(" usage: /scope [show|clear|new <targets>|add <targets> [key=value]...]"));
|
|
975
|
+
return true;
|
|
976
|
+
}
|
|
977
|
+
case "/privacy": {
|
|
978
|
+
const sub = (args[0] ?? "status").toLowerCase();
|
|
979
|
+
if (sub === "on" || sub === "enable") {
|
|
980
|
+
updateConfig({ privateMode: true });
|
|
981
|
+
console.log(chalk.dim(" privateMode: " +
|
|
982
|
+
chalk.green("on") +
|
|
983
|
+
" (history not written; in-memory only)"));
|
|
984
|
+
return true;
|
|
985
|
+
}
|
|
986
|
+
if (sub === "off" || sub === "disable") {
|
|
987
|
+
updateConfig({ privateMode: false });
|
|
988
|
+
console.log(chalk.dim(" privateMode: " + chalk.dim("off")));
|
|
989
|
+
return true;
|
|
990
|
+
}
|
|
991
|
+
if (sub === "status" || sub === "show") {
|
|
992
|
+
const cfg = getConfig();
|
|
993
|
+
console.log(chalk.dim(` privateMode: ${cfg.privateMode ? chalk.green("on") : chalk.dim("off")} retention: ${cfg.historyRetentionLimit || "unlimited"}`));
|
|
994
|
+
return true;
|
|
995
|
+
}
|
|
996
|
+
const { clearAllHistory } = await import("./store/history.js");
|
|
997
|
+
const { clearAuditLogs, clearArtifacts } = await import("./store/logs.js");
|
|
998
|
+
if (sub === "clear-history") {
|
|
999
|
+
const r = await clearAllHistory();
|
|
1000
|
+
console.log(chalk.dim(` history cleared (${r.detail || "ok"})`));
|
|
1001
|
+
return true;
|
|
1002
|
+
}
|
|
1003
|
+
if (sub === "clear-logs") {
|
|
1004
|
+
const r = await clearAuditLogs();
|
|
1005
|
+
console.log(chalk.dim(` audit logs cleared (${r.removed} files)`));
|
|
1006
|
+
return true;
|
|
1007
|
+
}
|
|
1008
|
+
if (sub === "clear-artifacts") {
|
|
1009
|
+
const r = await clearArtifacts();
|
|
1010
|
+
console.log(chalk.dim(` artifacts cleared (${r.removed} files)`));
|
|
1011
|
+
return true;
|
|
1012
|
+
}
|
|
1013
|
+
if (sub === "clear-all") {
|
|
1014
|
+
const a = await clearAllHistory();
|
|
1015
|
+
const b = await clearAuditLogs();
|
|
1016
|
+
const c = await clearArtifacts();
|
|
1017
|
+
console.log(chalk.dim(` history (${a.detail || "ok"}); logs (${b.removed}); artifacts (${c.removed})`));
|
|
1018
|
+
return true;
|
|
1019
|
+
}
|
|
1020
|
+
console.log(chalk.dim(" usage: /privacy [status|on|off|clear-history|clear-logs|clear-artifacts|clear-all]"));
|
|
1021
|
+
return true;
|
|
1022
|
+
}
|
|
1023
|
+
case "/freeonly": {
|
|
1024
|
+
const arg = (args[0] ?? "").toLowerCase().trim();
|
|
1025
|
+
if (!arg) {
|
|
1026
|
+
const value = getConfig().freeOnly;
|
|
1027
|
+
console.log(chalk.dim(` freeOnly: ${value ? chalk.green("on") : chalk.dim("off")} (applies when /fallback is on)`));
|
|
1028
|
+
return true;
|
|
1029
|
+
}
|
|
1030
|
+
if (arg === "on" || arg === "true" || arg === "enable") {
|
|
1031
|
+
updateConfig({ freeOnly: true });
|
|
1032
|
+
console.log(chalk.dim(" freeOnly: " + chalk.green("on")));
|
|
1033
|
+
return true;
|
|
1034
|
+
}
|
|
1035
|
+
if (arg === "off" || arg === "false" || arg === "disable") {
|
|
1036
|
+
updateConfig({ freeOnly: false });
|
|
1037
|
+
console.log(chalk.dim(" freeOnly: " + chalk.dim("off")));
|
|
1038
|
+
return true;
|
|
1039
|
+
}
|
|
1040
|
+
console.log(chalk.dim(" usage: /freeonly [on|off]"));
|
|
1041
|
+
return true;
|
|
1042
|
+
}
|
|
1043
|
+
case "/fallback": {
|
|
1044
|
+
const arg = (args[0] ?? "").toLowerCase().trim();
|
|
1045
|
+
if (!arg) {
|
|
1046
|
+
const value = getConfig().providerFallback;
|
|
1047
|
+
console.log(chalk.dim(` fallback: ${value ? chalk.green("on") : chalk.dim("off")} (selected provider/model only when off)`));
|
|
1048
|
+
return true;
|
|
1049
|
+
}
|
|
1050
|
+
if (arg === "on" || arg === "true" || arg === "enable") {
|
|
1051
|
+
updateConfig({ providerFallback: true });
|
|
1052
|
+
console.log(chalk.dim(" fallback: " + chalk.green("on")));
|
|
1053
|
+
return true;
|
|
1054
|
+
}
|
|
1055
|
+
if (arg === "off" || arg === "false" || arg === "disable") {
|
|
1056
|
+
updateConfig({ providerFallback: false });
|
|
1057
|
+
console.log(chalk.dim(" fallback: " + chalk.dim("off")));
|
|
1058
|
+
return true;
|
|
1059
|
+
}
|
|
1060
|
+
console.log(chalk.dim(" usage: /fallback [on|off]"));
|
|
1061
|
+
return true;
|
|
1062
|
+
}
|
|
1063
|
+
case "/output": {
|
|
1064
|
+
const target = args[0] ?? "last";
|
|
1065
|
+
if (target === "list" || target === "ls") {
|
|
1066
|
+
const all = listViewports();
|
|
1067
|
+
if (all.length === 0) {
|
|
1068
|
+
console.log(chalk.dim(" no tool outputs recorded yet"));
|
|
1069
|
+
}
|
|
1070
|
+
else {
|
|
1071
|
+
for (const v of all) {
|
|
1072
|
+
console.log(chalk.dim(` ${v.id} — ${v.toolName} ${v.argsDisplay}${v.artifactPath ? ` (${v.artifactPath})` : ""}`));
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
return true;
|
|
1076
|
+
}
|
|
1077
|
+
const viewport = target === "last" ? getLastViewport() : getViewport(target);
|
|
1078
|
+
if (!viewport) {
|
|
1079
|
+
console.log(chalk.dim(` no viewport: ${target}`));
|
|
1080
|
+
return true;
|
|
1081
|
+
}
|
|
1082
|
+
await toggleViewport(viewport.id);
|
|
1083
|
+
return true;
|
|
1084
|
+
}
|
|
785
1085
|
case "/think":
|
|
786
1086
|
case "/thinking": {
|
|
787
1087
|
const thinking = getLastThinking();
|
|
@@ -793,9 +1093,6 @@ async function handleSlash(line, state) {
|
|
|
793
1093
|
}
|
|
794
1094
|
return true;
|
|
795
1095
|
}
|
|
796
|
-
case "/output":
|
|
797
|
-
await toggleLastToolOutput();
|
|
798
|
-
return true;
|
|
799
1096
|
case "/exit":
|
|
800
1097
|
case "/quit":
|
|
801
1098
|
return false;
|
|
@@ -812,15 +1109,19 @@ async function handleSlash(line, state) {
|
|
|
812
1109
|
}
|
|
813
1110
|
export async function startRepl(options = {}) {
|
|
814
1111
|
const config = getConfig();
|
|
815
|
-
const provider = options.provider
|
|
1112
|
+
const provider = options.provider
|
|
1113
|
+
? assertProvider(options.provider)
|
|
1114
|
+
: config.defaultProvider;
|
|
816
1115
|
const state = {
|
|
817
1116
|
mode: options.mode ?? config.defaultMode,
|
|
818
1117
|
provider,
|
|
819
1118
|
model: options.model ?? getProviderModel(provider),
|
|
820
1119
|
messages: [],
|
|
1120
|
+
session: createSessionPolicy(),
|
|
821
1121
|
};
|
|
822
1122
|
const promptHistory = [];
|
|
823
1123
|
let isReadingPrompt = false;
|
|
1124
|
+
let outputShortcutBusy = false;
|
|
824
1125
|
emitKeypressEvents(input);
|
|
825
1126
|
// Survive stray promise rejections (eg AbortError from a cancelled
|
|
826
1127
|
// SSE reader) without killing the REPL. Anything that ends up here
|
|
@@ -847,16 +1148,33 @@ export async function startRepl(options = {}) {
|
|
|
847
1148
|
const handleThinkingShortcut = () => {
|
|
848
1149
|
process.stdout.write(`\n${renderThinkingToggleMessage()}\n`);
|
|
849
1150
|
};
|
|
1151
|
+
const handleOutputShortcut = async () => {
|
|
1152
|
+
if (outputShortcutBusy)
|
|
1153
|
+
return;
|
|
1154
|
+
outputShortcutBusy = true;
|
|
1155
|
+
try {
|
|
1156
|
+
const v = getLastViewport();
|
|
1157
|
+
if (v) {
|
|
1158
|
+
if (currentAbortController)
|
|
1159
|
+
process.stdout.write("\n");
|
|
1160
|
+
await toggleViewport(v.id);
|
|
1161
|
+
}
|
|
1162
|
+
else {
|
|
1163
|
+
process.stdout.write(chalk.dim("\n (no tool output to expand yet)\n"));
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
finally {
|
|
1167
|
+
outputShortcutBusy = false;
|
|
1168
|
+
}
|
|
1169
|
+
};
|
|
850
1170
|
const handleKeypress = (_sequence, key) => {
|
|
851
|
-
if (key
|
|
1171
|
+
if (isCtrlT(key) && !isReadingPrompt)
|
|
852
1172
|
handleThinkingShortcut();
|
|
853
|
-
if (key
|
|
854
|
-
void
|
|
855
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
856
|
-
process.stderr.write(chalk.dim(`\n output toggle failed: ${message}\n`));
|
|
857
|
-
});
|
|
1173
|
+
if (isCtrlO(key) && !isReadingPrompt) {
|
|
1174
|
+
void handleOutputShortcut();
|
|
858
1175
|
}
|
|
859
|
-
if ((key
|
|
1176
|
+
if ((isEscape(key) || isCtrlC(key)) &&
|
|
1177
|
+
currentAbortController) {
|
|
860
1178
|
currentAbortController.abort();
|
|
861
1179
|
}
|
|
862
1180
|
};
|
|
@@ -903,7 +1221,7 @@ export async function startRepl(options = {}) {
|
|
|
903
1221
|
mode: state.mode,
|
|
904
1222
|
}));
|
|
905
1223
|
console.log(renderSuggestions());
|
|
906
|
-
console.log(chalk.dim(" ESC
|
|
1224
|
+
console.log(chalk.dim(" ESC abort │ Ctrl+C clears input │ Ctrl+T or /think for thinking │ Ctrl+O or /output last for full tool output\n"));
|
|
907
1225
|
// Hint thinking-capable users that the toggle exists. We default it to
|
|
908
1226
|
// off for speed, since on NIM many models route through a much slower
|
|
909
1227
|
// chat-template path when reasoning is enabled.
|
|
@@ -916,22 +1234,15 @@ export async function startRepl(options = {}) {
|
|
|
916
1234
|
chalk.cyan("low|medium|high") +
|
|
917
1235
|
chalk.dim(") to enable it.\n"));
|
|
918
1236
|
}
|
|
919
|
-
// Non-blocking update check
|
|
920
|
-
|
|
921
|
-
checkForUpdateSilent();
|
|
922
|
-
}
|
|
1237
|
+
// Non-blocking update check
|
|
1238
|
+
checkForUpdateSilent();
|
|
923
1239
|
try {
|
|
924
1240
|
while (true) {
|
|
925
1241
|
isReadingPrompt = true;
|
|
926
1242
|
const line = (await readPromptLine({
|
|
927
1243
|
history: promptHistory,
|
|
928
1244
|
onThinkingShortcut: handleThinkingShortcut,
|
|
929
|
-
|
|
930
|
-
void toggleLastToolOutput().catch((error) => {
|
|
931
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
932
|
-
process.stderr.write(chalk.dim(`\n output toggle failed: ${message}\n`));
|
|
933
|
-
});
|
|
934
|
-
},
|
|
1245
|
+
onOutputShortcut: handleOutputShortcut,
|
|
935
1246
|
})).trim();
|
|
936
1247
|
isReadingPrompt = false;
|
|
937
1248
|
if (!line)
|
|
@@ -969,6 +1280,7 @@ export async function startRepl(options = {}) {
|
|
|
969
1280
|
model: state.model,
|
|
970
1281
|
history: state.messages,
|
|
971
1282
|
signal,
|
|
1283
|
+
session: state.session,
|
|
972
1284
|
}));
|
|
973
1285
|
}
|
|
974
1286
|
console.log();
|
|
@@ -992,7 +1304,13 @@ export async function startRepl(options = {}) {
|
|
|
992
1304
|
if (siginfoRegistered)
|
|
993
1305
|
process.off(siginfo, handleThinkingShortcut);
|
|
994
1306
|
if (state.messages.length > 0) {
|
|
995
|
-
|
|
1307
|
+
// Honor `--no-history` and the persistent privateMode setting.
|
|
1308
|
+
// The session.allow set is already in-memory only; saveSession itself
|
|
1309
|
+
// also bails early when privateMode is on, but checking here keeps
|
|
1310
|
+
// intent obvious in the call site.
|
|
1311
|
+
if (!options.noHistory && !getConfig().privateMode) {
|
|
1312
|
+
await saveSession(state.messages, `repl-${new Date().toISOString()}`);
|
|
1313
|
+
}
|
|
996
1314
|
}
|
|
997
1315
|
if (input.isTTY)
|
|
998
1316
|
input.setRawMode(false);
|