@pentoshi/clai 3.6.0 → 3.7.3

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 (322) hide show
  1. package/dist/agent/compaction-summary.d.ts +17 -0
  2. package/dist/agent/compaction-summary.js +64 -0
  3. package/dist/agent/compaction-summary.js.map +1 -0
  4. package/dist/agent/confirm-port.d.ts +3 -1
  5. package/dist/agent/confirm-port.js +5 -1
  6. package/dist/agent/confirm-port.js.map +1 -1
  7. package/dist/agent/context-manager.js +45 -22
  8. package/dist/agent/context-manager.js.map +1 -1
  9. package/dist/agent/events.d.ts +6 -0
  10. package/dist/agent/evidence-governor.d.ts +37 -0
  11. package/dist/agent/evidence-governor.js +70 -0
  12. package/dist/agent/evidence-governor.js.map +1 -0
  13. package/dist/agent/loop-guard.d.ts +29 -11
  14. package/dist/agent/loop-guard.js +93 -39
  15. package/dist/agent/loop-guard.js.map +1 -1
  16. package/dist/agent/must-continue.d.ts +56 -0
  17. package/dist/agent/must-continue.js +240 -0
  18. package/dist/agent/must-continue.js.map +1 -0
  19. package/dist/agent/outcomes.d.ts +101 -0
  20. package/dist/agent/outcomes.js +359 -0
  21. package/dist/agent/outcomes.js.map +1 -0
  22. package/dist/agent/plan-decision.d.ts +33 -0
  23. package/dist/agent/plan-decision.js +104 -0
  24. package/dist/agent/plan-decision.js.map +1 -0
  25. package/dist/agent/plan-tool.d.ts +12 -0
  26. package/dist/agent/plan-tool.js +312 -104
  27. package/dist/agent/plan-tool.js.map +1 -1
  28. package/dist/agent/project-root.js +3 -0
  29. package/dist/agent/project-root.js.map +1 -1
  30. package/dist/agent/prompt-composer.d.ts +21 -0
  31. package/dist/agent/prompt-composer.js +46 -0
  32. package/dist/agent/prompt-composer.js.map +1 -0
  33. package/dist/agent/runner.d.ts +11 -1
  34. package/dist/agent/runner.js +1226 -971
  35. package/dist/agent/runner.js.map +1 -1
  36. package/dist/agent/scope-context.d.ts +3 -0
  37. package/dist/agent/scope-context.js +24 -0
  38. package/dist/agent/scope-context.js.map +1 -0
  39. package/dist/agent/scratch-write.d.ts +4 -0
  40. package/dist/agent/scratch-write.js +54 -0
  41. package/dist/agent/scratch-write.js.map +1 -0
  42. package/dist/agent/session-policy.d.ts +7 -0
  43. package/dist/agent/session-policy.js +61 -0
  44. package/dist/agent/session-policy.js.map +1 -1
  45. package/dist/agent/session-state.d.ts +41 -0
  46. package/dist/agent/session-state.js +107 -0
  47. package/dist/agent/session-state.js.map +1 -0
  48. package/dist/agent/step-budget.d.ts +14 -0
  49. package/dist/agent/step-budget.js +26 -0
  50. package/dist/agent/step-budget.js.map +1 -0
  51. package/dist/agent/task-analyzer.d.ts +12 -3
  52. package/dist/agent/task-analyzer.js +263 -23
  53. package/dist/agent/task-analyzer.js.map +1 -1
  54. package/dist/agent/task-evidence.d.ts +111 -29
  55. package/dist/agent/task-evidence.js +519 -91
  56. package/dist/agent/task-evidence.js.map +1 -1
  57. package/dist/agent/task-plan.d.ts +57 -0
  58. package/dist/agent/task-plan.js +147 -1
  59. package/dist/agent/task-plan.js.map +1 -1
  60. package/dist/agent/tool-call-parser.d.ts +4 -12
  61. package/dist/agent/tool-call-parser.js +34 -67
  62. package/dist/agent/tool-call-parser.js.map +1 -1
  63. package/dist/agent/tool-history.d.ts +7 -0
  64. package/dist/agent/tool-history.js +50 -0
  65. package/dist/agent/tool-history.js.map +1 -1
  66. package/dist/agent/tool-output-formatting.d.ts +13 -1
  67. package/dist/agent/tool-output-formatting.js +75 -22
  68. package/dist/agent/tool-output-formatting.js.map +1 -1
  69. package/dist/agent/turn-outcome.d.ts +11 -0
  70. package/dist/agent/turn-outcome.js +17 -0
  71. package/dist/agent/turn-outcome.js.map +1 -0
  72. package/dist/agent/turn-state.d.ts +11 -0
  73. package/dist/agent/turn-state.js +28 -0
  74. package/dist/agent/turn-state.js.map +1 -0
  75. package/dist/agent/workspace-orient.d.ts +11 -0
  76. package/dist/agent/workspace-orient.js +103 -12
  77. package/dist/agent/workspace-orient.js.map +1 -1
  78. package/dist/app/adapters/agent-event-adapter.d.ts +2 -13
  79. package/dist/app/adapters/agent-event-adapter.js +63 -25
  80. package/dist/app/adapters/agent-event-adapter.js.map +1 -1
  81. package/dist/app/adapters/current-agent-adapter.d.ts +0 -5
  82. package/dist/app/adapters/current-agent-adapter.js +4 -7
  83. package/dist/app/adapters/current-agent-adapter.js.map +1 -1
  84. package/dist/app/adapters/current-jobs-adapter.js +1 -0
  85. package/dist/app/adapters/current-jobs-adapter.js.map +1 -1
  86. package/dist/app/adapters/current-terminal-adapter.d.ts +0 -7
  87. package/dist/app/adapters/current-terminal-adapter.js +0 -7
  88. package/dist/app/adapters/current-terminal-adapter.js.map +1 -1
  89. package/dist/app/adapters/current-updates-adapter.d.ts +0 -5
  90. package/dist/app/adapters/current-updates-adapter.js +0 -5
  91. package/dist/app/adapters/current-updates-adapter.js.map +1 -1
  92. package/dist/app/adapters/in-memory-clipboard-adapter.d.ts +0 -5
  93. package/dist/app/adapters/in-memory-clipboard-adapter.js +0 -5
  94. package/dist/app/adapters/in-memory-clipboard-adapter.js.map +1 -1
  95. package/dist/app/commands/command.d.ts +0 -5
  96. package/dist/app/commands/command.js.map +1 -1
  97. package/dist/app/commands/registry.d.ts +0 -14
  98. package/dist/app/commands/registry.js +0 -20
  99. package/dist/app/commands/registry.js.map +1 -1
  100. package/dist/app/controllers/disposable.d.ts +0 -5
  101. package/dist/app/controllers/disposable.js +0 -5
  102. package/dist/app/controllers/disposable.js.map +1 -1
  103. package/dist/app/controllers/job-controller.d.ts +1 -5
  104. package/dist/app/controllers/job-controller.js +0 -4
  105. package/dist/app/controllers/job-controller.js.map +1 -1
  106. package/dist/app/controllers/plan-controller.d.ts +5 -8
  107. package/dist/app/controllers/plan-controller.js +30 -12
  108. package/dist/app/controllers/plan-controller.js.map +1 -1
  109. package/dist/app/controllers/session-controller.d.ts +17 -32
  110. package/dist/app/controllers/session-controller.js +56 -35
  111. package/dist/app/controllers/session-controller.js.map +1 -1
  112. package/dist/app/controllers/turn-controller.d.ts +2 -6
  113. package/dist/app/controllers/turn-controller.js +8 -8
  114. package/dist/app/controllers/turn-controller.js.map +1 -1
  115. package/dist/app/events/app-event.d.ts +4 -7
  116. package/dist/app/events/app-event.js +0 -6
  117. package/dist/app/events/app-event.js.map +1 -1
  118. package/dist/app/events/event-buffer.d.ts +6 -17
  119. package/dist/app/events/event-buffer.js +27 -24
  120. package/dist/app/events/event-buffer.js.map +1 -1
  121. package/dist/app/events/sequencer.d.ts +0 -9
  122. package/dist/app/events/sequencer.js +0 -4
  123. package/dist/app/events/sequencer.js.map +1 -1
  124. package/dist/app/ports/agent-port.d.ts +13 -3
  125. package/dist/app/ports/clipboard-port.d.ts +0 -5
  126. package/dist/app/ports/confirm-port.d.ts +0 -7
  127. package/dist/app/ports/jobs-port.d.ts +2 -5
  128. package/dist/app/ports/persistence-port.d.ts +0 -4
  129. package/dist/app/ports/secret-port.d.ts +0 -5
  130. package/dist/app/ports/terminal-port.d.ts +0 -6
  131. package/dist/app/ports/updates-port.d.ts +0 -4
  132. package/dist/attachments/service.d.ts +29 -0
  133. package/dist/attachments/service.js +47 -0
  134. package/dist/attachments/service.js.map +1 -0
  135. package/dist/commands/providers.js +0 -3
  136. package/dist/commands/providers.js.map +1 -1
  137. package/dist/commands/search-providers.d.ts +0 -33
  138. package/dist/commands/search-providers.js +0 -33
  139. package/dist/commands/search-providers.js.map +1 -1
  140. package/dist/commands/update.js +1 -9
  141. package/dist/commands/update.js.map +1 -1
  142. package/dist/index.js +22 -42
  143. package/dist/index.js.map +1 -1
  144. package/dist/llm/adapters/ollama-tools.d.ts +0 -4
  145. package/dist/llm/adapters/ollama-tools.js +0 -4
  146. package/dist/llm/adapters/ollama-tools.js.map +1 -1
  147. package/dist/llm/capabilities.d.ts +10 -10
  148. package/dist/llm/capabilities.js +43 -17
  149. package/dist/llm/capabilities.js.map +1 -1
  150. package/dist/llm/groq.js +0 -6
  151. package/dist/llm/groq.js.map +1 -1
  152. package/dist/llm/http.d.ts +0 -17
  153. package/dist/llm/http.js +0 -45
  154. package/dist/llm/http.js.map +1 -1
  155. package/dist/llm/router.d.ts +0 -6
  156. package/dist/llm/router.js +0 -15
  157. package/dist/llm/router.js.map +1 -1
  158. package/dist/llm/tool-protocol.d.ts +0 -6
  159. package/dist/llm/tool-protocol.js +3 -7
  160. package/dist/llm/tool-protocol.js.map +1 -1
  161. package/dist/modes/agent.d.ts +2 -1
  162. package/dist/modes/agent.js +4 -2
  163. package/dist/modes/agent.js.map +1 -1
  164. package/dist/modes/ask.d.ts +0 -18
  165. package/dist/modes/ask.js +0 -49
  166. package/dist/modes/ask.js.map +1 -1
  167. package/dist/os/cwd.d.ts +0 -21
  168. package/dist/os/cwd.js +0 -24
  169. package/dist/os/cwd.js.map +1 -1
  170. package/dist/os/pkgmgr.js +0 -6
  171. package/dist/os/pkgmgr.js.map +1 -1
  172. package/dist/prompts/embedded.d.ts +1 -0
  173. package/dist/prompts/embedded.js +9 -0
  174. package/dist/prompts/embedded.js.map +1 -0
  175. package/dist/prompts/index.d.ts +6 -18
  176. package/dist/prompts/index.js +104 -293
  177. package/dist/prompts/index.js.map +1 -1
  178. package/dist/prompts/system.agent.md +228 -0
  179. package/dist/prompts/system.ask.md +56 -0
  180. package/dist/repl/prompt-line.js +0 -36
  181. package/dist/repl/prompt-line.js.map +1 -1
  182. package/dist/repl/slash-commands.d.ts +0 -9
  183. package/dist/repl/slash-commands.js +2 -23
  184. package/dist/repl/slash-commands.js.map +1 -1
  185. package/dist/repl.d.ts +5 -0
  186. package/dist/repl.js +169 -55
  187. package/dist/repl.js.map +1 -1
  188. package/dist/safety/classifier.js +17 -9
  189. package/dist/safety/classifier.js.map +1 -1
  190. package/dist/safety/engagement-policy.d.ts +46 -0
  191. package/dist/safety/engagement-policy.js +198 -0
  192. package/dist/safety/engagement-policy.js.map +1 -0
  193. package/dist/store/engagement.d.ts +115 -0
  194. package/dist/store/engagement.js +278 -0
  195. package/dist/store/engagement.js.map +1 -0
  196. package/dist/store/logs.d.ts +5 -0
  197. package/dist/store/logs.js +52 -2
  198. package/dist/store/logs.js.map +1 -1
  199. package/dist/store/plan.d.ts +57 -5
  200. package/dist/store/plan.js +182 -8
  201. package/dist/store/plan.js.map +1 -1
  202. package/dist/store/scope.d.ts +4 -1
  203. package/dist/store/scope.js +2 -2
  204. package/dist/store/scope.js.map +1 -1
  205. package/dist/tools/definitions.js +70 -1
  206. package/dist/tools/definitions.js.map +1 -1
  207. package/dist/tools/elevated-shell.d.ts +60 -0
  208. package/dist/tools/elevated-shell.js +233 -0
  209. package/dist/tools/elevated-shell.js.map +1 -0
  210. package/dist/tools/file-diff.d.ts +125 -0
  211. package/dist/tools/file-diff.js +495 -0
  212. package/dist/tools/file-diff.js.map +1 -0
  213. package/dist/tools/fs.d.ts +11 -13
  214. package/dist/tools/fs.js +269 -78
  215. package/dist/tools/fs.js.map +1 -1
  216. package/dist/tools/http.d.ts +15 -0
  217. package/dist/tools/http.js +202 -61
  218. package/dist/tools/http.js.map +1 -1
  219. package/dist/tools/jobs.d.ts +71 -10
  220. package/dist/tools/jobs.js +403 -124
  221. package/dist/tools/jobs.js.map +1 -1
  222. package/dist/tools/nmap-runner.d.ts +11 -0
  223. package/dist/tools/nmap-runner.js +115 -74
  224. package/dist/tools/nmap-runner.js.map +1 -1
  225. package/dist/tools/pentest-workflows.d.ts +8 -0
  226. package/dist/tools/pentest-workflows.js +71 -0
  227. package/dist/tools/pentest-workflows.js.map +1 -0
  228. package/dist/tools/registry.d.ts +9 -0
  229. package/dist/tools/registry.js +244 -57
  230. package/dist/tools/registry.js.map +1 -1
  231. package/dist/tools/shell.d.ts +3 -0
  232. package/dist/tools/shell.js +35 -13
  233. package/dist/tools/shell.js.map +1 -1
  234. package/dist/tools/tool-types.d.ts +11 -0
  235. package/dist/tools/web/fetch-core.d.ts +7 -0
  236. package/dist/tools/web/fetch-core.js +40 -1
  237. package/dist/tools/web/fetch-core.js.map +1 -1
  238. package/dist/tools/web/fetch.js +8 -1
  239. package/dist/tools/web/fetch.js.map +1 -1
  240. package/dist/tui-v2/app/App.js +34 -41
  241. package/dist/tui-v2/app/App.js.map +1 -1
  242. package/dist/tui-v2/app/command-handlers.js +20 -1
  243. package/dist/tui-v2/app/command-handlers.js.map +1 -1
  244. package/dist/tui-v2/app/commands/picker-commands.js +3 -0
  245. package/dist/tui-v2/app/commands/picker-commands.js.map +1 -1
  246. package/dist/tui-v2/app/commands/session-commands.js +5 -0
  247. package/dist/tui-v2/app/commands/session-commands.js.map +1 -1
  248. package/dist/tui-v2/app/plan-lifecycle.d.ts +25 -3
  249. package/dist/tui-v2/app/plan-lifecycle.js +87 -24
  250. package/dist/tui-v2/app/plan-lifecycle.js.map +1 -1
  251. package/dist/tui-v2/bootstrap/patch-opentui-text.d.ts +6 -0
  252. package/dist/tui-v2/bootstrap/patch-opentui-text.js +53 -0
  253. package/dist/tui-v2/bootstrap/patch-opentui-text.js.map +1 -0
  254. package/dist/tui-v2/bootstrap/start-tui-v2.js +7 -0
  255. package/dist/tui-v2/bootstrap/start-tui-v2.js.map +1 -1
  256. package/dist/tui-v2/components/jobs/jobs-panel.js +23 -14
  257. package/dist/tui-v2/components/jobs/jobs-panel.js.map +1 -1
  258. package/dist/tui-v2/components/modal/confirm-modal.d.ts +1 -1
  259. package/dist/tui-v2/components/modal/confirm-modal.js +18 -7
  260. package/dist/tui-v2/components/modal/confirm-modal.js.map +1 -1
  261. package/dist/tui-v2/components/overlay/overlay-host.js +1 -1
  262. package/dist/tui-v2/components/overlay/overlay-host.js.map +1 -1
  263. package/dist/tui-v2/components/pager/pager.d.ts +6 -0
  264. package/dist/tui-v2/components/pager/pager.js +313 -67
  265. package/dist/tui-v2/components/pager/pager.js.map +1 -1
  266. package/dist/tui-v2/components/status/status-line.d.ts +9 -0
  267. package/dist/tui-v2/components/status/status-line.js +17 -3
  268. package/dist/tui-v2/components/status/status-line.js.map +1 -1
  269. package/dist/tui-v2/components/transcript/assistant-message.js +1 -1
  270. package/dist/tui-v2/components/transcript/assistant-message.js.map +1 -1
  271. package/dist/tui-v2/components/transcript/compacted-row.js +1 -1
  272. package/dist/tui-v2/components/transcript/compacted-row.js.map +1 -1
  273. package/dist/tui-v2/components/transcript/tool-card.d.ts +9 -1
  274. package/dist/tui-v2/components/transcript/tool-card.js +114 -9
  275. package/dist/tui-v2/components/transcript/tool-card.js.map +1 -1
  276. package/dist/tui-v2/components/transcript/transcript-row.js +2 -2
  277. package/dist/tui-v2/components/transcript/transcript-row.js.map +1 -1
  278. package/dist/tui-v2/composer/composer-editor.js +16 -0
  279. package/dist/tui-v2/composer/composer-editor.js.map +1 -1
  280. package/dist/tui-v2/controllers/overlay-controller.d.ts +16 -2
  281. package/dist/tui-v2/controllers/overlay-controller.js +92 -12
  282. package/dist/tui-v2/controllers/overlay-controller.js.map +1 -1
  283. package/dist/tui-v2/rendering/artifact-pager-source.d.ts +19 -0
  284. package/dist/tui-v2/rendering/artifact-pager-source.js +103 -0
  285. package/dist/tui-v2/rendering/artifact-pager-source.js.map +1 -0
  286. package/dist/tui-v2/rendering/file-diff-view.d.ts +63 -0
  287. package/dist/tui-v2/rendering/file-diff-view.js +263 -0
  288. package/dist/tui-v2/rendering/file-diff-view.js.map +1 -0
  289. package/dist/tui-v2/rendering/open-tool-output.d.ts +9 -1
  290. package/dist/tui-v2/rendering/open-tool-output.js +65 -12
  291. package/dist/tui-v2/rendering/open-tool-output.js.map +1 -1
  292. package/dist/tui-v2/rendering/pager-chrome.d.ts +12 -0
  293. package/dist/tui-v2/rendering/pager-chrome.js +56 -0
  294. package/dist/tui-v2/rendering/pager-chrome.js.map +1 -0
  295. package/dist/tui-v2/rendering/plan-view.js +1 -1
  296. package/dist/tui-v2/rendering/plan-view.js.map +1 -1
  297. package/dist/tui-v2/rendering/syntax-highlight.d.ts +37 -0
  298. package/dist/tui-v2/rendering/syntax-highlight.js +1303 -0
  299. package/dist/tui-v2/rendering/syntax-highlight.js.map +1 -0
  300. package/dist/tui-v2/rendering/theme.d.ts +28 -0
  301. package/dist/tui-v2/rendering/theme.js +28 -0
  302. package/dist/tui-v2/rendering/theme.js.map +1 -1
  303. package/dist/tui-v2/rendering/tool-presenter.d.ts +5 -0
  304. package/dist/tui-v2/rendering/tool-presenter.js +35 -3
  305. package/dist/tui-v2/rendering/tool-presenter.js.map +1 -1
  306. package/dist/tui-v2/state/transcript-hydrate.js +5 -0
  307. package/dist/tui-v2/state/transcript-hydrate.js.map +1 -1
  308. package/dist/tui-v2/state/transcript-reducer.js +12 -2
  309. package/dist/tui-v2/state/transcript-reducer.js.map +1 -1
  310. package/dist/tui-v2/state/transcript-store.d.ts +7 -0
  311. package/dist/tui-v2/state/transcript-store.js +46 -0
  312. package/dist/tui-v2/state/transcript-store.js.map +1 -1
  313. package/dist/tui-v2/state/transcript-types.d.ts +12 -0
  314. package/dist/tui-v2/state/transcript-types.js +9 -0
  315. package/dist/tui-v2/state/transcript-types.js.map +1 -1
  316. package/dist/types.d.ts +17 -1
  317. package/dist/ui/mentions.d.ts +6 -0
  318. package/dist/ui/mentions.js +50 -12
  319. package/dist/ui/mentions.js.map +1 -1
  320. package/dist/ui/plan-pane.js +1 -1
  321. package/dist/ui/plan-pane.js.map +1 -1
  322. package/package.json +18 -12
@@ -1,189 +1,468 @@
1
- import { spawn } from "node:child_process";
2
- import { createWriteStream } from "node:fs";
3
- import { readFile, mkdir } from "node:fs/promises";
4
- import { join } from "node:path";
5
- import { randomUUID } from "node:crypto";
1
+ import { spawn, execFileSync } from "node:child_process";
2
+ import { createWriteStream, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
3
+ import { mkdir, open, stat } from "node:fs/promises";
4
+ import { basename, join } from "node:path";
5
+ import { createHash, randomUUID } from "node:crypto";
6
+ import { redactSecrets } from "../llm/provider.js";
6
7
  import { safeCwd } from "../os/cwd.js";
7
8
  import { getJobsDir } from "../store/paths.js";
9
+ function displayArg(value) {
10
+ return /^[A-Za-z0-9_./:=@+-]+$/.test(value)
11
+ ? value
12
+ : JSON.stringify(value);
13
+ }
14
+ function commandDisplay(command) {
15
+ if (typeof command === "string")
16
+ return command;
17
+ return command.display ?? [command.command, ...command.argv].map(displayArg).join(" ");
18
+ }
19
+ const PER_FILE_BYTES = 1024 * 1024;
20
+ const TOTAL_STREAM_BYTES = 16 * 1024 * 1024;
21
+ const DEFAULT_TAIL_BYTES = 8_000;
22
+ const REGISTRY_FILE = "registry-v1.json";
23
+ function processAlive(pid) {
24
+ if (!pid)
25
+ return false;
26
+ try {
27
+ process.kill(pid, 0);
28
+ return true;
29
+ }
30
+ catch {
31
+ return false;
32
+ }
33
+ }
34
+ function processIdentity(pid) {
35
+ if (!pid || process.platform === "win32")
36
+ return undefined;
37
+ try {
38
+ const value = execFileSync("ps", ["-p", String(pid), "-o", "lstart=", "-o", "command="], {
39
+ encoding: "utf8",
40
+ timeout: 2_000,
41
+ stdio: ["ignore", "pipe", "ignore"],
42
+ }).trim();
43
+ return value ? createHash("sha256").update(value).digest("hex") : undefined;
44
+ }
45
+ catch {
46
+ return undefined;
47
+ }
48
+ }
49
+ class RotatingRedactedWriter {
50
+ receipt;
51
+ stream;
52
+ index = 0;
53
+ currentBytes = 0;
54
+ hash = createHash("sha256");
55
+ pending = "";
56
+ static REDACTION_OVERLAP_CHARS = 4096;
57
+ constructor(receipt) {
58
+ this.receipt = receipt;
59
+ }
60
+ append(raw) {
61
+ this.pending += Buffer.isBuffer(raw) ? raw.toString("utf8") : raw;
62
+ // Complete lines are safe to redact as a unit, including secrets split
63
+ // across arbitrary process data events. Keep an unterminated tail as the
64
+ // overlap window for the next event.
65
+ const lastNewline = this.pending.lastIndexOf("\n");
66
+ if (lastNewline >= 0) {
67
+ this.writeRedacted(this.pending.slice(0, lastNewline + 1));
68
+ this.pending = this.pending.slice(lastNewline + 1);
69
+ }
70
+ if (this.pending.length <= RotatingRedactedWriter.REDACTION_OVERLAP_CHARS)
71
+ return;
72
+ const flushLength = this.pending.length - RotatingRedactedWriter.REDACTION_OVERLAP_CHARS;
73
+ this.writeRedacted(this.pending.slice(0, flushLength));
74
+ this.pending = this.pending.slice(flushLength);
75
+ }
76
+ close() {
77
+ if (this.pending)
78
+ this.writeRedacted(this.pending);
79
+ this.pending = "";
80
+ this.stream?.end();
81
+ this.stream = undefined;
82
+ }
83
+ writeRedacted(source) {
84
+ const safe = redactSecrets(source);
85
+ if (safe !== source)
86
+ this.receipt.redacted = true;
87
+ let data = Buffer.from(safe, "utf8");
88
+ if (this.receipt.bytes >= TOTAL_STREAM_BYTES) {
89
+ this.receipt.droppedBytes += data.length;
90
+ return;
91
+ }
92
+ if (this.receipt.bytes + data.length > TOTAL_STREAM_BYTES) {
93
+ const keep = TOTAL_STREAM_BYTES - this.receipt.bytes;
94
+ this.receipt.droppedBytes += data.length - keep;
95
+ data = data.subarray(0, keep);
96
+ }
97
+ while (data.length > 0) {
98
+ if (!this.stream || this.currentBytes >= PER_FILE_BYTES)
99
+ this.rotate();
100
+ const room = PER_FILE_BYTES - this.currentBytes;
101
+ const part = data.subarray(0, room);
102
+ this.stream.write(part);
103
+ this.hash.update(part);
104
+ this.currentBytes += part.length;
105
+ this.receipt.bytes += part.length;
106
+ data = data.subarray(part.length);
107
+ }
108
+ this.receipt.sha256 = this.hash.copy().digest("hex");
109
+ }
110
+ rotate() {
111
+ this.stream?.end();
112
+ const path = this.index === 0 ? this.receipt.path : `${this.receipt.path}.${this.index}`;
113
+ this.index += 1;
114
+ this.currentBytes = 0;
115
+ this.receipt.chunks.push(path);
116
+ this.stream = createWriteStream(path, { flags: "w", mode: 0o600 });
117
+ }
118
+ }
8
119
  export class JobManager {
120
+ jobsDir;
9
121
  jobs = new Map();
10
122
  processes = new Map();
11
- streams = new Map();
123
+ writers = new Map();
12
124
  abortControllers = new Map();
13
- registerJob(id, job, ac, process) {
125
+ registryPath;
126
+ constructor(jobsDir = getJobsDir()) {
127
+ this.jobsDir = jobsDir;
128
+ this.registryPath = join(this.jobsDir, REGISTRY_FILE);
129
+ this.loadAndReconcile();
130
+ }
131
+ registerJob(id, job, ac, child) {
14
132
  this.jobs.set(id, job);
15
- if (ac) {
133
+ if (ac)
16
134
  this.abortControllers.set(id, ac);
17
- }
18
- if (process) {
19
- this.processes.set(id, process);
20
- }
135
+ if (child)
136
+ this.processes.set(id, child);
137
+ void this.persist();
21
138
  }
22
139
  updateJobStatus(id, status, exitCode) {
23
140
  const job = this.jobs.get(id);
24
- if (job) {
25
- job.status = status;
26
- if (exitCode !== undefined) {
27
- job.exitCode = exitCode;
28
- }
141
+ if (!job)
142
+ return;
143
+ job.status = status;
144
+ if (exitCode !== undefined)
145
+ job.exitCode = exitCode;
146
+ if (!["running", "starting", "stopping"].includes(status))
29
147
  job.endedAt = new Date().toISOString();
30
- this.abortControllers.delete(id);
31
- this.processes.delete(id);
32
- }
148
+ this.abortControllers.delete(id);
149
+ this.processes.delete(id);
150
+ void this.persist();
33
151
  }
34
152
  async startJob(command, options) {
153
+ if (options?.authorization?.expiresAt) {
154
+ const expiry = Date.parse(options.authorization.expiresAt);
155
+ if (Number.isFinite(expiry) && expiry <= Date.now()) {
156
+ return { ok: false, output: `Engagement authorization for ${options.authorization.target} has expired.`, exitCode: 1 };
157
+ }
158
+ }
35
159
  const id = randomUUID().slice(0, 8);
36
160
  const cwd = options?.cwd ?? safeCwd();
37
- const dir = getJobsDir();
38
- await mkdir(dir, { recursive: true });
39
- const artifactPath = join(dir, `${new Date().toISOString().replace(/[:.]/g, "-")}-${id}.txt`);
40
- const stream = createWriteStream(artifactPath, { flags: "w" });
41
- stream.write(`$ ${command}\n\n`);
161
+ await mkdir(this.jobsDir, { recursive: true });
162
+ const prefix = `${new Date().toISOString().replace(/[:.]/g, "-")}-${id}`;
163
+ const stdoutArtifact = join(this.jobsDir, `${prefix}.stdout.log`);
164
+ const stderrArtifact = join(this.jobsDir, `${prefix}.stderr.log`);
165
+ const makeReceipt = (path) => ({ path, chunks: [], bytes: 0, droppedBytes: 0, redacted: false, sha256: "" });
166
+ const safeDisplay = redactSecrets(commandDisplay(command));
167
+ const job = {
168
+ id,
169
+ command: safeDisplay,
170
+ commandDisplay: safeDisplay,
171
+ cwd,
172
+ status: "starting",
173
+ startedAt: new Date().toISOString(),
174
+ artifactPath: stdoutArtifact,
175
+ stdoutArtifact,
176
+ stderrArtifact,
177
+ artifacts: { stdout: makeReceipt(stdoutArtifact), stderr: makeReceipt(stderrArtifact) },
178
+ redactionProfile: "provider-secrets-v1",
179
+ ownerSessionId: options?.ownerSessionId ?? "unknown",
180
+ ...(options?.name ? { name: options.name } : {}),
181
+ ...(options?.authorization ? { authorization: options.authorization } : {}),
182
+ };
183
+ this.jobs.set(id, job);
184
+ await this.persist();
42
185
  try {
43
186
  const detached = process.platform !== "win32";
44
- const child = spawn(command, {
45
- cwd,
46
- detached,
47
- shell: true,
48
- stdio: ["ignore", "pipe", "pipe"],
49
- });
50
- const job = {
51
- id,
52
- command,
53
- cwd,
54
- pid: child.pid,
55
- status: "running",
56
- startedAt: new Date().toISOString(),
57
- artifactPath,
58
- };
59
- this.jobs.set(id, job);
187
+ const child = typeof command === "string"
188
+ ? spawn(command, {
189
+ cwd,
190
+ detached,
191
+ shell: true,
192
+ stdio: ["ignore", "pipe", "pipe"],
193
+ })
194
+ : spawn(command.command, command.argv, {
195
+ cwd,
196
+ detached,
197
+ shell: false,
198
+ stdio: [command.stdinText === undefined ? "ignore" : "pipe", "pipe", "pipe"],
199
+ });
200
+ // Sensitive input is never copied into the job record, display, logs, or
201
+ // artifacts. Write it once to the child and close stdin immediately.
202
+ if (typeof command !== "string" && command.stdinText !== undefined) {
203
+ child.stdin?.end(command.stdinText);
204
+ }
205
+ job.pid = child.pid;
206
+ job.processGroupId = detached ? child.pid : undefined;
207
+ job.status = "running";
208
+ job.heartbeatAt = new Date().toISOString();
209
+ job.processIdentity = processIdentity(child.pid);
210
+ const stdout = new RotatingRedactedWriter(job.artifacts.stdout);
211
+ const stderr = new RotatingRedactedWriter(job.artifacts.stderr);
212
+ stdout.append(`$ ${job.commandDisplay}\n\n`);
60
213
  this.processes.set(id, child);
61
- this.streams.set(id, stream);
62
- child.stdout?.on("data", (chunk) => {
63
- stream.write(chunk);
64
- });
65
- child.stderr?.on("data", (chunk) => {
66
- stream.write(chunk);
67
- });
68
- child.on("close", (code) => {
69
- job.status = "exited";
214
+ this.writers.set(id, { stdout, stderr });
215
+ const expiresAt = options?.authorization?.expiresAt ? Date.parse(options.authorization.expiresAt) : Number.NaN;
216
+ const expiryTimer = Number.isFinite(expiresAt) && expiresAt > Date.now()
217
+ ? setTimeout(() => { void this.stopJob(id, { graceMs: 1_000 }); }, expiresAt - Date.now())
218
+ : undefined;
219
+ expiryTimer?.unref?.();
220
+ child.stdout?.on("data", (chunk) => { stdout.append(chunk); job.heartbeatAt = new Date().toISOString(); void this.persist(); });
221
+ child.stderr?.on("data", (chunk) => { stderr.append(chunk); job.heartbeatAt = new Date().toISOString(); void this.persist(); });
222
+ child.on("close", (code, signal) => {
223
+ if (expiryTimer)
224
+ clearTimeout(expiryTimer);
70
225
  job.exitCode = code ?? undefined;
226
+ job.signal = signal ?? undefined;
227
+ job.status = signal ? "killed" : code === 0 ? "exited" : "failed";
71
228
  job.endedAt = new Date().toISOString();
72
- stream.end();
229
+ stdout.close();
230
+ stderr.close();
73
231
  this.processes.delete(id);
74
- this.streams.delete(id);
232
+ this.writers.delete(id);
233
+ void this.persist();
75
234
  });
76
235
  child.on("error", () => {
236
+ if (expiryTimer)
237
+ clearTimeout(expiryTimer);
77
238
  job.status = "failed";
78
239
  job.endedAt = new Date().toISOString();
79
- stream.end();
240
+ stdout.close();
241
+ stderr.close();
80
242
  this.processes.delete(id);
81
- this.streams.delete(id);
243
+ this.writers.delete(id);
244
+ void this.persist();
82
245
  });
83
- // Detach so the child outlives the parent if needed
84
246
  child.unref();
247
+ await this.persist();
85
248
  return {
86
249
  ok: true,
87
- output: `Background job started: id=${id} pid=${child.pid ?? "?"}\nCommand: ${command}\nOutput: ${artifactPath}`,
250
+ output: `Background job started: id=${id} (canonical job ID) pid=${child.pid ?? "?"}\n` +
251
+ `Use shell.tail {"id":"${id}"} or shell.jobs; do not use the artifact filename as the id.\n` +
252
+ `Command: ${job.commandDisplay}\nArtifact: ${stdoutArtifact}`,
253
+ outputPath: stdoutArtifact,
254
+ backgroundJob: {
255
+ id,
256
+ status: job.status,
257
+ artifactPath: stdoutArtifact,
258
+ ...(options?.profile ? { profile: options.profile } : {}),
259
+ ...(options?.estimatedSeconds !== undefined ? { estimatedSeconds: options.estimatedSeconds } : {}),
260
+ nextOffset: 0,
261
+ },
88
262
  };
89
263
  }
90
264
  catch (error) {
91
- stream.end();
92
- return {
93
- ok: false,
94
- output: `Failed to start job: ${error instanceof Error ? error.message : String(error)}`,
95
- exitCode: 1,
96
- };
265
+ job.status = "failed";
266
+ job.endedAt = new Date().toISOString();
267
+ await this.persist();
268
+ return { ok: false, output: `Failed to start job: ${error instanceof Error ? error.message : String(error)}`, exitCode: 1 };
97
269
  }
98
270
  }
99
271
  listJobs() {
100
- if (this.jobs.size === 0) {
272
+ if (this.jobs.size === 0)
101
273
  return { ok: true, output: "No background jobs." };
102
- }
103
- const lines = [];
274
+ const lines = [...this.jobs.values()].map((job) => {
275
+ const elapsedEnd = job.endedAt ? new Date(job.endedAt).getTime() : Date.now();
276
+ const elapsed = Math.max(0, Math.round((elapsedEnd - new Date(job.startedAt).getTime()) / 1000));
277
+ const health = job.status === "running" ? (processAlive(job.pid) ? "alive" : "unresponsive") : "terminal";
278
+ return `[${job.id}] ${job.status} health=${health} exit=${job.exitCode ?? "?"} ${elapsed}s ${job.commandDisplay.slice(0, 60)}`;
279
+ });
280
+ return { ok: true, output: lines.join("\n") };
281
+ }
282
+ resolveJobId(input) {
283
+ if (this.jobs.has(input))
284
+ return input;
285
+ const inputBase = basename(input);
104
286
  for (const job of this.jobs.values()) {
105
- const status = job.status === "running" ? "▶ running" : `${job.status} (exit=${job.exitCode ?? "?"})`;
106
- const elapsed = job.endedAt
107
- ? `${Math.round((new Date(job.endedAt).getTime() - new Date(job.startedAt).getTime()) / 1000)}s`
108
- : `${Math.round((Date.now() - new Date(job.startedAt).getTime()) / 1000)}s ago`;
109
- lines.push(`[${job.id}] ${status} ${elapsed} ${job.command.slice(0, 60)}`);
287
+ const artifacts = [
288
+ job.artifactPath,
289
+ job.stdoutArtifact,
290
+ job.stderrArtifact,
291
+ ...job.artifacts.stdout.chunks,
292
+ ...job.artifacts.stderr.chunks,
293
+ ];
294
+ if (artifacts.some((path) => input === path || inputBase === basename(path)) ||
295
+ inputBase.includes(`-${job.id}.`)) {
296
+ return job.id;
297
+ }
110
298
  }
111
- return { ok: true, output: lines.join("\n") };
299
+ return undefined;
112
300
  }
113
301
  getJob(id) {
114
- return this.jobs.get(id);
302
+ const resolved = this.resolveJobId(id);
303
+ return resolved ? this.jobs.get(resolved) : undefined;
115
304
  }
116
- async tailJob(id, bytes) {
117
- const job = this.jobs.get(id);
305
+ async tailJob(id, bytesOrCursor) {
306
+ const resolved = this.resolveJobId(id);
307
+ const job = resolved ? this.jobs.get(resolved) : undefined;
118
308
  if (!job) {
119
- return { ok: false, output: `Job "${id}" not found.`, exitCode: 1 };
309
+ const known = [...this.jobs.keys()].join(", ") || "none";
310
+ return { ok: false, output: `Job "${id}" not found. Canonical job IDs: ${known}.`, exitCode: 1 };
120
311
  }
121
- const tailBytes = bytes ?? 8_000;
312
+ const cursor = typeof bytesOrCursor === "number" ? { bytes: bytesOrCursor } : (bytesOrCursor ?? {});
313
+ const stream = cursor.stream ?? "combined";
314
+ const paths = stream === "stderr" ? job.artifacts.stderr.chunks : stream === "stdout" ? job.artifacts.stdout.chunks : [...job.artifacts.stdout.chunks, ...job.artifacts.stderr.chunks];
315
+ const readablePaths = paths.length > 0 ? paths : [stream === "stderr" ? job.stderrArtifact : job.stdoutArtifact];
316
+ const path = readablePaths.at(-1);
122
317
  try {
123
- const content = await readFile(job.artifactPath, "utf8");
124
- const tail = content.length > tailBytes
125
- ? `... (showing last ${tailBytes.toLocaleString()} bytes)\n${content.slice(-tailBytes)}`
126
- : content;
318
+ const sizes = await Promise.all(readablePaths.map(async (chunkPath) => ({ path: chunkPath, size: (await stat(chunkPath)).size })));
319
+ const total = sizes.reduce((sum, entry) => sum + entry.size, 0);
320
+ const max = Math.max(1, Math.min(cursor.bytes ?? DEFAULT_TAIL_BYTES, 1024 * 1024));
321
+ const start = cursor.offset === undefined ? Math.max(0, total - max) : Math.max(0, Math.min(cursor.offset, total));
322
+ const length = Math.min(max, total - start);
323
+ const parts = [];
324
+ let logical = 0;
325
+ let remaining = length;
326
+ for (const entry of sizes) {
327
+ const chunkStart = logical;
328
+ const chunkEnd = logical + entry.size;
329
+ logical = chunkEnd;
330
+ if (remaining <= 0 || start >= chunkEnd)
331
+ continue;
332
+ const localStart = Math.max(0, start - chunkStart);
333
+ const localLength = Math.min(remaining, entry.size - localStart);
334
+ const handle = await open(entry.path, "r");
335
+ const buffer = Buffer.alloc(localLength);
336
+ try {
337
+ await handle.read(buffer, 0, localLength, localStart);
338
+ }
339
+ finally {
340
+ await handle.close();
341
+ }
342
+ parts.push(buffer);
343
+ remaining -= localLength;
344
+ }
345
+ const nextOffset = start + (length - remaining);
127
346
  return {
128
347
  ok: true,
129
- output: `[${job.id}] ${job.status}:\n${tail}`,
130
- outputPath: job.artifactPath,
348
+ output: `[${job.id}] ${job.status} exit=${job.exitCode ?? "?"} signal=${job.signal ?? "?"} ` +
349
+ `stream=${stream} offset=${start} nextOffset=${nextOffset} total=${total}:\n` +
350
+ Buffer.concat(parts).toString("utf8"),
351
+ outputPath: path,
352
+ backgroundJob: {
353
+ id: job.id,
354
+ status: job.status,
355
+ artifactPath: path,
356
+ nextOffset,
357
+ ...(job.exitCode !== undefined ? { exitCode: job.exitCode } : {}),
358
+ ...(job.signal !== undefined ? { signal: job.signal } : {}),
359
+ },
131
360
  };
132
361
  }
133
362
  catch (error) {
134
- return {
135
- ok: false,
136
- output: `Failed to read job output: ${error instanceof Error ? error.message : String(error)}`,
137
- exitCode: 1,
138
- };
363
+ return { ok: false, output: `Failed to read job output: ${error instanceof Error ? error.message : String(error)}`, exitCode: 1 };
139
364
  }
140
365
  }
141
- stopJob(id, signal) {
142
- const job = this.jobs.get(id);
143
- if (!job) {
366
+ async stopJob(id, options) {
367
+ const resolved = this.resolveJobId(id);
368
+ const job = resolved ? this.jobs.get(resolved) : undefined;
369
+ if (!job || !resolved)
144
370
  return { ok: false, output: `Job "${id}" not found.`, exitCode: 1 };
371
+ id = resolved;
372
+ if (job.status !== "running" && job.status !== "starting")
373
+ return { ok: false, output: `Job "${id}" is already ${job.status}.`, exitCode: 1 };
374
+ job.status = "stopping";
375
+ await this.persist();
376
+ this.abortControllers.get(id)?.abort();
377
+ const pid = job.pid;
378
+ const send = (signal) => {
379
+ if (!pid)
380
+ return false;
381
+ try {
382
+ process.platform !== "win32" && job.processGroupId ? process.kill(-job.processGroupId, signal) : process.kill(pid, signal);
383
+ return true;
384
+ }
385
+ catch {
386
+ return !processAlive(pid);
387
+ }
388
+ };
389
+ const graceful = options?.signal ?? "SIGTERM";
390
+ if (!send(graceful) && processAlive(pid)) {
391
+ job.status = "running";
392
+ await this.persist();
393
+ return { ok: false, output: `Failed to signal job "${id}".`, exitCode: 1 };
145
394
  }
146
- if (job.status !== "running") {
147
- return {
148
- ok: false,
149
- output: `Job "${id}" is already ${job.status}.`,
150
- exitCode: 1,
151
- };
152
- }
153
- const ac = this.abortControllers.get(id);
154
- if (ac) {
155
- ac.abort();
395
+ const waitForExit = async (ms) => {
396
+ const deadline = Date.now() + ms;
397
+ while (Date.now() < deadline) {
398
+ if (!processAlive(pid))
399
+ return true;
400
+ await new Promise((resolve) => setTimeout(resolve, 25));
401
+ }
402
+ return !processAlive(pid);
403
+ };
404
+ let stopped = await waitForExit(options?.graceMs ?? 2_000);
405
+ let actualSignal = graceful;
406
+ if (!stopped && options?.escalate !== false) {
407
+ actualSignal = "SIGKILL";
408
+ send(actualSignal);
409
+ stopped = await waitForExit(1_000);
156
410
  }
157
- const child = this.processes.get(id);
158
- if (!child?.pid) {
159
- job.status = "killed";
160
- job.endedAt = new Date().toISOString();
161
- this.abortControllers.delete(id);
162
- return { ok: true, output: `Job "${id}" killed.` };
411
+ if (!stopped) {
412
+ job.status = "running";
413
+ await this.persist();
414
+ return { ok: false, output: `Job "${id}" remains alive after ${actualSignal}.`, exitCode: 1 };
163
415
  }
416
+ job.status = "killed";
417
+ job.signal = actualSignal;
418
+ job.endedAt = new Date().toISOString();
419
+ this.abortControllers.delete(id);
420
+ this.processes.delete(id);
421
+ this.writers.get(id)?.stdout.close();
422
+ this.writers.get(id)?.stderr.close();
423
+ this.writers.delete(id);
424
+ await this.persist();
425
+ return { ok: true, output: `Job "${id}" stopped and termination verified (${actualSignal}).` };
426
+ }
427
+ getRunningJobs() { return [...this.jobs.values()].filter((job) => job.status === "running" || job.status === "starting" || job.status === "stopping"); }
428
+ getRecentJobs(limit = 50) { return [...this.jobs.values()].sort((a, b) => b.startedAt.localeCompare(a.startedAt)).slice(0, limit); }
429
+ loadAndReconcile() {
164
430
  try {
165
- const sig = signal ?? "SIGTERM";
166
- if (process.platform !== "win32") {
167
- process.kill(-child.pid, sig);
168
- }
169
- else {
170
- child.kill(sig);
431
+ if (!existsSync(this.registryPath))
432
+ return;
433
+ const parsed = JSON.parse(readFileSync(this.registryPath, "utf8"));
434
+ if (parsed.schemaVersion !== 1 || !Array.isArray(parsed.jobs))
435
+ return;
436
+ for (const job of parsed.jobs) {
437
+ if (["starting", "running", "stopping"].includes(job.status)) {
438
+ const identity = processIdentity(job.pid);
439
+ if (!processAlive(job.pid) || !identity || !job.processIdentity || identity !== job.processIdentity) {
440
+ job.status = "lost";
441
+ job.endedAt = new Date().toISOString();
442
+ }
443
+ else {
444
+ job.status = "running";
445
+ job.heartbeatAt = new Date().toISOString();
446
+ }
447
+ }
448
+ this.jobs.set(job.id, job);
171
449
  }
172
- job.status = "killed";
173
- job.endedAt = new Date().toISOString();
174
- this.abortControllers.delete(id);
175
- return { ok: true, output: `Job "${id}" sent ${sig}.` };
450
+ this.persistSync();
176
451
  }
177
- catch {
178
- return {
179
- ok: false,
180
- output: `Failed to stop job "${id}".`,
181
- exitCode: 1,
182
- };
452
+ catch { /* Corrupt registries are ignored, never trusted as running. */ }
453
+ }
454
+ registry() { return { schemaVersion: 1, jobs: [...this.jobs.values()] }; }
455
+ persistSync() {
456
+ try {
457
+ mkdirSync(this.jobsDir, { recursive: true });
458
+ const temp = `${this.registryPath}.${process.pid}.${randomUUID()}.tmp`;
459
+ writeFileSync(temp, `${JSON.stringify(this.registry(), null, 2)}\n`, { mode: 0o600 });
460
+ renameSync(temp, this.registryPath);
183
461
  }
462
+ catch { /* best effort during constructor reconciliation */ }
184
463
  }
185
- getRunningJobs() {
186
- return [...this.jobs.values()].filter((j) => j.status === "running");
464
+ async persist() {
465
+ this.persistSync();
187
466
  }
188
467
  }
189
468
  export const jobManager = new JobManager();