@arnilo/prism-tools 0.0.0-stage → 0.13.0

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 (331) hide show
  1. package/CHANGELOG.md +410 -0
  2. package/LICENSE +21 -0
  3. package/README.md +44 -2
  4. package/browser/README.md +98 -0
  5. package/browser/skills/browser-automation/SKILL.md +14 -0
  6. package/browser/src/actions/cdp.ts +131 -0
  7. package/browser/src/actions/dialog.ts +25 -0
  8. package/browser/src/actions/download_release.ts +22 -0
  9. package/browser/src/actions/index.ts +37 -0
  10. package/browser/src/actions/locator.ts +70 -0
  11. package/browser/src/actions/navigate.ts +23 -0
  12. package/browser/src/actions/screenshot.ts +27 -0
  13. package/browser/src/actions/scroll.ts +24 -0
  14. package/browser/src/actions/select_page.ts +21 -0
  15. package/browser/src/actions/types.ts +114 -0
  16. package/browser/src/actions/upload.ts +44 -0
  17. package/browser/src/actions/wait.ts +45 -0
  18. package/browser/src/browser.ts +197 -0
  19. package/browser/src/cdp.ts +178 -0
  20. package/browser/src/checkpoint.ts +189 -0
  21. package/browser/src/config.ts +72 -0
  22. package/browser/src/downloads.ts +253 -0
  23. package/browser/src/errors.ts +31 -0
  24. package/browser/src/evaluate.ts +101 -0
  25. package/browser/src/index.ts +129 -0
  26. package/browser/src/limits.ts +271 -0
  27. package/browser/src/manager.ts +730 -0
  28. package/browser/src/network.ts +263 -0
  29. package/browser/src/observe.ts +178 -0
  30. package/browser/src/policy.ts +133 -0
  31. package/browser/src/screenshot.ts +107 -0
  32. package/browser/src/shared-sandbox.ts +53 -0
  33. package/browser/src/snapshot.ts +99 -0
  34. package/browser/src/targets.ts +165 -0
  35. package/browser/src/tools.ts +480 -0
  36. package/browser/src/types.ts +316 -0
  37. package/browser/src/uploads.ts +174 -0
  38. package/coding/README.md +337 -0
  39. package/coding/src/agent/acp-operations.ts +146 -0
  40. package/coding/src/agent/artifacts.ts +38 -0
  41. package/coding/src/agent/ask-user-decision.ts +623 -0
  42. package/coding/src/agent/atomic-write.ts +23 -0
  43. package/coding/src/agent/bounded-file.ts +21 -0
  44. package/coding/src/agent/checks.ts +301 -0
  45. package/coding/src/agent/coding-checkpoint.ts +828 -0
  46. package/coding/src/agent/delete.ts +237 -0
  47. package/coding/src/agent/diagnostics-tool.ts +163 -0
  48. package/coding/src/agent/diagnostics.ts +262 -0
  49. package/coding/src/agent/edit-diff.ts +382 -0
  50. package/coding/src/agent/edit.ts +322 -0
  51. package/coding/src/agent/effects.ts +102 -0
  52. package/coding/src/agent/env.ts +28 -0
  53. package/coding/src/agent/execution-policy.ts +89 -0
  54. package/coding/src/agent/file-mutation-queue.ts +72 -0
  55. package/coding/src/agent/forge/github.ts +603 -0
  56. package/coding/src/agent/forge/index.ts +16 -0
  57. package/coding/src/agent/forge/types.ts +185 -0
  58. package/coding/src/agent/git-aware-repository.ts +305 -0
  59. package/coding/src/agent/git-exec.ts +326 -0
  60. package/coding/src/agent/git-status.ts +177 -0
  61. package/coding/src/agent/git-tools.ts +360 -0
  62. package/coding/src/agent/git.ts +1074 -0
  63. package/coding/src/agent/glob-match.ts +146 -0
  64. package/coding/src/agent/glob.ts +182 -0
  65. package/coding/src/agent/goal-verify.ts +385 -0
  66. package/coding/src/agent/grep-worker.ts +74 -0
  67. package/coding/src/agent/grep.ts +459 -0
  68. package/coding/src/agent/identity.ts +48 -0
  69. package/coding/src/agent/index.ts +676 -0
  70. package/coding/src/agent/language/client.ts +429 -0
  71. package/coding/src/agent/language/framing.ts +190 -0
  72. package/coding/src/agent/language/index.ts +23 -0
  73. package/coding/src/agent/language/intelligence.ts +682 -0
  74. package/coding/src/agent/language/types.ts +187 -0
  75. package/coding/src/agent/lifecycle-events.ts +121 -0
  76. package/coding/src/agent/lifecycle.ts +166 -0
  77. package/coding/src/agent/limits.ts +214 -0
  78. package/coding/src/agent/move.ts +178 -0
  79. package/coding/src/agent/mutation-path.ts +52 -0
  80. package/coding/src/agent/output-accumulator.ts +304 -0
  81. package/coding/src/agent/path-utils.ts +120 -0
  82. package/coding/src/agent/process/index.ts +58 -0
  83. package/coding/src/agent/process/recovery.ts +508 -0
  84. package/coding/src/agent/process/sessions-host.ts +222 -0
  85. package/coding/src/agent/process/sessions-monitor.ts +141 -0
  86. package/coding/src/agent/process/sessions-recovery.ts +241 -0
  87. package/coding/src/agent/process/sessions-spawn.ts +613 -0
  88. package/coding/src/agent/process/sessions-teardown.ts +281 -0
  89. package/coding/src/agent/process/sessions.ts +30 -0
  90. package/coding/src/agent/process/types.ts +358 -0
  91. package/coding/src/agent/read-path-set.ts +126 -0
  92. package/coding/src/agent/read.ts +713 -0
  93. package/coding/src/agent/repository/glob.ts +147 -0
  94. package/coding/src/agent/repository/indexed-search.ts +480 -0
  95. package/coding/src/agent/repository/list.ts +131 -0
  96. package/coding/src/agent/repository/operations.ts +22 -0
  97. package/coding/src/agent/repository/path.ts +98 -0
  98. package/coding/src/agent/repository/search.ts +277 -0
  99. package/coding/src/agent/repository/types.ts +243 -0
  100. package/coding/src/agent/repository/walk.ts +126 -0
  101. package/coding/src/agent/repository.ts +12 -0
  102. package/coding/src/agent/shell-background.ts +264 -0
  103. package/coding/src/agent/shell-output.ts +75 -0
  104. package/coding/src/agent/shell.ts +526 -0
  105. package/coding/src/agent/todo.ts +399 -0
  106. package/coding/src/agent/truncate.ts +251 -0
  107. package/coding/src/agent/write.ts +170 -0
  108. package/coding/src/ask-user-host.ts +48 -0
  109. package/coding/src/config.ts +278 -0
  110. package/coding/src/hard-deny.ts +97 -0
  111. package/coding/src/index.ts +318 -0
  112. package/coding/src/principal-guard.ts +84 -0
  113. package/coding/src/sandbox.ts +92 -0
  114. package/coding/src/security/approval.ts +144 -0
  115. package/coding/src/security/command-rules.ts +57 -0
  116. package/coding/src/security/docker-cli.ts +222 -0
  117. package/coding/src/security/docker-sandbox.ts +1205 -0
  118. package/coding/src/security/e2b-sandbox.ts +853 -0
  119. package/coding/src/security/egress/dns-pin.ts +88 -0
  120. package/coding/src/security/egress/index.ts +32 -0
  121. package/coding/src/security/egress/limits.ts +57 -0
  122. package/coding/src/security/egress/policy.ts +137 -0
  123. package/coding/src/security/egress/proxy.ts +599 -0
  124. package/coding/src/security/egress/types.ts +30 -0
  125. package/coding/src/security/index.ts +199 -0
  126. package/coding/src/security/native-sandbox.ts +639 -0
  127. package/coding/src/security/path-containment.ts +55 -0
  128. package/coding/src/security/sandbox-coding-operations.ts +392 -0
  129. package/coding/src/security/sandbox-fs-operations.ts +644 -0
  130. package/coding/src/security/sandbox-limits.ts +147 -0
  131. package/coding/src/security/sandbox-tar.ts +308 -0
  132. package/coding/src/security/sandbox.ts +179 -0
  133. package/coding/src/security/semaphore.ts +55 -0
  134. package/coding/src/targets.ts +110 -0
  135. package/coding/src/workspace/diff.ts +78 -0
  136. package/coding/src/workspace/errors.ts +28 -0
  137. package/coding/src/workspace/index.ts +5 -0
  138. package/coding/src/workspace/merge.ts +151 -0
  139. package/coding/src/workspace/routing.ts +76 -0
  140. package/coding/src/workspace/service.ts +545 -0
  141. package/computer-use/README.md +105 -0
  142. package/computer-use/skills/computer-use-linux/SKILL.md +17 -0
  143. package/computer-use/src/classify.ts +53 -0
  144. package/computer-use/src/command.ts +29 -0
  145. package/computer-use/src/config.ts +69 -0
  146. package/computer-use/src/create.ts +170 -0
  147. package/computer-use/src/devices.ts +247 -0
  148. package/computer-use/src/env.ts +28 -0
  149. package/computer-use/src/index.ts +157 -0
  150. package/computer-use/src/skill.ts +21 -0
  151. package/openapi/README.md +97 -0
  152. package/openapi/src/compile.ts +386 -0
  153. package/openapi/src/config.ts +73 -0
  154. package/openapi/src/errors.ts +16 -0
  155. package/openapi/src/execute.ts +241 -0
  156. package/openapi/src/index.ts +124 -0
  157. package/openapi/src/limits.ts +63 -0
  158. package/openapi/src/specs.ts +86 -0
  159. package/openapi/src/tools.ts +60 -0
  160. package/openapi/src/types.ts +61 -0
  161. package/package.json +171 -4
  162. package/research/README.md +72 -0
  163. package/research/skills/research-report/SKILL.md +12 -0
  164. package/research/skills/research-scoping/SKILL.md +10 -0
  165. package/research/skills/research-synthesis/SKILL.md +10 -0
  166. package/research/skills/source-triage/SKILL.md +10 -0
  167. package/research/src/check.ts +71 -0
  168. package/research/src/config.ts +41 -0
  169. package/research/src/index.ts +147 -0
  170. package/research/src/ledger.ts +157 -0
  171. package/research/src/urls.ts +21 -0
  172. package/web/README.md +287 -0
  173. package/web/skills/web-research/SKILL.md +17 -0
  174. package/web/src/brave.ts +66 -0
  175. package/web/src/cache.ts +151 -0
  176. package/web/src/config.ts +210 -0
  177. package/web/src/controls.ts +128 -0
  178. package/web/src/domains.ts +114 -0
  179. package/web/src/exa.ts +60 -0
  180. package/web/src/firecrawl.ts +160 -0
  181. package/web/src/index.ts +387 -0
  182. package/web/src/kagi.ts +73 -0
  183. package/web/src/limits.ts +55 -0
  184. package/web/src/native/convert.ts +84 -0
  185. package/web/src/native/fetch.ts +226 -0
  186. package/web/src/normalize.ts +131 -0
  187. package/web/src/obscura/cdp.ts +201 -0
  188. package/web/src/obscura/classify.ts +35 -0
  189. package/web/src/obscura/cli.ts +254 -0
  190. package/web/src/obscura/errors.ts +8 -0
  191. package/web/src/obscura/index.ts +59 -0
  192. package/web/src/obscura/limits.ts +92 -0
  193. package/web/src/obscura/mcp.ts +104 -0
  194. package/web/src/obscura/playwright.ts +133 -0
  195. package/web/src/obscura/process.ts +235 -0
  196. package/web/src/obscura/search-profile.ts +73 -0
  197. package/web/src/obscura/types.ts +48 -0
  198. package/web/src/obscura/web.ts +419 -0
  199. package/web/src/prompt-fetch.ts +88 -0
  200. package/web/src/render.ts +75 -0
  201. package/web/src/searxng.ts +78 -0
  202. package/web/src/slice.ts +139 -0
  203. package/web/src/tavily.ts +77 -0
  204. package/web/src/tools.ts +182 -0
  205. package/web/src/transport.ts +310 -0
  206. package/web/src/types.ts +199 -0
  207. package/work/README.md +77 -0
  208. package/work/THIRD_PARTY_NOTICES +24 -0
  209. package/work/docling/ocr.py +150 -0
  210. package/work/sandbox/Dockerfile +36 -0
  211. package/work/sandbox/soffice.sh +12 -0
  212. package/work/src/command.ts +29 -0
  213. package/work/src/config.ts +138 -0
  214. package/work/src/connectors/cli.ts +343 -0
  215. package/work/src/connectors/drafts.ts +647 -0
  216. package/work/src/connectors/errors.ts +8 -0
  217. package/work/src/connectors/file-bytes.ts +149 -0
  218. package/work/src/connectors/google-workspace-http.ts +399 -0
  219. package/work/src/connectors/google-workspace.ts +465 -0
  220. package/work/src/connectors/gws-tools.ts +378 -0
  221. package/work/src/connectors/http.ts +123 -0
  222. package/work/src/connectors/idempotency.ts +235 -0
  223. package/work/src/connectors/identity.ts +34 -0
  224. package/work/src/connectors/index.ts +93 -0
  225. package/work/src/connectors/limits.ts +50 -0
  226. package/work/src/connectors/m365-tools.ts +308 -0
  227. package/work/src/connectors/microsoft365-http.ts +428 -0
  228. package/work/src/connectors/microsoft365.ts +402 -0
  229. package/work/src/connectors/normalize.ts +120 -0
  230. package/work/src/connectors/tool-helpers.ts +398 -0
  231. package/work/src/connectors/tools.ts +42 -0
  232. package/work/src/connectors/types.ts +399 -0
  233. package/work/src/diagrams/canonicalize.ts +166 -0
  234. package/work/src/diagrams/caps.ts +52 -0
  235. package/work/src/diagrams/embed.ts +340 -0
  236. package/work/src/diagrams/errors.ts +76 -0
  237. package/work/src/diagrams/index.ts +81 -0
  238. package/work/src/diagrams/messages.ts +150 -0
  239. package/work/src/diagrams/origin.ts +52 -0
  240. package/work/src/diagrams/telemetry.ts +34 -0
  241. package/work/src/diagrams/xml.ts +267 -0
  242. package/work/src/document-extraction/index.ts +563 -0
  243. package/work/src/document-reader/ambient.d.ts +2 -0
  244. package/work/src/document-reader/errors.ts +7 -0
  245. package/work/src/document-reader/index.ts +181 -0
  246. package/work/src/document-reader/mistral-ocr.ts +318 -0
  247. package/work/src/document-reader/parsers.ts +171 -0
  248. package/work/src/documents/caps.ts +135 -0
  249. package/work/src/documents/diff.ts +269 -0
  250. package/work/src/documents/errors.ts +44 -0
  251. package/work/src/documents/fidelity.ts +98 -0
  252. package/work/src/documents/generate.ts +96 -0
  253. package/work/src/documents/hash.ts +8 -0
  254. package/work/src/documents/index.ts +14 -0
  255. package/work/src/documents/model-schema.ts +639 -0
  256. package/work/src/documents/parse.ts +252 -0
  257. package/work/src/documents/patch-history.ts +81 -0
  258. package/work/src/documents/patch.ts +476 -0
  259. package/work/src/documents/preview-html.ts +245 -0
  260. package/work/src/documents/preview.ts +227 -0
  261. package/work/src/documents/telemetry.ts +36 -0
  262. package/work/src/documents/translate/docx.ts +292 -0
  263. package/work/src/documents/translate/pptx.ts +304 -0
  264. package/work/src/documents/translate/xlsx.ts +137 -0
  265. package/work/src/documents/types.ts +188 -0
  266. package/work/src/index.ts +311 -0
  267. package/work/src/runtime/worker-pool-entry.ts +102 -0
  268. package/work/src/runtime/worker-pool.ts +267 -0
  269. package/work/src/sandbox/composition.ts +212 -0
  270. package/work/src/sandbox/image.ts +6 -0
  271. package/work/src/sandbox/index.ts +14 -0
  272. package/work/src/sheets/caps.ts +68 -0
  273. package/work/src/sheets/csv.ts +351 -0
  274. package/work/src/sheets/decimal.ts +388 -0
  275. package/work/src/sheets/errors.ts +52 -0
  276. package/work/src/sheets/index.ts +62 -0
  277. package/work/src/sheets/inference.ts +378 -0
  278. package/work/src/sheets/telemetry.ts +34 -0
  279. package/work/src/sheets/types.ts +162 -0
  280. package/work/src/sheets/xlsx.ts +388 -0
  281. package/work/src/skills/index.ts +8 -0
  282. package/work/src/skills/load.ts +43 -0
  283. package/work/src/tools/filesystem.ts +21 -0
  284. package/work/src/tools/index.ts +1 -0
  285. package/work/src/tools/local-filesystem.ts +33 -0
  286. package/work/src/tools/office.ts +311 -0
  287. package/work/vendor/hermes-agent/LICENSE +21 -0
  288. package/work/vendor/hermes-agent/skills/productivity/docx/LICENSE +21 -0
  289. package/work/vendor/hermes-agent/skills/productivity/docx/SKILL.md +196 -0
  290. package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_comments.py +289 -0
  291. package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_common.py +94 -0
  292. package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_create.py +177 -0
  293. package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_edit.py +250 -0
  294. package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_read.py +149 -0
  295. package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_revisions.py +147 -0
  296. package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_template.py +70 -0
  297. package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_validate.py +156 -0
  298. package/work/vendor/hermes-agent/skills/productivity/pdf/LICENSE +21 -0
  299. package/work/vendor/hermes-agent/skills/productivity/pdf/SKILL.md +125 -0
  300. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/_raster.py +76 -0
  301. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/extract_marker.py +88 -0
  302. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/extract_pymupdf.py +101 -0
  303. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_create.py +130 -0
  304. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_fill_form.py +97 -0
  305. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_form_layout.py +168 -0
  306. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_make_form.py +145 -0
  307. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_merge.py +50 -0
  308. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_meta.py +115 -0
  309. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_page_image.py +99 -0
  310. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_read.py +153 -0
  311. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_secure.py +71 -0
  312. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_split.py +84 -0
  313. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_stamp.py +143 -0
  314. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_watermark.py +51 -0
  315. package/work/vendor/hermes-agent/skills/productivity/powerpoint/LICENSE +21 -0
  316. package/work/vendor/hermes-agent/skills/productivity/powerpoint/SKILL.md +220 -0
  317. package/work/vendor/hermes-agent/skills/productivity/powerpoint/scripts/pptx_create.py +214 -0
  318. package/work/vendor/hermes-agent/skills/productivity/powerpoint/scripts/pptx_edit.py +436 -0
  319. package/work/vendor/hermes-agent/skills/productivity/powerpoint/scripts/pptx_from_template.py +88 -0
  320. package/work/vendor/hermes-agent/skills/productivity/powerpoint/scripts/pptx_read.py +131 -0
  321. package/work/vendor/hermes-agent/skills/productivity/powerpoint/scripts/pptx_render.py +93 -0
  322. package/work/vendor/hermes-agent/skills/productivity/xlsx/LICENSE +21 -0
  323. package/work/vendor/hermes-agent/skills/productivity/xlsx/SKILL.md +196 -0
  324. package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/csv_to_xlsx.py +104 -0
  325. package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/xlsx_create.py +259 -0
  326. package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/xlsx_edit.py +263 -0
  327. package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/xlsx_read.py +160 -0
  328. package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/xlsx_recalc.py +111 -0
  329. package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/xlsx_restructure.py +337 -0
  330. package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/xlsx_to_csv.py +69 -0
  331. package/work/vendor-lock.json +11 -0
@@ -0,0 +1,75 @@
1
+ /**
2
+ * `shell_output`: read, wait on, or kill a process started by `shell({ background: true })`
3
+ * (plan 189 Task 9).
4
+ *
5
+ * Registered only alongside `shell`, and only when the backend can start a process at all. Output is
6
+ * the increment since the previous call for that id, capped like the shell's tail; the process state
7
+ * and exit code are reported, never fabricated.
8
+ */
9
+ import type { JsonObject, ToolDefinition, ToolResult } from "@arnilo/prism";
10
+
11
+ import { CODING_UNSUPPORTED_EFFECT } from "./effects.js";
12
+ import { type BackgroundProcesses, HARD_MAX_BACKGROUND_WAIT_SECONDS } from "./shell-background.js";
13
+
14
+ export interface ShellOutputToolOptions {
15
+ readonly background: BackgroundProcesses;
16
+ }
17
+
18
+ function statusLine(state: string, running: boolean, exitCode: number | null): string {
19
+ if (running) return "[process is still running]";
20
+ if (state === "killed") return "[process killed]";
21
+ return exitCode === null ? "[process ended]" : `[process exited with code ${exitCode}]`;
22
+ }
23
+
24
+ export function createShellOutputTool(options: ShellOutputToolOptions): ToolDefinition {
25
+ const { background } = options;
26
+ return {
27
+ name: "shell_output",
28
+ kind: "execute",
29
+ effect: CODING_UNSUPPORTED_EFFECT,
30
+ effects: ["execute"],
31
+ description: `Read, wait on, or kill a background process started by shell({ background: true }). Returns the output produced since the previous shell_output call for that id (tail-capped), whether the process is still running, and its exit code once it ended. waitSeconds (default 0, max ${HARD_MAX_BACKGROUND_WAIT_SECONDS}) waits for new output or exit; kill: true ends the process. An id from another session is an error.`,
32
+ parameters: {
33
+ type: "object",
34
+ properties: {
35
+ id: { type: "string", description: "Process id returned by shell({ background: true })" },
36
+ waitSeconds: {
37
+ type: "number",
38
+ description: `Seconds to wait for new output or exit (default 0, max ${HARD_MAX_BACKGROUND_WAIT_SECONDS})`,
39
+ },
40
+ kill: { type: "boolean", description: "End the process before reading its remaining output" },
41
+ },
42
+ required: ["id"],
43
+ additionalProperties: false,
44
+ } as JsonObject,
45
+ async execute(args, context): Promise<ToolResult> {
46
+ const toolCallId = context.toolCallId;
47
+ const id = typeof args.id === "string" ? args.id.trim() : "";
48
+ if (id.length === 0) {
49
+ const message = "id is required and must be a non-empty string.";
50
+ return { toolCallId, name: "shell_output", content: [{ type: "text", text: message }], error: { message } };
51
+ }
52
+ const waitSeconds = typeof args.waitSeconds === "number" && Number.isFinite(args.waitSeconds) ? Math.max(0, args.waitSeconds) : 0;
53
+ const kill = args.kill === true;
54
+ const sessionId = context.sessionId ?? "";
55
+ try {
56
+ const result = await background.read({ id, sessionId, waitSeconds, kill });
57
+ const status = statusLine(result.state, result.running, result.exitCode);
58
+ return {
59
+ toolCallId,
60
+ name: "shell_output",
61
+ content: [{ type: "text", text: result.output ? `${result.output}\n\n${status}` : status }],
62
+ metadata: {
63
+ id: result.id,
64
+ state: result.state,
65
+ running: result.running,
66
+ exitCode: result.exitCode,
67
+ },
68
+ };
69
+ } catch (error) {
70
+ const message = error instanceof Error ? error.message : String(error);
71
+ return { toolCallId, name: "shell_output", content: [{ type: "text", text: message }], error: { message } };
72
+ }
73
+ },
74
+ };
75
+ }
@@ -0,0 +1,526 @@
1
+ /**
2
+ * Shell tool: execute a command in the host shell.
3
+ *
4
+ * Behavioral port of pi's core/tools/bash for @arnilo/prism-tools/coding, adapted to
5
+ * Prism's `ToolDefinition` contract. Drops pi's TUI (live `onUpdate` streaming,
6
+ * `renderCall`/`renderResult`) and process-shutdown child tracking; re-ports spawn/
7
+ * kill/waitForChildProcess directly over stdlib so the package stays self-contained.
8
+ *
9
+ * Deviations from pi (documented):
10
+ * - Tool named `"shell"` (pi: `"bash"`).
11
+ * - `timeout` param is in **seconds** (matches pi).
12
+ * - Shell resolution honors `process.env.SHELL` → `/bin/bash` → `sh` (pi forces `/bin/bash`);
13
+ * overridable via `options.shellPath`. Rationale: a host integrating the tool usually wants its
14
+ * login shell respected; `shellPath` still lets a host force bash for fully predictable POSIX.
15
+ * - Non-zero exit is **not** a tool error: returned as a normal `ToolResult` with `exitCode` in
16
+ * `metadata` and a status footer in `content` (pi throws). timeout/abort *are* error results
17
+ * (the command did not complete). Rationale: `ToolResult.error` should mean the tool call failed,
18
+ * not that the command returned non-zero.
19
+ * - Drops detached-child PID tracking (`killTrackedDetachedChildren`): the host owns process
20
+ * lifecycle; the tool kills the tree only on timeout/abort. Drops the stdin command transport
21
+ * - Shell resolution honors `process.env.SHELL` → `/bin/bash` → `sh` (pi forces `/bin/bash`);
22
+ * (argv `-c` only). Default spawn env is the allow-list from `env.ts` (PATH,
23
+ * locale, HOME, TERM) — never the full `process.env` (P1 hardening).
24
+ */
25
+ import { type ChildProcess, spawn } from "node:child_process";
26
+ import { constants, existsSync } from "node:fs";
27
+ import { access as fsAccess } from "node:fs/promises";
28
+ import type { JsonObject, ToolDefinition, ToolResult } from "@arnilo/prism";
29
+ import { CODING_UNSUPPORTED_EFFECT } from "./effects.js";
30
+ import { buildChildEnv, DEFAULT_CHILD_ENV_INHERIT } from "./env.js";
31
+ import type { ExecutionPolicy } from "./execution-policy.js";
32
+ import { enforceExecutionPolicy } from "./execution-policy.js";
33
+ import {
34
+ DEFAULT_MAX_BYTES,
35
+ DEFAULT_MAX_LINES,
36
+ DEFAULT_MAX_TOTAL_OUTPUT_BYTES,
37
+ DEFAULT_SHELL_TIMEOUT_SECONDS,
38
+ HARD_MAX_BYTES,
39
+ HARD_MAX_LINES,
40
+ HARD_MAX_TOTAL_OUTPUT_BYTES,
41
+ HARD_SHELL_TIMEOUT_SECONDS,
42
+ validateCodingLimit,
43
+ } from "./limits.js";
44
+ import { OutputAccumulator, type OutputSnapshot } from "./output-accumulator.js";
45
+ import type { BackgroundProcesses } from "./shell-background.js";
46
+ import { formatSize, type TruncationResult } from "./truncate.js";
47
+
48
+ const EXIT_STDIO_GRACE_MS = 100;
49
+
50
+ export interface ShellConfig {
51
+ shell: string;
52
+ args: string[];
53
+ }
54
+
55
+ export interface BashSpawnContext {
56
+ command: string;
57
+ cwd: string;
58
+ env: NodeJS.ProcessEnv;
59
+ }
60
+
61
+ export type BashSpawnHook = (context: BashSpawnContext) => BashSpawnContext;
62
+
63
+ export interface BashExecOptions {
64
+ onData: (data: Buffer) => void;
65
+ signal?: AbortSignal;
66
+ timeout?: number;
67
+ env?: NodeJS.ProcessEnv;
68
+ }
69
+
70
+ export interface BashOperations {
71
+ /** Execute a command and stream combined output. Resolves to the exit code (null if killed). */
72
+ exec: (command: string, cwd: string, options: BashExecOptions) => Promise<{ exitCode: number | null }>;
73
+ }
74
+
75
+ export interface ShellToolOptions {
76
+ /** Structured pre-execution policy checked before spawn. */
77
+ executionPolicy?: ExecutionPolicy;
78
+ /** Custom operations backend (default: local shell). Override to delegate to remote shells. */
79
+ operations?: BashOperations;
80
+ /** Command prefix prepended to every command (e.g. shell setup commands). */
81
+ commandPrefix?: string;
82
+ /** Explicit shell binary path; overrides SHELL/defaults. */
83
+ shellPath?: string;
84
+ /** Hook to adjust command, cwd, or env before execution. */
85
+ spawnHook?: BashSpawnHook;
86
+ /** Restrict the process environment cloned for the spawn hook / child process to these
87
+ * names (e.g. scrub secrets). Unset uses the default allow-list (PATH, locale, HOME,
88
+ * TERM) — never the full `process.env`. */
89
+ envAllowlist?: readonly string[];
90
+ /** Max lines kept in the tail snapshot (default 2000). */
91
+ maxLines?: number;
92
+ /** Max bytes kept in the tail snapshot (default 50KB). */
93
+ maxBytes?: number;
94
+ /** Default wall timeout in seconds (default 600, hard cap 3600). */
95
+ timeout?: number;
96
+ /** Maximum combined raw stdout/stderr retained or spilled (default 64 MiB). */
97
+ maxTotalOutputBytes?: number;
98
+ /** Temp-file prefix for spilled full output (default "prism-shell"). */
99
+ tempFilePrefix?: string;
100
+ /**
101
+ * Background-process runtime (plan 189 Task 9). When set, `background: true` starts the command
102
+ * through the process-session library and returns its id instead of waiting for exit.
103
+ */
104
+ background?: BackgroundProcesses;
105
+ }
106
+
107
+ // --- spawn internals (re-ported from pi utils/shell.js + utils/child-process.js) ---
108
+
109
+ /** Default allow-list env for the spawn hook / child process (never the full process.env). */
110
+ function pickSpawnEnv(allowlist?: readonly string[]): NodeJS.ProcessEnv {
111
+ if (!allowlist) return buildChildEnv({ inherit: DEFAULT_CHILD_ENV_INHERIT });
112
+ const env: NodeJS.ProcessEnv = {};
113
+ for (const name of allowlist) {
114
+ const value = process.env[name];
115
+ if (value !== undefined) env[name] = value;
116
+ }
117
+ return env;
118
+ }
119
+
120
+ /** Resolve the shell binary + args. shellPath → SHELL env → /bin/bash → sh. */
121
+ export function getShellConfig(customShellPath?: string): ShellConfig {
122
+ if (customShellPath) {
123
+ if (existsSync(customShellPath)) return { shell: customShellPath, args: ["-c"] };
124
+ throw new Error(`Custom shell path not found: ${customShellPath}`);
125
+ }
126
+ const shellEnv = process.env.SHELL;
127
+ if (shellEnv && existsSync(shellEnv)) return { shell: shellEnv, args: ["-c"] };
128
+ if (existsSync("/bin/bash")) return { shell: "/bin/bash", args: ["-c"] };
129
+ return { shell: "sh", args: ["-c"] };
130
+ }
131
+
132
+ /** Kill a process and all its descendants (cross-platform). */
133
+ export function killProcessTree(pid: number): void {
134
+ if (process.platform === "win32") {
135
+ try {
136
+ spawn("taskkill", ["/F", "/T", "/PID", String(pid)], {
137
+ stdio: "ignore",
138
+ detached: true,
139
+ windowsHide: true,
140
+ });
141
+ } catch {
142
+ // ignore — best effort
143
+ }
144
+ return;
145
+ }
146
+ try {
147
+ // child is spawned detached, so it is its own process-group leader: -pid targets the group.
148
+ process.kill(-pid, "SIGKILL");
149
+ } catch {
150
+ try {
151
+ process.kill(pid, "SIGKILL");
152
+ } catch {
153
+ // already dead
154
+ }
155
+ }
156
+ }
157
+
158
+ /**
159
+ * Wait for a child to terminate without hanging on inherited stdio handles held by detached descendants.
160
+ *
161
+ * A short-lived child can `exit` while a detached descendant keeps its stdout/stderr pipe open. After
162
+ * `exit` we wait for the pipes to fall idle: the grace timer is re-armed on every chunk, so an actively
163
+ * writing descendant keeps us reading, while a quiet inherited handle releases us after the grace elapses.
164
+ */
165
+ export function waitForChildProcess(child: ChildProcess): Promise<number | null> {
166
+ return new Promise((resolve, reject) => {
167
+ let settled = false;
168
+ let exited = false;
169
+ let exitCode: number | null = null;
170
+ let postExitTimer: NodeJS.Timeout | undefined;
171
+ let stdoutEnded = child.stdout === null;
172
+ let stderrEnded = child.stderr === null;
173
+ const cleanup = () => {
174
+ if (postExitTimer) {
175
+ clearTimeout(postExitTimer);
176
+ postExitTimer = undefined;
177
+ }
178
+ child.removeListener("error", onError);
179
+ child.removeListener("exit", onExit);
180
+ child.removeListener("close", onClose);
181
+ child.stdout?.removeListener("end", onStdoutEnd);
182
+ child.stderr?.removeListener("end", onStderrEnd);
183
+ child.stdout?.removeListener("data", onData);
184
+ child.stderr?.removeListener("data", onData);
185
+ };
186
+ const finalize = (code: number | null) => {
187
+ if (settled) return;
188
+ settled = true;
189
+ cleanup();
190
+ child.stdout?.destroy();
191
+ child.stderr?.destroy();
192
+ resolve(code);
193
+ };
194
+ const maybeFinalizeAfterExit = () => {
195
+ if (!exited || settled) return;
196
+ if (stdoutEnded && stderrEnded) finalize(exitCode);
197
+ };
198
+ const armIdleTimer = () => {
199
+ if (postExitTimer) clearTimeout(postExitTimer);
200
+ postExitTimer = setTimeout(() => finalize(exitCode), EXIT_STDIO_GRACE_MS);
201
+ };
202
+ const onData = () => {
203
+ if (exited && !settled) armIdleTimer();
204
+ };
205
+ const onStdoutEnd = () => {
206
+ stdoutEnded = true;
207
+ maybeFinalizeAfterExit();
208
+ };
209
+ const onStderrEnd = () => {
210
+ stderrEnded = true;
211
+ maybeFinalizeAfterExit();
212
+ };
213
+ const onError = (err: Error) => {
214
+ if (settled) return;
215
+ settled = true;
216
+ cleanup();
217
+ reject(err);
218
+ };
219
+ const onExit = (code: number | null) => {
220
+ exited = true;
221
+ exitCode = code;
222
+ maybeFinalizeAfterExit();
223
+ if (!settled) armIdleTimer();
224
+ };
225
+ const onClose = (code: number | null) => {
226
+ finalize(code);
227
+ };
228
+ child.stdout?.once("end", onStdoutEnd);
229
+ child.stderr?.once("end", onStderrEnd);
230
+ child.stdout?.on("data", onData);
231
+ child.stderr?.on("data", onData);
232
+ child.once("error", onError);
233
+ child.once("exit", onExit);
234
+ child.once("close", onClose);
235
+ });
236
+ }
237
+
238
+ /** Default local-shell operations: spawn the command in a shell, stream combined stdout+stderr. */
239
+ export function createLocalBashOperations(options?: { shellPath?: string }): BashOperations {
240
+ return {
241
+ exec: async (command, cwd, { onData, signal, timeout, env }) => {
242
+ const timeoutSeconds = validateCodingLimit("timeout", timeout ?? DEFAULT_SHELL_TIMEOUT_SECONDS, HARD_SHELL_TIMEOUT_SECONDS);
243
+ const shellConfig = getShellConfig(options?.shellPath);
244
+ try {
245
+ await fsAccess(cwd, constants.F_OK);
246
+ } catch {
247
+ throw new Error(`Working directory does not exist: ${cwd}\nCannot execute shell commands.`);
248
+ }
249
+ if (signal?.aborted) {
250
+ throw new Error("aborted");
251
+ }
252
+ const child = spawn(shellConfig.shell, [...shellConfig.args, command], {
253
+ cwd,
254
+ detached: process.platform !== "win32",
255
+ env: env ?? buildChildEnv({ inherit: DEFAULT_CHILD_ENV_INHERIT }), // allow-list; never pass process.env through
256
+ stdio: ["ignore", "pipe", "pipe"],
257
+ windowsHide: true,
258
+ });
259
+ let timedOut = false;
260
+ let timeoutHandle: NodeJS.Timeout | undefined;
261
+ const onAbort = () => {
262
+ if (child.pid) killProcessTree(child.pid);
263
+ };
264
+ try {
265
+ timeoutHandle = setTimeout(() => {
266
+ timedOut = true;
267
+ if (child.pid) killProcessTree(child.pid);
268
+ }, timeoutSeconds * 1000);
269
+ child.stdout?.on("data", onData);
270
+ child.stderr?.on("data", onData);
271
+ if (signal) {
272
+ if (signal.aborted) onAbort();
273
+ else signal.addEventListener("abort", onAbort, { once: true });
274
+ }
275
+ const exitCode = await waitForChildProcess(child);
276
+ if (signal?.aborted) throw new Error("aborted");
277
+ if (timedOut) throw new Error(`timeout:${timeoutSeconds}`);
278
+ return { exitCode };
279
+ } finally {
280
+ if (timeoutHandle) clearTimeout(timeoutHandle);
281
+ if (signal) signal.removeEventListener("abort", onAbort);
282
+ }
283
+ },
284
+ };
285
+ }
286
+
287
+ // --- result formatting (adapted from pi, minus TUI) ---
288
+
289
+ function formatOutput(
290
+ snapshot: OutputSnapshot,
291
+ lastLineBytes: number,
292
+ emptyText = "(no output)",
293
+ ): { text: string; truncation: TruncationResult; fullOutputPath?: string } {
294
+ const truncation = snapshot.truncation;
295
+ let text = snapshot.content || emptyText;
296
+ if (truncation.truncated) {
297
+ const startLine = truncation.totalLines - truncation.outputLines + 1;
298
+ const endLine = truncation.totalLines;
299
+ if (truncation.lastLinePartial) {
300
+ const lastLineSize = formatSize(lastLineBytes);
301
+ text += `\n\n[Showing last ${formatSize(truncation.outputBytes)} of line ${endLine} (line is ${lastLineSize}).${snapshot.fullOutputPath ? ` Full output: ${snapshot.fullOutputPath}` : ""}]`;
302
+ } else if (truncation.truncatedBy === "lines") {
303
+ text += `\n\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines}.${snapshot.fullOutputPath ? ` Full output: ${snapshot.fullOutputPath}` : ""}]`;
304
+ } else {
305
+ text += `\n\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines} (${formatSize(truncation.maxBytes)} limit).${snapshot.fullOutputPath ? ` Full output: ${snapshot.fullOutputPath}` : ""}]`;
306
+ }
307
+ }
308
+ return { text, truncation, fullOutputPath: snapshot.fullOutputPath };
309
+ }
310
+
311
+ function appendStatus(text: string, status: string): string {
312
+ return text ? `${text}\n\n${status}` : status;
313
+ }
314
+
315
+ // --- tool factory ---
316
+
317
+ export function createShellTool(cwd: string, options?: ShellToolOptions): ToolDefinition {
318
+ const ops = options?.operations ?? createLocalBashOperations({ shellPath: options?.shellPath });
319
+ const commandPrefix = options?.commandPrefix;
320
+ const spawnHook = options?.spawnHook;
321
+ const maxLines = validateCodingLimit("maxLines", options?.maxLines ?? DEFAULT_MAX_LINES, HARD_MAX_LINES);
322
+ const maxBytes = validateCodingLimit("maxBytes", options?.maxBytes ?? DEFAULT_MAX_BYTES, HARD_MAX_BYTES);
323
+ const defaultTimeout = validateCodingLimit("timeout", options?.timeout ?? DEFAULT_SHELL_TIMEOUT_SECONDS, HARD_SHELL_TIMEOUT_SECONDS);
324
+ const maxTotalOutputBytes = validateCodingLimit(
325
+ "maxTotalOutputBytes",
326
+ options?.maxTotalOutputBytes ?? DEFAULT_MAX_TOTAL_OUTPUT_BYTES,
327
+ HARD_MAX_TOTAL_OUTPUT_BYTES,
328
+ );
329
+ if (maxTotalOutputBytes < maxBytes) throw new Error("maxTotalOutputBytes must be at least maxBytes");
330
+ const tempFilePrefix = options?.tempFilePrefix ?? "prism-shell";
331
+ const background = options?.background;
332
+
333
+ return {
334
+ name: "shell",
335
+ kind: "execute",
336
+ effect: CODING_UNSUPPORTED_EFFECT,
337
+ effects: ["execute"],
338
+ exclusive: true,
339
+ description: `Execute a shell command in the current working directory. Returns combined stdout and stderr. Prefer grep, glob, read, write, or edit for those jobs; use shell for everything else (git, rm, mv, directory listings). Output is truncated to the last ${maxLines} lines or ${maxBytes / 1024}KB and capped at ${formatSize(maxTotalOutputBytes)} total. Truncated successful output is saved to a temp file. Timeout defaults to ${defaultTimeout} seconds.${background ? " With background: true the command keeps running: the call returns a process id and the first output, and shell_output reads, waits on, or kills it." : ""}`,
340
+ parameters: {
341
+ type: "object",
342
+ properties: {
343
+ command: { type: "string", description: "Shell command to execute" },
344
+ timeout: { type: "number", description: `Timeout in seconds (default ${defaultTimeout}, max ${HARD_SHELL_TIMEOUT_SECONDS})` },
345
+ ...(background
346
+ ? { background: { type: "boolean", description: "Start the command and return a process id instead of waiting for it to exit" } }
347
+ : {}),
348
+ },
349
+ required: ["command"],
350
+ additionalProperties: false,
351
+ } as JsonObject,
352
+ async execute(args, context): Promise<ToolResult> {
353
+ const toolCallId = context.toolCallId;
354
+ const command = typeof args.command === "string" ? args.command : "";
355
+ const timeoutInput = typeof args.timeout === "number" ? args.timeout : undefined;
356
+
357
+ if (command.length === 0) {
358
+ return {
359
+ toolCallId,
360
+ name: "shell",
361
+ content: [{ type: "text", text: "Error: command is required and must be a non-empty string." }],
362
+ error: { message: "command is required and must be a non-empty string." },
363
+ };
364
+ }
365
+
366
+ let timeout: number;
367
+ try {
368
+ timeout = validateCodingLimit("timeout", timeoutInput ?? defaultTimeout, HARD_SHELL_TIMEOUT_SECONDS);
369
+ } catch (error) {
370
+ const message = error instanceof Error ? error.message : String(error);
371
+ return { toolCallId, name: "shell", content: [{ type: "text", text: message }], error: { message } };
372
+ }
373
+
374
+ const resolvedCommand = commandPrefix ? `${commandPrefix}\n${command}` : command;
375
+ const baseEnv = pickSpawnEnv(options?.envAllowlist);
376
+ let spawnContext = spawnHook
377
+ ? spawnHook({ command: resolvedCommand, cwd, env: baseEnv })
378
+ : { command: resolvedCommand, cwd, env: baseEnv };
379
+
380
+ const policyCheck = await enforceExecutionPolicy(
381
+ options?.executionPolicy,
382
+ {
383
+ kind: "shell",
384
+ operation: "execute",
385
+ command: spawnContext.command,
386
+ paths: [spawnContext.cwd],
387
+ risk: "high",
388
+ metadata: { timeout, sessionId: context.sessionId, runId: context.runId, signal: context.signal },
389
+ },
390
+ toolCallId,
391
+ "shell",
392
+ );
393
+ if (!policyCheck.allowed) return policyCheck.result;
394
+ if (policyCheck.action.command) {
395
+ spawnContext = { ...spawnContext, command: policyCheck.action.command };
396
+ }
397
+
398
+ if (args.background === true && background !== undefined) {
399
+ try {
400
+ const started = await background.start({
401
+ command: spawnContext.command,
402
+ cwd: spawnContext.cwd,
403
+ env: spawnContext.env,
404
+ sessionId: context.sessionId ?? "",
405
+ });
406
+ const status = started.running
407
+ ? `[process is still running; read it with shell_output({ id: "${started.id}" })]`
408
+ : started.exitCode === null
409
+ ? "[process already ended]"
410
+ : `[process exited with code ${started.exitCode}]`;
411
+ return {
412
+ toolCallId,
413
+ name: "shell",
414
+ content: [{ type: "text", text: `Started process ${started.id}${started.output ? `\n\n${started.output}` : ""}\n\n${status}` }],
415
+ metadata: { processId: started.id, background: true, running: started.running, exitCode: started.exitCode },
416
+ };
417
+ } catch (error) {
418
+ const message = error instanceof Error ? error.message : String(error);
419
+ return { toolCallId, name: "shell", content: [{ type: "text", text: message }], error: { message } };
420
+ }
421
+ }
422
+
423
+ const outputAbort = new AbortController();
424
+ const output = new OutputAccumulator({
425
+ maxLines,
426
+ maxBytes,
427
+ maxTotalOutputBytes,
428
+ tempFilePrefix,
429
+ onLimit: () => outputAbort.abort("output-limit"),
430
+ onStorageError: () => outputAbort.abort("output-storage-error"),
431
+ });
432
+ const signal = context.signal ? AbortSignal.any([context.signal, outputAbort.signal]) : outputAbort.signal;
433
+ let acceptingOutput = true;
434
+ const handleData = (data: Buffer) => {
435
+ if (acceptingOutput) output.append(data);
436
+ };
437
+ const finishOutput = async (retainSpill: boolean): Promise<OutputSnapshot> => {
438
+ acceptingOutput = false;
439
+ output.finish();
440
+ const snapshot = output.snapshot({ persistIfTruncated: true });
441
+ if (retainSpill) {
442
+ await output.closeTempFile();
443
+ return snapshot;
444
+ }
445
+ const hadStorageError = output.hasStorageError();
446
+ try {
447
+ await output.cleanupTempFile();
448
+ } catch (error) {
449
+ if (!hadStorageError) throw error;
450
+ }
451
+ return { ...snapshot, fullOutputPath: undefined };
452
+ };
453
+
454
+ try {
455
+ let exitCode: number | null = null;
456
+ let executionError: unknown;
457
+ try {
458
+ exitCode = (
459
+ await ops.exec(spawnContext.command, spawnContext.cwd, {
460
+ onData: handleData,
461
+ signal,
462
+ timeout,
463
+ env: spawnContext.env,
464
+ })
465
+ ).exitCode;
466
+ } catch (error) {
467
+ executionError = error;
468
+ }
469
+
470
+ if (executionError !== undefined || output.isOutputLimitExceeded() || output.hasStorageError() || context.signal?.aborted) {
471
+ const outputLimitExceeded = output.isOutputLimitExceeded();
472
+ const outputStorageFailed = output.hasStorageError();
473
+ const snapshot = await finishOutput(false);
474
+ const { text } = formatOutput(snapshot, output.getLastLineBytes(), "");
475
+ const rawMessage = executionError instanceof Error ? executionError.message : String(executionError ?? "");
476
+ const status = outputLimitExceeded
477
+ ? `Command output exceeded ${formatSize(maxTotalOutputBytes)} limit`
478
+ : outputStorageFailed
479
+ ? "Command output spill failed"
480
+ : rawMessage.startsWith("timeout:")
481
+ ? `Command timed out after ${rawMessage.split(":")[1]} seconds`
482
+ : context.signal?.aborted
483
+ ? "Command aborted"
484
+ : rawMessage || "Command failed";
485
+ return {
486
+ toolCallId,
487
+ name: "shell",
488
+ content: [{ type: "text", text: appendStatus(text, `[${status}]`) }],
489
+ error: { message: status },
490
+ metadata: {
491
+ exitCode: null,
492
+ truncation: snapshot.truncation,
493
+ totalOutputBytes: output.getTotalRawBytes(),
494
+ outputLimitExceeded,
495
+ outputStorageFailed,
496
+ },
497
+ };
498
+ }
499
+
500
+ const snapshot = await finishOutput(true);
501
+ let text = formatOutput(snapshot, output.getLastLineBytes()).text;
502
+ if (exitCode !== 0 && exitCode !== null) text = appendStatus(text, `[Command exited with code ${exitCode}]`);
503
+ return {
504
+ toolCallId,
505
+ name: "shell",
506
+ content: [{ type: "text", text }],
507
+ metadata: {
508
+ exitCode,
509
+ truncation: snapshot.truncation,
510
+ fullOutputPath: snapshot.fullOutputPath,
511
+ totalOutputBytes: output.getTotalRawBytes(),
512
+ outputLimitExceeded: false,
513
+ },
514
+ };
515
+ } catch (err) {
516
+ try {
517
+ await output.cleanupTempFile();
518
+ } catch {
519
+ /* retain primary error */
520
+ }
521
+ const message = err instanceof Error ? err.message : String(err);
522
+ return { toolCallId, name: "shell", content: [{ type: "text", text: message }], error: { message } };
523
+ }
524
+ },
525
+ };
526
+ }