@pentoshi/clai 0.5.10 → 0.7.1
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/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 +120 -30
- package/dist/agent/runner.js.map +1 -1
- package/dist/commands/doctor.js +21 -3
- 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/context/manager.d.ts +4 -0
- package/dist/context/manager.js +48 -0
- package/dist/context/manager.js.map +1 -0
- package/dist/index.js +156 -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.js +31 -40
- package/dist/llm/gemini.js.map +1 -1
- package/dist/llm/http.d.ts +21 -0
- package/dist/llm/http.js +140 -1
- package/dist/llm/http.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 +15 -9
- 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/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 +283 -18
- package/dist/repl.js.map +1 -1
- package/dist/safety/classifier.d.ts +5 -1
- package/dist/safety/classifier.js +254 -29
- package/dist/safety/classifier.js.map +1 -1
- package/dist/safety/patterns.d.ts +48 -1
- package/dist/safety/patterns.js +129 -13
- package/dist/safety/patterns.js.map +1 -1
- package/dist/store/config.d.ts +21 -1
- package/dist/store/config.js +28 -7
- 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 +8 -4
- 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 -7
- package/dist/store/project.js.map +1 -1
- package/dist/store/scope.d.ts +29 -0
- package/dist/store/scope.js +113 -0
- package/dist/store/scope.js.map +1 -0
- package/dist/tools/artifacts.d.ts +9 -0
- package/dist/tools/artifacts.js +38 -0
- package/dist/tools/artifacts.js.map +1 -0
- package/dist/tools/fs.d.ts +6 -2
- package/dist/tools/fs.js +95 -17
- package/dist/tools/fs.js.map +1 -1
- package/dist/tools/http.d.ts +5 -2
- package/dist/tools/http.js +177 -8
- 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 +224 -43
- package/dist/tools/registry.js.map +1 -1
- package/dist/tools/shell.d.ts +45 -0
- package/dist/tools/shell.js +430 -12
- 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 +8 -0
- 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/dist/ui/tool-output.d.ts +18 -0
- package/dist/ui/tool-output.js +135 -0
- package/dist/ui/tool-output.js.map +1 -0
- package/package.json +1 -1
package/dist/prompts/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { detectSystem } from
|
|
1
|
+
import { detectSystem } from "../os/detect.js";
|
|
2
2
|
const askPrompt = `You are clai in /ask mode — a cybersecurity and pentesting assistant. Do NOT execute anything.
|
|
3
3
|
OS: {{os}} | Shell: {{shell}} | CWD: {{cwd}}
|
|
4
4
|
|
|
@@ -19,10 +19,11 @@ TOOLS (use EXACT arg names — wrong names = failure):
|
|
|
19
19
|
- fs.list: {"path":"<dir>"} — list directory
|
|
20
20
|
- fs.search: {"pattern":"<regex>","path":"<dir>"} — search file CONTENTS (NOT filenames)
|
|
21
21
|
- pkg.install: {"tool":"<name>"} — install package (only if user asks or command not found)
|
|
22
|
-
- net.scan: {"target":"<ip
|
|
23
|
-
- http.fetch: {"url":"<url>","method":"<optional>","body":"<optional>","headers":{"Key":"Value"}} — HTTP request
|
|
22
|
+
- net.scan: {"target":"<ip|cidr|hostname>","ports":"<optional 80,443,1-1000>","profile":{"scanType":"syn|tcp|udp|ping","serviceDetect":bool,"topPorts":int,"timing":"T0|T1|T2|T3|T4|T5","scripts":["safe-script-name"]},"iOwnThis":bool} — nmap scan. Target/ports/flags are strictly validated (no shell injection). Prefer the structured profile field; the legacy flags string still works but every token must be safe.
|
|
23
|
+
- http.fetch: {"url":"<url>","method":"<optional GET|HEAD|POST|PUT|PATCH|DELETE|OPTIONS>","body":"<optional>","headers":{"Key":"Value"},"maxBytes":<optional>,"iOwnThis":<optional bool>} — HTTP request. GET/HEAD auto-execute against public URLs; non-GET/HEAD and private/loopback/metadata addresses require confirmation; pass iOwnThis=true to allow private targets you own.
|
|
24
24
|
- sysinfo: {} — OS info
|
|
25
25
|
- pentest.recon: {"target":"<ip/host>"} — whois + dig + nmap top-100
|
|
26
|
+
- tool.batch: {"calls":[{"name":"<tool>","args":{...}}, ...],"concurrency":<optional 1-4>} — run up to 8 read-only tools (fs.read/list/search, http.fetch GET/HEAD, sysinfo) in parallel and aggregate their outputs. Use this for independent recon lookups (e.g. resolve a hostname AND read robots.txt) instead of a chain of single calls.
|
|
26
27
|
|
|
27
28
|
FORMAT — one tool per response:
|
|
28
29
|
\`\`\`tool
|
|
@@ -78,13 +79,20 @@ Do NOT: run sysinfo after answering, list home dirs, scan localhost unprompted,
|
|
|
78
79
|
function render(template, values) {
|
|
79
80
|
return Object.entries(values).reduce((current, [key, value]) => current.replaceAll(`{{${key}}}`, value), template);
|
|
80
81
|
}
|
|
82
|
+
/**
|
|
83
|
+
* Internal exports for tests that verify the canonical inline templates
|
|
84
|
+
* have not drifted from the markdown copies in src/prompts/. These are
|
|
85
|
+
* not part of the public API.
|
|
86
|
+
*/
|
|
87
|
+
export const _ASK_TEMPLATE = askPrompt;
|
|
88
|
+
export const _AGENT_TEMPLATE = agentPrompt;
|
|
81
89
|
export function renderAskSystemPrompt() {
|
|
82
90
|
const system = detectSystem();
|
|
83
91
|
return render(askPrompt, {
|
|
84
92
|
os: `${system.osName} ${system.release} ${system.arch}`,
|
|
85
93
|
shell: system.shell,
|
|
86
94
|
cwd: system.cwd,
|
|
87
|
-
tool_list:
|
|
95
|
+
tool_list: "none",
|
|
88
96
|
});
|
|
89
97
|
}
|
|
90
98
|
export function renderAgentSystemPrompt(toolList) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/prompts/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAE/C,MAAM,SAAS,GAAG;;;;;;;;;0LASwK,CAAC;AAE3L,MAAM,WAAW,GAAG
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/prompts/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAE/C,MAAM,SAAS,GAAG;;;;;;;;;0LASwK,CAAC;AAE3L,MAAM,WAAW,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;2KAkEuJ,CAAC;AAE5K,SAAS,MAAM,CAAC,QAAgB,EAAE,MAA8B;IAC9D,OAAO,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,MAAM,CAClC,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,UAAU,CAAC,KAAK,GAAG,IAAI,EAAE,KAAK,CAAC,EAClE,QAAQ,CACT,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,SAAS,CAAC;AACvC,MAAM,CAAC,MAAM,eAAe,GAAG,WAAW,CAAC;AAE3C,MAAM,UAAU,qBAAqB;IACnC,MAAM,MAAM,GAAG,YAAY,EAAE,CAAC;IAC9B,OAAO,MAAM,CAAC,SAAS,EAAE;QACvB,EAAE,EAAE,GAAG,MAAM,CAAC,MAAM,IAAI,MAAM,CAAC,OAAO,IAAI,MAAM,CAAC,IAAI,EAAE;QACvD,KAAK,EAAE,MAAM,CAAC,KAAK;QACnB,GAAG,EAAE,MAAM,CAAC,GAAG;QACf,SAAS,EAAE,MAAM;KAClB,CAAC,CAAC;AACL,CAAC;AAED,MAAM,UAAU,uBAAuB,CAAC,QAAgB;IACtD,MAAM,MAAM,GAAG,YAAY,EAAE,CAAC;IAC9B,OAAO,MAAM,CAAC,WAAW,EAAE;QACzB,EAAE,EAAE,GAAG,MAAM,CAAC,MAAM,IAAI,MAAM,CAAC,OAAO,IAAI,MAAM,CAAC,IAAI,EAAE;QACvD,KAAK,EAAE,MAAM,CAAC,KAAK;QACnB,GAAG,EAAE,MAAM,CAAC,GAAG;QACf,SAAS,EAAE,QAAQ;KACpB,CAAC,CAAC;AACL,CAAC"}
|
package/dist/repl.d.ts
CHANGED
package/dist/repl.js
CHANGED
|
@@ -3,17 +3,19 @@ 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 { getLastViewport, getViewport, listViewports, toggleViewport, } from "./ui/output-pane.js";
|
|
18
|
+
import { compactMessages, estimateMessagesTokens, } from "./agent/context-manager.js";
|
|
17
19
|
const slashCommands = [
|
|
18
20
|
{ command: "/ask", description: "switch to ask mode" },
|
|
19
21
|
{ command: "/agent", description: "switch to agent mode" },
|
|
@@ -49,9 +51,40 @@ const slashCommands = [
|
|
|
49
51
|
{ command: "/history", description: "show past sessions" },
|
|
50
52
|
{ command: "/save", usage: "<name>", description: "save session" },
|
|
51
53
|
{ command: "/cwd", usage: "<path>", description: "change working directory" },
|
|
52
|
-
{
|
|
54
|
+
{
|
|
55
|
+
command: "/allow",
|
|
56
|
+
usage: "<tool>|list",
|
|
57
|
+
description: "allow a tool for this session (not persisted)",
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
command: "/disallow",
|
|
61
|
+
usage: "<tool>",
|
|
62
|
+
description: "revoke a session allow",
|
|
63
|
+
},
|
|
53
64
|
{ command: "/think", description: "show thinking from last response" },
|
|
54
65
|
{ command: "/thinking", description: "alias for /think" },
|
|
66
|
+
{
|
|
67
|
+
command: "/output",
|
|
68
|
+
usage: "[last|<id>|list]",
|
|
69
|
+
description: "toggle full tool output (same as Ctrl+O)",
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
command: "/freeonly",
|
|
73
|
+
usage: "[on|off]",
|
|
74
|
+
description: "skip paid providers in fallback (off by default)",
|
|
75
|
+
},
|
|
76
|
+
{ command: "/compact", description: "compact session history now" },
|
|
77
|
+
{ command: "/context", description: "show estimated context size" },
|
|
78
|
+
{
|
|
79
|
+
command: "/scope",
|
|
80
|
+
usage: "[show|clear|new <targets>]",
|
|
81
|
+
description: "manage pentest engagement scope",
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
command: "/privacy",
|
|
85
|
+
usage: "[status|clear-history|clear-logs|clear-artifacts|clear-all|on|off]",
|
|
86
|
+
description: "control retention and private mode (in-memory only)",
|
|
87
|
+
},
|
|
55
88
|
{ command: "/update", description: "check for updates" },
|
|
56
89
|
{ command: "/exit", description: "quit" },
|
|
57
90
|
{ command: "/quit", description: "alias for /exit" },
|
|
@@ -184,7 +217,9 @@ function isAbortLikeError(error) {
|
|
|
184
217
|
return false;
|
|
185
218
|
}
|
|
186
219
|
function slashCommandLabel(command) {
|
|
187
|
-
return command.usage
|
|
220
|
+
return command.usage
|
|
221
|
+
? `${command.command} ${command.usage}`
|
|
222
|
+
: command.command;
|
|
188
223
|
}
|
|
189
224
|
function slashCommandFilter(line) {
|
|
190
225
|
if (!line.startsWith("/") || /\s/.test(line))
|
|
@@ -344,7 +379,8 @@ async function readPromptLine(options) {
|
|
|
344
379
|
if (key.name === "up") {
|
|
345
380
|
if (menu.visible && menu.suggestions.length > 0) {
|
|
346
381
|
selectedIndex =
|
|
347
|
-
(selectedIndex - 1 + menu.suggestions.length) %
|
|
382
|
+
(selectedIndex - 1 + menu.suggestions.length) %
|
|
383
|
+
menu.suggestions.length;
|
|
348
384
|
refresh();
|
|
349
385
|
return;
|
|
350
386
|
}
|
|
@@ -509,7 +545,8 @@ async function withAbortableInput(run) {
|
|
|
509
545
|
return await run(ac.signal);
|
|
510
546
|
}
|
|
511
547
|
catch (error) {
|
|
512
|
-
if (ac.signal.aborted ||
|
|
548
|
+
if (ac.signal.aborted ||
|
|
549
|
+
(error instanceof Error && error.name === "AbortError")) {
|
|
513
550
|
throw new AbortRunError();
|
|
514
551
|
}
|
|
515
552
|
throw error;
|
|
@@ -528,7 +565,8 @@ function help() {
|
|
|
528
565
|
return ` ${chalk.cyan(label)}${chalk.dim(command.description)}`;
|
|
529
566
|
})
|
|
530
567
|
.join("\n");
|
|
531
|
-
return lines +
|
|
568
|
+
return (lines +
|
|
569
|
+
chalk.dim("\n\n ESC abort │ Ctrl+C clears input (twice to exit) │ Ctrl+T toggle thinking │ Ctrl+O / /output last toggle tool output"));
|
|
532
570
|
}
|
|
533
571
|
async function pickModelInteractively(provider, currentModel) {
|
|
534
572
|
const models = knownModels[provider] ?? [];
|
|
@@ -763,17 +801,224 @@ async function handleSlash(line, state) {
|
|
|
763
801
|
}
|
|
764
802
|
case "/allow": {
|
|
765
803
|
const tool = args[0];
|
|
766
|
-
if (!tool)
|
|
767
|
-
console.log(chalk.dim("usage: /allow <tool
|
|
804
|
+
if (!tool) {
|
|
805
|
+
console.log(chalk.dim("usage: /allow <tool>|list"));
|
|
806
|
+
return true;
|
|
807
|
+
}
|
|
808
|
+
if (tool === "list" || tool === "ls") {
|
|
809
|
+
if (state.session.allow.size === 0) {
|
|
810
|
+
console.log(chalk.dim(" no session allows"));
|
|
811
|
+
}
|
|
812
|
+
else {
|
|
813
|
+
for (const allowed of state.session.allow) {
|
|
814
|
+
console.log(chalk.dim(` ✓ ${allowed}`));
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
return true;
|
|
818
|
+
}
|
|
819
|
+
state.session.allow.add(tool);
|
|
820
|
+
console.log(chalk.dim(` allowed ${tool} for this session only ✓`));
|
|
821
|
+
return true;
|
|
822
|
+
}
|
|
823
|
+
case "/disallow": {
|
|
824
|
+
const tool = args[0];
|
|
825
|
+
if (!tool) {
|
|
826
|
+
console.log(chalk.dim("usage: /disallow <tool>"));
|
|
827
|
+
return true;
|
|
828
|
+
}
|
|
829
|
+
if (state.session.allow.delete(tool)) {
|
|
830
|
+
console.log(chalk.dim(` revoked ${tool} ✓`));
|
|
831
|
+
}
|
|
768
832
|
else {
|
|
769
|
-
|
|
770
|
-
updateConfig({
|
|
771
|
-
allowAlwaysTools: Array.from(new Set([...config.allowAlwaysTools, tool])),
|
|
772
|
-
});
|
|
773
|
-
console.log(chalk.dim(` allowed ${tool} ✓`));
|
|
833
|
+
console.log(chalk.dim(` ${tool} was not in the session allow list`));
|
|
774
834
|
}
|
|
775
835
|
return true;
|
|
776
836
|
}
|
|
837
|
+
case "/context": {
|
|
838
|
+
const tokens = estimateMessagesTokens(state.messages);
|
|
839
|
+
console.log(chalk.dim(` ${state.messages.length} message(s), ~${tokens.toLocaleString()} tokens estimated`));
|
|
840
|
+
return true;
|
|
841
|
+
}
|
|
842
|
+
case "/compact": {
|
|
843
|
+
const before = state.messages.length;
|
|
844
|
+
const compacted = compactMessages(state.messages, { budgetTokens: 0 });
|
|
845
|
+
state.messages.splice(0, state.messages.length, ...compacted);
|
|
846
|
+
console.log(chalk.dim(` compacted ${before} → ${state.messages.length} messages (~${estimateMessagesTokens(state.messages).toLocaleString()} tokens)`));
|
|
847
|
+
return true;
|
|
848
|
+
}
|
|
849
|
+
case "/scope": {
|
|
850
|
+
const sub = (args[0] ?? "show").toLowerCase();
|
|
851
|
+
const { loadScope, saveScope, clearScope, isScopeActive, getScopePath, resetScopeCache, } = await import("./store/scope.js");
|
|
852
|
+
if (sub === "show" || sub === "ls" || sub === "list") {
|
|
853
|
+
resetScopeCache();
|
|
854
|
+
const scope = await loadScope();
|
|
855
|
+
if (!scope) {
|
|
856
|
+
console.log(chalk.dim(" no engagement scope configured"));
|
|
857
|
+
console.log(chalk.dim(` expected at: ${getScopePath()}`));
|
|
858
|
+
console.log(chalk.dim(" create one with: /scope new domain1,domain2 [--name <eng>] or `clai scope new --targets ...`"));
|
|
859
|
+
return true;
|
|
860
|
+
}
|
|
861
|
+
const status = isScopeActive(scope)
|
|
862
|
+
? chalk.green("active")
|
|
863
|
+
: chalk.yellow("expired or empty");
|
|
864
|
+
console.log(chalk.dim(` scope: ${scope.name ?? "(unnamed)"} [${status}]`));
|
|
865
|
+
console.log(chalk.dim(` authorized: ${scope.authorizedTargets.join(", ")}`));
|
|
866
|
+
if (scope.excludedTargets && scope.excludedTargets.length > 0) {
|
|
867
|
+
console.log(chalk.dim(` excluded: ${scope.excludedTargets.join(", ")}`));
|
|
868
|
+
}
|
|
869
|
+
if (scope.expiresAt) {
|
|
870
|
+
console.log(chalk.dim(` expires: ${scope.expiresAt}`));
|
|
871
|
+
}
|
|
872
|
+
return true;
|
|
873
|
+
}
|
|
874
|
+
if (sub === "clear" || sub === "reset" || sub === "off") {
|
|
875
|
+
await clearScope();
|
|
876
|
+
console.log(chalk.dim(" engagement scope cleared"));
|
|
877
|
+
return true;
|
|
878
|
+
}
|
|
879
|
+
if (sub === "new" || sub === "set" || sub === "add") {
|
|
880
|
+
const rest = args.slice(1).join(" ").trim();
|
|
881
|
+
if (!rest) {
|
|
882
|
+
console.log(chalk.dim(" usage: /scope new <target1,target2,...> [name=<engagement>] [expires=<iso>]"));
|
|
883
|
+
return true;
|
|
884
|
+
}
|
|
885
|
+
// Parse: first whitespace-delimited token is the targets list,
|
|
886
|
+
// remaining `key=value` pairs configure name/expires/note.
|
|
887
|
+
const tokens = rest.split(/\s+/);
|
|
888
|
+
const targetsRaw = tokens[0] ?? "";
|
|
889
|
+
const targets = targetsRaw
|
|
890
|
+
.split(",")
|
|
891
|
+
.map((t) => t.trim())
|
|
892
|
+
.filter(Boolean);
|
|
893
|
+
if (targets.length === 0) {
|
|
894
|
+
console.log(chalk.dim(" no targets parsed"));
|
|
895
|
+
return true;
|
|
896
|
+
}
|
|
897
|
+
let name;
|
|
898
|
+
let expires;
|
|
899
|
+
let note;
|
|
900
|
+
let exclude;
|
|
901
|
+
for (const token of tokens.slice(1)) {
|
|
902
|
+
const eq = token.indexOf("=");
|
|
903
|
+
if (eq < 0)
|
|
904
|
+
continue;
|
|
905
|
+
const key = token.slice(0, eq).toLowerCase();
|
|
906
|
+
const value = token.slice(eq + 1);
|
|
907
|
+
if (key === "name")
|
|
908
|
+
name = value;
|
|
909
|
+
else if (key === "expires")
|
|
910
|
+
expires = value;
|
|
911
|
+
else if (key === "note")
|
|
912
|
+
note = value;
|
|
913
|
+
else if (key === "exclude")
|
|
914
|
+
exclude = value
|
|
915
|
+
.split(",")
|
|
916
|
+
.map((t) => t.trim())
|
|
917
|
+
.filter(Boolean);
|
|
918
|
+
}
|
|
919
|
+
const scope = {
|
|
920
|
+
name,
|
|
921
|
+
authorizedTargets: targets,
|
|
922
|
+
excludedTargets: exclude,
|
|
923
|
+
authorizationNote: note,
|
|
924
|
+
createdAt: new Date().toISOString(),
|
|
925
|
+
expiresAt: expires,
|
|
926
|
+
};
|
|
927
|
+
await saveScope(scope);
|
|
928
|
+
console.log(chalk.dim(` saved scope${name ? ` "${name}"` : ""} with ${targets.length} target(s)`));
|
|
929
|
+
return true;
|
|
930
|
+
}
|
|
931
|
+
console.log(chalk.dim(" usage: /scope [show|clear|new <targets> [key=value]...]"));
|
|
932
|
+
return true;
|
|
933
|
+
}
|
|
934
|
+
case "/privacy": {
|
|
935
|
+
const sub = (args[0] ?? "status").toLowerCase();
|
|
936
|
+
if (sub === "on" || sub === "enable") {
|
|
937
|
+
updateConfig({ privateMode: true });
|
|
938
|
+
console.log(chalk.dim(" privateMode: " +
|
|
939
|
+
chalk.green("on") +
|
|
940
|
+
" (history not written; in-memory only)"));
|
|
941
|
+
return true;
|
|
942
|
+
}
|
|
943
|
+
if (sub === "off" || sub === "disable") {
|
|
944
|
+
updateConfig({ privateMode: false });
|
|
945
|
+
console.log(chalk.dim(" privateMode: " + chalk.dim("off")));
|
|
946
|
+
return true;
|
|
947
|
+
}
|
|
948
|
+
if (sub === "status" || sub === "show") {
|
|
949
|
+
const cfg = getConfig();
|
|
950
|
+
console.log(chalk.dim(` privateMode: ${cfg.privateMode ? chalk.green("on") : chalk.dim("off")} retention: ${cfg.historyRetentionLimit || "unlimited"}`));
|
|
951
|
+
return true;
|
|
952
|
+
}
|
|
953
|
+
const { clearAllHistory } = await import("./store/history.js");
|
|
954
|
+
const { clearAuditLogs, clearArtifacts } = await import("./store/logs.js");
|
|
955
|
+
if (sub === "clear-history") {
|
|
956
|
+
const r = await clearAllHistory();
|
|
957
|
+
console.log(chalk.dim(` history cleared (${r.detail || "ok"})`));
|
|
958
|
+
return true;
|
|
959
|
+
}
|
|
960
|
+
if (sub === "clear-logs") {
|
|
961
|
+
const r = await clearAuditLogs();
|
|
962
|
+
console.log(chalk.dim(` audit logs cleared (${r.removed} files)`));
|
|
963
|
+
return true;
|
|
964
|
+
}
|
|
965
|
+
if (sub === "clear-artifacts") {
|
|
966
|
+
const r = await clearArtifacts();
|
|
967
|
+
console.log(chalk.dim(` artifacts cleared (${r.removed} files)`));
|
|
968
|
+
return true;
|
|
969
|
+
}
|
|
970
|
+
if (sub === "clear-all") {
|
|
971
|
+
const a = await clearAllHistory();
|
|
972
|
+
const b = await clearAuditLogs();
|
|
973
|
+
const c = await clearArtifacts();
|
|
974
|
+
console.log(chalk.dim(` history (${a.detail || "ok"}); logs (${b.removed}); artifacts (${c.removed})`));
|
|
975
|
+
return true;
|
|
976
|
+
}
|
|
977
|
+
console.log(chalk.dim(" usage: /privacy [status|on|off|clear-history|clear-logs|clear-artifacts|clear-all]"));
|
|
978
|
+
return true;
|
|
979
|
+
}
|
|
980
|
+
case "/freeonly": {
|
|
981
|
+
const arg = (args[0] ?? "").toLowerCase().trim();
|
|
982
|
+
if (!arg) {
|
|
983
|
+
const value = getConfig().freeOnly;
|
|
984
|
+
console.log(chalk.dim(` freeOnly: ${value ? chalk.green("on") : chalk.dim("off")} (paid providers ${value ? "skipped" : "in fallback"})`));
|
|
985
|
+
return true;
|
|
986
|
+
}
|
|
987
|
+
if (arg === "on" || arg === "true" || arg === "enable") {
|
|
988
|
+
updateConfig({ freeOnly: true });
|
|
989
|
+
console.log(chalk.dim(" freeOnly: " + chalk.green("on")));
|
|
990
|
+
return true;
|
|
991
|
+
}
|
|
992
|
+
if (arg === "off" || arg === "false" || arg === "disable") {
|
|
993
|
+
updateConfig({ freeOnly: false });
|
|
994
|
+
console.log(chalk.dim(" freeOnly: " + chalk.dim("off")));
|
|
995
|
+
return true;
|
|
996
|
+
}
|
|
997
|
+
console.log(chalk.dim(" usage: /freeonly [on|off]"));
|
|
998
|
+
return true;
|
|
999
|
+
}
|
|
1000
|
+
case "/output": {
|
|
1001
|
+
const target = args[0] ?? "last";
|
|
1002
|
+
if (target === "list" || target === "ls") {
|
|
1003
|
+
const all = listViewports();
|
|
1004
|
+
if (all.length === 0) {
|
|
1005
|
+
console.log(chalk.dim(" no tool outputs recorded yet"));
|
|
1006
|
+
}
|
|
1007
|
+
else {
|
|
1008
|
+
for (const v of all) {
|
|
1009
|
+
console.log(chalk.dim(` ${v.id} — ${v.toolName} ${v.argsDisplay}${v.artifactPath ? ` (${v.artifactPath})` : ""}`));
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
return true;
|
|
1013
|
+
}
|
|
1014
|
+
const viewport = target === "last" ? getLastViewport() : getViewport(target);
|
|
1015
|
+
if (!viewport) {
|
|
1016
|
+
console.log(chalk.dim(` no viewport: ${target}`));
|
|
1017
|
+
return true;
|
|
1018
|
+
}
|
|
1019
|
+
await toggleViewport(viewport.id);
|
|
1020
|
+
return true;
|
|
1021
|
+
}
|
|
777
1022
|
case "/think":
|
|
778
1023
|
case "/thinking": {
|
|
779
1024
|
const thinking = getLastThinking();
|
|
@@ -801,12 +1046,15 @@ async function handleSlash(line, state) {
|
|
|
801
1046
|
}
|
|
802
1047
|
export async function startRepl(options = {}) {
|
|
803
1048
|
const config = getConfig();
|
|
804
|
-
const provider = options.provider
|
|
1049
|
+
const provider = options.provider
|
|
1050
|
+
? assertProvider(options.provider)
|
|
1051
|
+
: config.defaultProvider;
|
|
805
1052
|
const state = {
|
|
806
1053
|
mode: options.mode ?? config.defaultMode,
|
|
807
1054
|
provider,
|
|
808
1055
|
model: options.model ?? getProviderModel(provider),
|
|
809
1056
|
messages: [],
|
|
1057
|
+
session: createSessionPolicy(),
|
|
810
1058
|
};
|
|
811
1059
|
const promptHistory = [];
|
|
812
1060
|
let isReadingPrompt = false;
|
|
@@ -839,7 +1087,17 @@ export async function startRepl(options = {}) {
|
|
|
839
1087
|
const handleKeypress = (_sequence, key) => {
|
|
840
1088
|
if (key.ctrl && key.name === "t" && !isReadingPrompt)
|
|
841
1089
|
handleThinkingShortcut();
|
|
842
|
-
if (
|
|
1090
|
+
if (key.ctrl && key.name === "o" && !isReadingPrompt) {
|
|
1091
|
+
const v = getLastViewport();
|
|
1092
|
+
if (v) {
|
|
1093
|
+
void toggleViewport(v.id);
|
|
1094
|
+
}
|
|
1095
|
+
else {
|
|
1096
|
+
process.stdout.write(chalk.dim("\n (no tool output to expand yet)\n"));
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
if ((key.name === "escape" || (key.ctrl && key.name === "c")) &&
|
|
1100
|
+
currentAbortController) {
|
|
843
1101
|
currentAbortController.abort();
|
|
844
1102
|
}
|
|
845
1103
|
};
|
|
@@ -886,7 +1144,7 @@ export async function startRepl(options = {}) {
|
|
|
886
1144
|
mode: state.mode,
|
|
887
1145
|
}));
|
|
888
1146
|
console.log(renderSuggestions());
|
|
889
|
-
console.log(chalk.dim(" ESC
|
|
1147
|
+
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"));
|
|
890
1148
|
// Hint thinking-capable users that the toggle exists. We default it to
|
|
891
1149
|
// off for speed, since on NIM many models route through a much slower
|
|
892
1150
|
// chat-template path when reasoning is enabled.
|
|
@@ -944,6 +1202,7 @@ export async function startRepl(options = {}) {
|
|
|
944
1202
|
model: state.model,
|
|
945
1203
|
history: state.messages,
|
|
946
1204
|
signal,
|
|
1205
|
+
session: state.session,
|
|
947
1206
|
}));
|
|
948
1207
|
}
|
|
949
1208
|
console.log();
|
|
@@ -967,7 +1226,13 @@ export async function startRepl(options = {}) {
|
|
|
967
1226
|
if (siginfoRegistered)
|
|
968
1227
|
process.off(siginfo, handleThinkingShortcut);
|
|
969
1228
|
if (state.messages.length > 0) {
|
|
970
|
-
|
|
1229
|
+
// Honor `--no-history` and the persistent privateMode setting.
|
|
1230
|
+
// The session.allow set is already in-memory only; saveSession itself
|
|
1231
|
+
// also bails early when privateMode is on, but checking here keeps
|
|
1232
|
+
// intent obvious in the call site.
|
|
1233
|
+
if (!options.noHistory && !getConfig().privateMode) {
|
|
1234
|
+
await saveSession(state.messages, `repl-${new Date().toISOString()}`);
|
|
1235
|
+
}
|
|
971
1236
|
}
|
|
972
1237
|
if (input.isTTY)
|
|
973
1238
|
input.setRawMode(false);
|