@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.
Files changed (122) hide show
  1. package/dist/agent/context-manager.d.ts +27 -0
  2. package/dist/agent/context-manager.js +75 -0
  3. package/dist/agent/context-manager.js.map +1 -0
  4. package/dist/agent/runner.d.ts +21 -1
  5. package/dist/agent/runner.js +120 -30
  6. package/dist/agent/runner.js.map +1 -1
  7. package/dist/commands/doctor.js +21 -3
  8. package/dist/commands/doctor.js.map +1 -1
  9. package/dist/commands/update.js +11 -2
  10. package/dist/commands/update.js.map +1 -1
  11. package/dist/context/manager.d.ts +4 -0
  12. package/dist/context/manager.js +48 -0
  13. package/dist/context/manager.js.map +1 -0
  14. package/dist/index.js +156 -5
  15. package/dist/index.js.map +1 -1
  16. package/dist/llm/anthropic.js +29 -38
  17. package/dist/llm/anthropic.js.map +1 -1
  18. package/dist/llm/gemini.js +31 -40
  19. package/dist/llm/gemini.js.map +1 -1
  20. package/dist/llm/http.d.ts +21 -0
  21. package/dist/llm/http.js +140 -1
  22. package/dist/llm/http.js.map +1 -1
  23. package/dist/llm/ollama.js +18 -27
  24. package/dist/llm/ollama.js.map +1 -1
  25. package/dist/llm/router.d.ts +7 -0
  26. package/dist/llm/router.js +15 -9
  27. package/dist/llm/router.js.map +1 -1
  28. package/dist/modes/agent.d.ts +4 -2
  29. package/dist/modes/agent.js +2 -2
  30. package/dist/modes/agent.js.map +1 -1
  31. package/dist/os/pkgmgr.d.ts +7 -1
  32. package/dist/os/pkgmgr.js +97 -18
  33. package/dist/os/pkgmgr.js.map +1 -1
  34. package/dist/prompts/index.d.ts +7 -0
  35. package/dist/prompts/index.js +12 -4
  36. package/dist/prompts/index.js.map +1 -1
  37. package/dist/repl.d.ts +1 -0
  38. package/dist/repl.js +283 -18
  39. package/dist/repl.js.map +1 -1
  40. package/dist/safety/classifier.d.ts +5 -1
  41. package/dist/safety/classifier.js +254 -29
  42. package/dist/safety/classifier.js.map +1 -1
  43. package/dist/safety/patterns.d.ts +48 -1
  44. package/dist/safety/patterns.js +129 -13
  45. package/dist/safety/patterns.js.map +1 -1
  46. package/dist/store/config.d.ts +21 -1
  47. package/dist/store/config.js +28 -7
  48. package/dist/store/config.js.map +1 -1
  49. package/dist/store/history.d.ts +9 -0
  50. package/dist/store/history.js +58 -1
  51. package/dist/store/history.js.map +1 -1
  52. package/dist/store/keys.d.ts +2 -1
  53. package/dist/store/keys.js +8 -4
  54. package/dist/store/keys.js.map +1 -1
  55. package/dist/store/logs.d.ts +7 -0
  56. package/dist/store/logs.js +39 -1
  57. package/dist/store/logs.js.map +1 -1
  58. package/dist/store/project.d.ts +1 -0
  59. package/dist/store/project.js +34 -7
  60. package/dist/store/project.js.map +1 -1
  61. package/dist/store/scope.d.ts +29 -0
  62. package/dist/store/scope.js +113 -0
  63. package/dist/store/scope.js.map +1 -0
  64. package/dist/tools/artifacts.d.ts +9 -0
  65. package/dist/tools/artifacts.js +38 -0
  66. package/dist/tools/artifacts.js.map +1 -0
  67. package/dist/tools/fs.d.ts +6 -2
  68. package/dist/tools/fs.js +95 -17
  69. package/dist/tools/fs.js.map +1 -1
  70. package/dist/tools/http.d.ts +5 -2
  71. package/dist/tools/http.js +177 -8
  72. package/dist/tools/http.js.map +1 -1
  73. package/dist/tools/policies/output-policy.d.ts +13 -0
  74. package/dist/tools/policies/output-policy.js +56 -0
  75. package/dist/tools/policies/output-policy.js.map +1 -0
  76. package/dist/tools/reducers/ffuf.d.ts +6 -0
  77. package/dist/tools/reducers/ffuf.js +74 -0
  78. package/dist/tools/reducers/ffuf.js.map +1 -0
  79. package/dist/tools/reducers/generic.d.ts +2 -0
  80. package/dist/tools/reducers/generic.js +60 -0
  81. package/dist/tools/reducers/generic.js.map +1 -0
  82. package/dist/tools/reducers/gobuster.d.ts +2 -0
  83. package/dist/tools/reducers/gobuster.js +36 -0
  84. package/dist/tools/reducers/gobuster.js.map +1 -0
  85. package/dist/tools/reducers/httpx.d.ts +2 -0
  86. package/dist/tools/reducers/httpx.js +38 -0
  87. package/dist/tools/reducers/httpx.js.map +1 -0
  88. package/dist/tools/reducers/nmap.d.ts +7 -0
  89. package/dist/tools/reducers/nmap.js +82 -0
  90. package/dist/tools/reducers/nmap.js.map +1 -0
  91. package/dist/tools/reducers/nuclei.d.ts +2 -0
  92. package/dist/tools/reducers/nuclei.js +51 -0
  93. package/dist/tools/reducers/nuclei.js.map +1 -0
  94. package/dist/tools/reducers/sqlmap.d.ts +2 -0
  95. package/dist/tools/reducers/sqlmap.js +39 -0
  96. package/dist/tools/reducers/sqlmap.js.map +1 -0
  97. package/dist/tools/reducers/subdomains.d.ts +6 -0
  98. package/dist/tools/reducers/subdomains.js +31 -0
  99. package/dist/tools/reducers/subdomains.js.map +1 -0
  100. package/dist/tools/reducers/types.d.ts +14 -0
  101. package/dist/tools/reducers/types.js +2 -0
  102. package/dist/tools/reducers/types.js.map +1 -0
  103. package/dist/tools/registry.d.ts +1 -1
  104. package/dist/tools/registry.js +224 -43
  105. package/dist/tools/registry.js.map +1 -1
  106. package/dist/tools/shell.d.ts +45 -0
  107. package/dist/tools/shell.js +430 -12
  108. package/dist/tools/shell.js.map +1 -1
  109. package/dist/tools/validate.d.ts +37 -0
  110. package/dist/tools/validate.js +144 -0
  111. package/dist/tools/validate.js.map +1 -0
  112. package/dist/types.d.ts +8 -0
  113. package/dist/ui/keys.d.ts +21 -0
  114. package/dist/ui/keys.js +13 -0
  115. package/dist/ui/keys.js.map +1 -0
  116. package/dist/ui/output-pane.d.ts +31 -0
  117. package/dist/ui/output-pane.js +81 -0
  118. package/dist/ui/output-pane.js.map +1 -0
  119. package/dist/ui/tool-output.d.ts +18 -0
  120. package/dist/ui/tool-output.js +135 -0
  121. package/dist/ui/tool-output.js.map +1 -0
  122. package/package.json +1 -1
@@ -1,4 +1,4 @@
1
- import { detectSystem } from '../os/detect.js';
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/host>","ports":"<optional>"} — nmap scan
23
- - http.fetch: {"url":"<url>","method":"<optional>","body":"<optional>","headers":{"Key":"Value"}} — HTTP request with optional headers
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: 'none',
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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;2KAiEuJ,CAAC;AAG5K,SAAS,MAAM,CAAC,QAAgB,EAAE,MAA8B;IAC9D,OAAO,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,UAAU,CAAC,KAAK,GAAG,IAAI,EAAE,KAAK,CAAC,EAAE,QAAQ,CAAC,CAAC;AACrH,CAAC;AAED,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"}
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
@@ -3,6 +3,7 @@ export interface ReplOptions {
3
3
  mode?: Mode | undefined;
4
4
  provider?: ProviderId | undefined;
5
5
  model?: string | undefined;
6
+ noHistory?: boolean | undefined;
6
7
  }
7
8
  export interface SlashCommand {
8
9
  command: string;
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
- { command: "/allow", usage: "<tool>", description: "allow a tool for session" },
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 ? `${command.command} ${command.usage}` : command.command;
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) % 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 || (error instanceof Error && error.name === "AbortError")) {
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 + chalk.dim("\n\n ESC abort │ Ctrl+C clears input (twice to exit) │ Ctrl+T toggle thinking");
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
- const config = getConfig();
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 ? assertProvider(options.provider) : config.defaultProvider;
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 ((key.name === "escape" || (key.ctrl && key.name === "c")) && currentAbortController) {
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 to abort a response │ Ctrl+C clears input (twice to exit) │ Ctrl+T or /think for thinking\n"));
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
- await saveSession(state.messages, `repl-${new Date().toISOString()}`);
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);