@dyyz1993/pi-coding-agent 0.70.6 → 0.74.5

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 (275) hide show
  1. package/CHANGELOG.md +266 -80
  2. package/README.md +48 -20
  3. package/dist/bun/cli.d.ts.map +1 -1
  4. package/dist/bun/cli.js +4 -2
  5. package/dist/bun/cli.js.map +1 -1
  6. package/dist/bun/restore-sandbox-env.d.ts +13 -0
  7. package/dist/bun/restore-sandbox-env.d.ts.map +1 -0
  8. package/dist/bun/restore-sandbox-env.js +32 -0
  9. package/dist/bun/restore-sandbox-env.js.map +1 -0
  10. package/dist/cli/args.d.ts +2 -1
  11. package/dist/cli/args.d.ts.map +1 -1
  12. package/dist/cli/args.js +34 -22
  13. package/dist/cli/args.js.map +1 -1
  14. package/dist/cli/list-models.d.ts.map +1 -1
  15. package/dist/cli/list-models.js +2 -1
  16. package/dist/cli/list-models.js.map +1 -1
  17. package/dist/cli.d.ts.map +1 -1
  18. package/dist/cli.js +9 -4
  19. package/dist/cli.js.map +1 -1
  20. package/dist/config.d.ts +16 -8
  21. package/dist/config.d.ts.map +1 -1
  22. package/dist/config.js +238 -66
  23. package/dist/config.js.map +1 -1
  24. package/dist/core/agent-session-runtime.d.ts +10 -0
  25. package/dist/core/agent-session-runtime.d.ts.map +1 -1
  26. package/dist/core/agent-session-runtime.js +14 -0
  27. package/dist/core/agent-session-runtime.js.map +1 -1
  28. package/dist/core/agent-session-services.d.ts +2 -1
  29. package/dist/core/agent-session-services.d.ts.map +1 -1
  30. package/dist/core/agent-session-services.js +1 -0
  31. package/dist/core/agent-session-services.js.map +1 -1
  32. package/dist/core/agent-session.d.ts +37 -26
  33. package/dist/core/agent-session.d.ts.map +1 -1
  34. package/dist/core/agent-session.js +1068 -1116
  35. package/dist/core/agent-session.js.map +1 -1
  36. package/dist/core/auth-guidance.d.ts +5 -0
  37. package/dist/core/auth-guidance.d.ts.map +1 -0
  38. package/dist/core/auth-guidance.js +21 -0
  39. package/dist/core/auth-guidance.js.map +1 -0
  40. package/dist/core/auth-storage.d.ts +9 -0
  41. package/dist/core/auth-storage.d.ts.map +1 -1
  42. package/dist/core/auth-storage.js +20 -1
  43. package/dist/core/auth-storage.js.map +1 -1
  44. package/dist/core/bash-executor.d.ts.map +1 -1
  45. package/dist/core/bash-executor.js +9 -6
  46. package/dist/core/bash-executor.js.map +1 -1
  47. package/dist/core/compaction/compaction.d.ts +0 -1
  48. package/dist/core/compaction/compaction.d.ts.map +1 -1
  49. package/dist/core/compaction/compaction.js.map +1 -1
  50. package/dist/core/export-html/ansi-to-html.d.ts.map +1 -1
  51. package/dist/core/export-html/ansi-to-html.js +1 -1
  52. package/dist/core/export-html/ansi-to-html.js.map +1 -1
  53. package/dist/core/export-html/template.css +53 -4
  54. package/dist/core/export-html/template.js +84 -20
  55. package/dist/core/export-html/tool-renderer.d.ts +0 -6
  56. package/dist/core/export-html/tool-renderer.d.ts.map +1 -1
  57. package/dist/core/export-html/tool-renderer.js +15 -2
  58. package/dist/core/export-html/tool-renderer.js.map +1 -1
  59. package/dist/core/extensions/index.d.ts +1 -1
  60. package/dist/core/extensions/index.d.ts.map +1 -1
  61. package/dist/core/extensions/index.js.map +1 -1
  62. package/dist/core/extensions/loader.d.ts +0 -1
  63. package/dist/core/extensions/loader.d.ts.map +1 -1
  64. package/dist/core/extensions/loader.js +49 -137
  65. package/dist/core/extensions/loader.js.map +1 -1
  66. package/dist/core/extensions/runner.d.ts +24 -20
  67. package/dist/core/extensions/runner.d.ts.map +1 -1
  68. package/dist/core/extensions/runner.js +128 -253
  69. package/dist/core/extensions/runner.js.map +1 -1
  70. package/dist/core/extensions/types.d.ts +88 -60
  71. package/dist/core/extensions/types.d.ts.map +1 -1
  72. package/dist/core/extensions/types.js +10 -0
  73. package/dist/core/extensions/types.js.map +1 -1
  74. package/dist/core/file-store/file-snapshot-manager.d.ts +95 -0
  75. package/dist/core/file-store/file-snapshot-manager.d.ts.map +1 -0
  76. package/dist/core/file-store/file-snapshot-manager.js +508 -0
  77. package/dist/core/file-store/file-snapshot-manager.js.map +1 -0
  78. package/dist/core/file-store/index.d.ts +5 -0
  79. package/dist/core/file-store/index.d.ts.map +1 -0
  80. package/dist/core/file-store/index.js +3 -0
  81. package/dist/core/file-store/index.js.map +1 -0
  82. package/dist/core/messages.d.ts +10 -2
  83. package/dist/core/messages.d.ts.map +1 -1
  84. package/dist/core/messages.js +23 -6
  85. package/dist/core/messages.js.map +1 -1
  86. package/dist/core/model-registry.d.ts +19 -1
  87. package/dist/core/model-registry.d.ts.map +1 -1
  88. package/dist/core/model-registry.js +97 -16
  89. package/dist/core/model-registry.js.map +1 -1
  90. package/dist/core/model-resolver.d.ts.map +1 -1
  91. package/dist/core/model-resolver.js +24 -15
  92. package/dist/core/model-resolver.js.map +1 -1
  93. package/dist/core/package-manager.d.ts +1 -0
  94. package/dist/core/package-manager.d.ts.map +1 -1
  95. package/dist/core/package-manager.js +61 -35
  96. package/dist/core/package-manager.js.map +1 -1
  97. package/dist/core/provider-display-names.d.ts +2 -0
  98. package/dist/core/provider-display-names.d.ts.map +1 -0
  99. package/dist/core/provider-display-names.js +32 -0
  100. package/dist/core/provider-display-names.js.map +1 -0
  101. package/dist/core/resource-loader.d.ts.map +1 -1
  102. package/dist/core/resource-loader.js +9 -21
  103. package/dist/core/resource-loader.js.map +1 -1
  104. package/dist/core/sdk.d.ts +9 -1
  105. package/dist/core/sdk.d.ts.map +1 -1
  106. package/dist/core/sdk.js +39 -18
  107. package/dist/core/sdk.js.map +1 -1
  108. package/dist/core/session-manager.d.ts +27 -17
  109. package/dist/core/session-manager.d.ts.map +1 -1
  110. package/dist/core/session-manager.js +133 -47
  111. package/dist/core/session-manager.js.map +1 -1
  112. package/dist/core/settings-manager.d.ts +22 -3
  113. package/dist/core/settings-manager.d.ts.map +1 -1
  114. package/dist/core/settings-manager.js +54 -6
  115. package/dist/core/settings-manager.js.map +1 -1
  116. package/dist/core/skills.d.ts.map +1 -1
  117. package/dist/core/skills.js +3 -8
  118. package/dist/core/skills.js.map +1 -1
  119. package/dist/core/slash-commands.d.ts.map +1 -1
  120. package/dist/core/slash-commands.js +4 -3
  121. package/dist/core/slash-commands.js.map +1 -1
  122. package/dist/core/tools/bash.d.ts +0 -2
  123. package/dist/core/tools/bash.d.ts.map +1 -1
  124. package/dist/core/tools/bash.js +155 -110
  125. package/dist/core/tools/bash.js.map +1 -1
  126. package/dist/core/tools/edit-diff.d.ts.map +1 -1
  127. package/dist/core/tools/edit-diff.js +3 -2
  128. package/dist/core/tools/edit-diff.js.map +1 -1
  129. package/dist/core/tools/edit.d.ts.map +1 -1
  130. package/dist/core/tools/edit.js +4 -3
  131. package/dist/core/tools/edit.js.map +1 -1
  132. package/dist/core/tools/find.d.ts.map +1 -1
  133. package/dist/core/tools/find.js +1 -1
  134. package/dist/core/tools/find.js.map +1 -1
  135. package/dist/core/tools/grep.d.ts.map +1 -1
  136. package/dist/core/tools/grep.js +1 -1
  137. package/dist/core/tools/grep.js.map +1 -1
  138. package/dist/core/tools/output-accumulator.d.ts +50 -0
  139. package/dist/core/tools/output-accumulator.d.ts.map +1 -0
  140. package/dist/core/tools/output-accumulator.js +178 -0
  141. package/dist/core/tools/output-accumulator.js.map +1 -0
  142. package/dist/core/tools/read.d.ts.map +1 -1
  143. package/dist/core/tools/read.js +70 -13
  144. package/dist/core/tools/read.js.map +1 -1
  145. package/dist/index.d.ts +1 -1
  146. package/dist/index.d.ts.map +1 -1
  147. package/dist/index.js.map +1 -1
  148. package/dist/main.d.ts.map +1 -1
  149. package/dist/main.js +17 -39
  150. package/dist/main.js.map +1 -1
  151. package/dist/migrations.d.ts +1 -1
  152. package/dist/migrations.d.ts.map +1 -1
  153. package/dist/migrations.js +3 -3
  154. package/dist/migrations.js.map +1 -1
  155. package/dist/modes/interactive/components/config-selector.d.ts.map +1 -1
  156. package/dist/modes/interactive/components/config-selector.js +3 -1
  157. package/dist/modes/interactive/components/config-selector.js.map +1 -1
  158. package/dist/modes/interactive/components/extension-selector.d.ts +1 -4
  159. package/dist/modes/interactive/components/extension-selector.d.ts.map +1 -1
  160. package/dist/modes/interactive/components/extension-selector.js +14 -56
  161. package/dist/modes/interactive/components/extension-selector.js.map +1 -1
  162. package/dist/modes/interactive/components/login-dialog.d.ts +5 -1
  163. package/dist/modes/interactive/components/login-dialog.d.ts.map +1 -1
  164. package/dist/modes/interactive/components/login-dialog.js +19 -4
  165. package/dist/modes/interactive/components/login-dialog.js.map +1 -1
  166. package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
  167. package/dist/modes/interactive/components/model-selector.js +1 -1
  168. package/dist/modes/interactive/components/model-selector.js.map +1 -1
  169. package/dist/modes/interactive/components/oauth-selector.d.ts +18 -6
  170. package/dist/modes/interactive/components/oauth-selector.d.ts.map +1 -1
  171. package/dist/modes/interactive/components/oauth-selector.js +93 -25
  172. package/dist/modes/interactive/components/oauth-selector.js.map +1 -1
  173. package/dist/modes/interactive/components/scoped-models-selector.d.ts.map +1 -1
  174. package/dist/modes/interactive/components/scoped-models-selector.js +1 -1
  175. package/dist/modes/interactive/components/scoped-models-selector.js.map +1 -1
  176. package/dist/modes/interactive/components/session-selector.d.ts.map +1 -1
  177. package/dist/modes/interactive/components/session-selector.js +3 -7
  178. package/dist/modes/interactive/components/session-selector.js.map +1 -1
  179. package/dist/modes/interactive/components/settings-selector.d.ts +5 -0
  180. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  181. package/dist/modes/interactive/components/settings-selector.js +53 -1
  182. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  183. package/dist/modes/interactive/interactive-mode.d.ts +20 -4
  184. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  185. package/dist/modes/interactive/interactive-mode.js +423 -186
  186. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  187. package/dist/modes/interactive/theme/dark.json +1 -1
  188. package/dist/modes/interactive/theme/light.json +1 -1
  189. package/dist/modes/print-mode.d.ts +3 -0
  190. package/dist/modes/print-mode.d.ts.map +1 -1
  191. package/dist/modes/print-mode.js +62 -19
  192. package/dist/modes/print-mode.js.map +1 -1
  193. package/dist/modes/rpc/rpc-client.d.ts +80 -60
  194. package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
  195. package/dist/modes/rpc/rpc-client.js +108 -93
  196. package/dist/modes/rpc/rpc-client.js.map +1 -1
  197. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  198. package/dist/modes/rpc/rpc-mode.js +106 -0
  199. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  200. package/dist/modes/rpc/rpc-types.d.ts +115 -0
  201. package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  202. package/dist/modes/rpc/rpc-types.js.map +1 -1
  203. package/dist/package-manager-cli.d.ts.map +1 -1
  204. package/dist/package-manager-cli.js +238 -12
  205. package/dist/package-manager-cli.js.map +1 -1
  206. package/dist/utils/child-process.d.ts +1 -0
  207. package/dist/utils/child-process.d.ts.map +1 -1
  208. package/dist/utils/child-process.js +8 -0
  209. package/dist/utils/child-process.js.map +1 -1
  210. package/dist/utils/clipboard-image.d.ts.map +1 -1
  211. package/dist/utils/clipboard-image.js +2 -2
  212. package/dist/utils/clipboard-image.js.map +1 -1
  213. package/dist/utils/clipboard.d.ts.map +1 -1
  214. package/dist/utils/clipboard.js +84 -45
  215. package/dist/utils/clipboard.js.map +1 -1
  216. package/dist/utils/paths.d.ts +9 -0
  217. package/dist/utils/paths.d.ts.map +1 -1
  218. package/dist/utils/paths.js +31 -0
  219. package/dist/utils/paths.js.map +1 -1
  220. package/dist/utils/pi-user-agent.d.ts +2 -0
  221. package/dist/utils/pi-user-agent.d.ts.map +1 -0
  222. package/dist/utils/pi-user-agent.js +5 -0
  223. package/dist/utils/pi-user-agent.js.map +1 -0
  224. package/dist/utils/structured-output.d.ts +10 -0
  225. package/dist/utils/structured-output.d.ts.map +1 -0
  226. package/dist/utils/structured-output.js +57 -0
  227. package/dist/utils/structured-output.js.map +1 -0
  228. package/dist/utils/tools-manager.d.ts.map +1 -1
  229. package/dist/utils/tools-manager.js +6 -2
  230. package/dist/utils/tools-manager.js.map +1 -1
  231. package/dist/utils/version-check.d.ts +14 -0
  232. package/dist/utils/version-check.d.ts.map +1 -0
  233. package/dist/utils/version-check.js +77 -0
  234. package/dist/utils/version-check.js.map +1 -0
  235. package/docs/compaction.md +14 -14
  236. package/docs/custom-provider.md +40 -31
  237. package/docs/development.md +1 -1
  238. package/docs/docs.json +148 -0
  239. package/docs/extensions.md +116 -56
  240. package/docs/index.md +70 -0
  241. package/docs/json.md +4 -4
  242. package/docs/models.md +150 -3
  243. package/docs/packages.md +10 -5
  244. package/docs/providers.md +62 -17
  245. package/docs/quickstart.md +142 -0
  246. package/docs/rollback-architecture.md +693 -0
  247. package/docs/rollback-test-cases.md +412 -0
  248. package/docs/rpc.md +1 -1
  249. package/docs/sdk.md +26 -26
  250. package/docs/{session.md → session-format.md} +6 -6
  251. package/docs/sessions.md +137 -0
  252. package/docs/settings.md +52 -9
  253. package/docs/termux.md +1 -1
  254. package/docs/themes.md +2 -2
  255. package/docs/tui.md +20 -20
  256. package/docs/usage.md +277 -0
  257. package/examples/extensions/README.md +2 -4
  258. package/examples/extensions/border-status-editor.ts +150 -0
  259. package/examples/extensions/commands.ts +2 -2
  260. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  261. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  262. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  263. package/examples/extensions/dynamic-resources/dynamic.json +1 -1
  264. package/examples/extensions/git-checkpoint.ts +1 -1
  265. package/examples/extensions/handoff.ts +49 -11
  266. package/examples/extensions/plan-mode/index.ts +1 -1
  267. package/examples/extensions/sandbox/package-lock.json +5 -5
  268. package/examples/extensions/sandbox/package.json +1 -1
  269. package/examples/extensions/subagent/agents.ts +126 -0
  270. package/examples/extensions/with-deps/package-lock.json +2 -2
  271. package/examples/extensions/with-deps/package.json +1 -1
  272. package/examples/sdk/README.md +2 -2
  273. package/package.json +7 -16
  274. package/docs/tree.md +0 -233
  275. package/examples/extensions/antigravity-image-gen.ts +0 -418
@@ -1,3 +1,4 @@
1
+ import { APP_NAME } from "../config.js";
1
2
  export const BUILTIN_SLASH_COMMANDS = [
2
3
  { name: "settings", description: "Open settings menu" },
3
4
  { name: "model", description: "Select model (opens selector UI)" },
@@ -13,12 +14,12 @@ export const BUILTIN_SLASH_COMMANDS = [
13
14
  { name: "fork", description: "Create a new fork from a previous user message" },
14
15
  { name: "clone", description: "Duplicate the current session at the current position" },
15
16
  { name: "tree", description: "Navigate session tree (switch branches)" },
16
- { name: "login", description: "Login with OAuth provider" },
17
- { name: "logout", description: "Logout from OAuth provider" },
17
+ { name: "login", description: "Configure provider authentication" },
18
+ { name: "logout", description: "Remove provider authentication" },
18
19
  { name: "new", description: "Start a new session" },
19
20
  { name: "compact", description: "Manually compact the session context" },
20
21
  { name: "resume", description: "Resume a different session" },
21
22
  { name: "reload", description: "Reload keybindings, extensions, skills, prompts, and themes" },
22
- { name: "quit", description: "Quit pi" },
23
+ { name: "quit", description: `Quit ${APP_NAME}` },
23
24
  ];
24
25
  //# sourceMappingURL=slash-commands.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"slash-commands.js","sourceRoot":"","sources":["../../src/core/slash-commands.ts"],"names":[],"mappings":"AAgBA,MAAM,CAAC,MAAM,sBAAsB,GAAuC;IACzE,EAAE,IAAI,EAAE,UAAU,EAAE,WAAW,EAAE,oBAAoB,EAAE;IACvD,EAAE,IAAI,EAAE,OAAO,EAAE,WAAW,EAAE,kCAAkC,EAAE;IAClE,EAAE,IAAI,EAAE,eAAe,EAAE,WAAW,EAAE,0CAA0C,EAAE;IAClF,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,8DAA8D,EAAE;IAC/F,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,+CAA+C,EAAE;IAChF,EAAE,IAAI,EAAE,OAAO,EAAE,WAAW,EAAE,uCAAuC,EAAE;IACvE,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,sCAAsC,EAAE;IACrE,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,0BAA0B,EAAE;IACzD,EAAE,IAAI,EAAE,SAAS,EAAE,WAAW,EAAE,6BAA6B,EAAE;IAC/D,EAAE,IAAI,EAAE,WAAW,EAAE,WAAW,EAAE,wBAAwB,EAAE;IAC5D,EAAE,IAAI,EAAE,SAAS,EAAE,WAAW,EAAE,6BAA6B,EAAE;IAC/D,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,gDAAgD,EAAE;IAC/E,EAAE,IAAI,EAAE,OAAO,EAAE,WAAW,EAAE,uDAAuD,EAAE;IACvF,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,yCAAyC,EAAE;IACxE,EAAE,IAAI,EAAE,OAAO,EAAE,WAAW,EAAE,2BAA2B,EAAE;IAC3D,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,4BAA4B,EAAE;IAC7D,EAAE,IAAI,EAAE,KAAK,EAAE,WAAW,EAAE,qBAAqB,EAAE;IACnD,EAAE,IAAI,EAAE,SAAS,EAAE,WAAW,EAAE,sCAAsC,EAAE;IACxE,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,4BAA4B,EAAE;IAC7D,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,6DAA6D,EAAE;IAC9F,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,SAAS,EAAE;CACxC,CAAC","sourcesContent":["import type { SourceInfo } from \"./source-info.js\";\n\nexport type SlashCommandSource = \"extension\" | \"prompt\" | \"skill\";\n\nexport interface SlashCommandInfo {\n\tname: string;\n\tdescription?: string;\n\tsource: SlashCommandSource;\n\tsourceInfo: SourceInfo;\n}\n\nexport interface BuiltinSlashCommand {\n\tname: string;\n\tdescription: string;\n}\n\nexport const BUILTIN_SLASH_COMMANDS: ReadonlyArray<BuiltinSlashCommand> = [\n\t{ name: \"settings\", description: \"Open settings menu\" },\n\t{ name: \"model\", description: \"Select model (opens selector UI)\" },\n\t{ name: \"scoped-models\", description: \"Enable/disable models for Ctrl+P cycling\" },\n\t{ name: \"export\", description: \"Export session (HTML default, or specify path: .html/.jsonl)\" },\n\t{ name: \"import\", description: \"Import and resume a session from a JSONL file\" },\n\t{ name: \"share\", description: \"Share session as a secret GitHub gist\" },\n\t{ name: \"copy\", description: \"Copy last agent message to clipboard\" },\n\t{ name: \"name\", description: \"Set session display name\" },\n\t{ name: \"session\", description: \"Show session info and stats\" },\n\t{ name: \"changelog\", description: \"Show changelog entries\" },\n\t{ name: \"hotkeys\", description: \"Show all keyboard shortcuts\" },\n\t{ name: \"fork\", description: \"Create a new fork from a previous user message\" },\n\t{ name: \"clone\", description: \"Duplicate the current session at the current position\" },\n\t{ name: \"tree\", description: \"Navigate session tree (switch branches)\" },\n\t{ name: \"login\", description: \"Login with OAuth provider\" },\n\t{ name: \"logout\", description: \"Logout from OAuth provider\" },\n\t{ name: \"new\", description: \"Start a new session\" },\n\t{ name: \"compact\", description: \"Manually compact the session context\" },\n\t{ name: \"resume\", description: \"Resume a different session\" },\n\t{ name: \"reload\", description: \"Reload keybindings, extensions, skills, prompts, and themes\" },\n\t{ name: \"quit\", description: \"Quit pi\" },\n];\n"]}
1
+ {"version":3,"file":"slash-commands.js","sourceRoot":"","sources":["../../src/core/slash-commands.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAiBxC,MAAM,CAAC,MAAM,sBAAsB,GAAuC;IACzE,EAAE,IAAI,EAAE,UAAU,EAAE,WAAW,EAAE,oBAAoB,EAAE;IACvD,EAAE,IAAI,EAAE,OAAO,EAAE,WAAW,EAAE,kCAAkC,EAAE;IAClE,EAAE,IAAI,EAAE,eAAe,EAAE,WAAW,EAAE,0CAA0C,EAAE;IAClF,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,8DAA8D,EAAE;IAC/F,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,+CAA+C,EAAE;IAChF,EAAE,IAAI,EAAE,OAAO,EAAE,WAAW,EAAE,uCAAuC,EAAE;IACvE,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,sCAAsC,EAAE;IACrE,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,0BAA0B,EAAE;IACzD,EAAE,IAAI,EAAE,SAAS,EAAE,WAAW,EAAE,6BAA6B,EAAE;IAC/D,EAAE,IAAI,EAAE,WAAW,EAAE,WAAW,EAAE,wBAAwB,EAAE;IAC5D,EAAE,IAAI,EAAE,SAAS,EAAE,WAAW,EAAE,6BAA6B,EAAE;IAC/D,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,gDAAgD,EAAE;IAC/E,EAAE,IAAI,EAAE,OAAO,EAAE,WAAW,EAAE,uDAAuD,EAAE;IACvF,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,yCAAyC,EAAE;IACxE,EAAE,IAAI,EAAE,OAAO,EAAE,WAAW,EAAE,mCAAmC,EAAE;IACnE,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,gCAAgC,EAAE;IACjE,EAAE,IAAI,EAAE,KAAK,EAAE,WAAW,EAAE,qBAAqB,EAAE;IACnD,EAAE,IAAI,EAAE,SAAS,EAAE,WAAW,EAAE,sCAAsC,EAAE;IACxE,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,4BAA4B,EAAE;IAC7D,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,6DAA6D,EAAE;IAC9F,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,QAAQ,QAAQ,EAAE,EAAE;CACjD,CAAC","sourcesContent":["import { APP_NAME } from \"../config.js\";\nimport type { SourceInfo } from \"./source-info.js\";\n\nexport type SlashCommandSource = \"extension\" | \"prompt\" | \"skill\";\n\nexport interface SlashCommandInfo {\n\tname: string;\n\tdescription?: string;\n\tsource: SlashCommandSource;\n\tsourceInfo: SourceInfo;\n}\n\nexport interface BuiltinSlashCommand {\n\tname: string;\n\tdescription: string;\n}\n\nexport const BUILTIN_SLASH_COMMANDS: ReadonlyArray<BuiltinSlashCommand> = [\n\t{ name: \"settings\", description: \"Open settings menu\" },\n\t{ name: \"model\", description: \"Select model (opens selector UI)\" },\n\t{ name: \"scoped-models\", description: \"Enable/disable models for Ctrl+P cycling\" },\n\t{ name: \"export\", description: \"Export session (HTML default, or specify path: .html/.jsonl)\" },\n\t{ name: \"import\", description: \"Import and resume a session from a JSONL file\" },\n\t{ name: \"share\", description: \"Share session as a secret GitHub gist\" },\n\t{ name: \"copy\", description: \"Copy last agent message to clipboard\" },\n\t{ name: \"name\", description: \"Set session display name\" },\n\t{ name: \"session\", description: \"Show session info and stats\" },\n\t{ name: \"changelog\", description: \"Show changelog entries\" },\n\t{ name: \"hotkeys\", description: \"Show all keyboard shortcuts\" },\n\t{ name: \"fork\", description: \"Create a new fork from a previous user message\" },\n\t{ name: \"clone\", description: \"Duplicate the current session at the current position\" },\n\t{ name: \"tree\", description: \"Navigate session tree (switch branches)\" },\n\t{ name: \"login\", description: \"Configure provider authentication\" },\n\t{ name: \"logout\", description: \"Remove provider authentication\" },\n\t{ name: \"new\", description: \"Start a new session\" },\n\t{ name: \"compact\", description: \"Manually compact the session context\" },\n\t{ name: \"resume\", description: \"Resume a different session\" },\n\t{ name: \"reload\", description: \"Reload keybindings, extensions, skills, prompts, and themes\" },\n\t{ name: \"quit\", description: `Quit ${APP_NAME}` },\n];\n"]}
@@ -4,9 +4,7 @@ import type { ToolDefinition } from "../extensions/types.js";
4
4
  import { type TruncationResult } from "./truncate.js";
5
5
  declare const bashSchema: Type.TObject<{
6
6
  command: Type.TString;
7
- description: Type.TString;
8
7
  timeout: Type.TOptional<Type.TNumber>;
9
- backgroundAfter: Type.TOptional<Type.TNumber>;
10
8
  }>;
11
9
  export type BashToolInput = Static<typeof bashSchema>;
12
10
  export interface BashToolDetails {
@@ -1 +1 @@
1
- {"version":3,"file":"bash.d.ts","sourceRoot":"","sources":["../../../src/core/tools/bash.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AAEzD,OAAO,EAAE,KAAK,MAAM,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AAM5C,OAAO,KAAK,EAAE,cAAc,EAA2B,MAAM,wBAAwB,CAAC;AAKtF,OAAO,EAAoD,KAAK,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAIxG,QAAA,MAAM,UAAU;;;;;EAcd,CAAC;AAEH,MAAM,MAAM,aAAa,GAAG,MAAM,CAAC,OAAO,UAAU,CAAC,CAAC;AAEtD,MAAM,WAAW,eAAe;IAC/B,UAAU,CAAC,EAAE,gBAAgB,CAAC;IAC9B,cAAc,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;GAGG;AACH,MAAM,WAAW,cAAc;IAC9B;;;;;;OAMG;IACH,IAAI,EAAE,CACL,OAAO,EAAE,MAAM,EACf,GAAG,EAAE,MAAM,EACX,OAAO,EAAE;QACR,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;QAC/B,MAAM,CAAC,EAAE,WAAW,CAAC;QACrB,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;KACxB,KACG,OAAO,CAAC;QAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC,CAAC;CAC1C;AAED;;;;;GAKG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,CAAC,EAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,cAAc,CA4C1F;AAED,MAAM,WAAW,gBAAgB;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC,UAAU,CAAC;CACvB;AAED,MAAM,MAAM,aAAa,GAAG,CAAC,OAAO,EAAE,gBAAgB,KAAK,gBAAgB,CAAC;AAO5E,MAAM,WAAW,eAAe;IAC/B,oEAAoE;IACpE,UAAU,CAAC,EAAE,cAAc,CAAC;IAC5B,mFAAmF;IACnF,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,iDAAiD;IACjD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,2DAA2D;IAC3D,SAAS,CAAC,EAAE,aAAa,CAAC;CAC1B;AAID,KAAK,eAAe,GAAG;IACtB,SAAS,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9B,OAAO,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,QAAQ,EAAE,MAAM,CAAC,OAAO,GAAG,SAAS,CAAC;CACrC,CAAC;AA0GF,wBAAgB,wBAAwB,CACvC,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,eAAe,GACvB,cAAc,CAAC,OAAO,UAAU,EAAE,eAAe,GAAG,SAAS,EAAE,eAAe,CAAC,CAkJjF;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,eAAe,GAAG,SAAS,CAAC,OAAO,UAAU,CAAC,CAEnG","sourcesContent":["import type { AgentTool } from \"@dyyz1993/pi-agent-core\";\nimport { Container, Text, truncateToWidth } from \"@dyyz1993/pi-tui\";\nimport { type Static, Type } from \"typebox\";\nimport { keyHint } from \"../../modes/interactive/components/keybinding-hints.js\";\nimport { truncateToVisualLines } from \"../../modes/interactive/components/visual-truncate.js\";\nimport { theme } from \"../../modes/interactive/theme/theme.js\";\nimport { waitForChildProcess } from \"../../utils/child-process.js\";\nimport { getShellEnv } from \"../../utils/shell.js\";\nimport type { ToolDefinition, ToolRenderResultOptions } from \"../extensions/types.js\";\nimport { OutputCollector } from \"./output-collector.js\";\nimport { getTextOutput, invalidArgText, str } from \"./render-utils.js\";\nimport { spawnManagedProcess } from \"./spawn-managed.js\";\nimport { wrapToolDefinition } from \"./tool-definition-wrapper.js\";\nimport { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, type TruncationResult } from \"./truncate.js\";\n\nconst DEFAULT_TIMEOUT_SECONDS = 300;\n\nconst bashSchema = Type.Object({\n\tcommand: Type.String({ description: \"Bash command to execute\" }),\n\tdescription: Type.String({ description: \"Clear, concise description of what this command does in 5-10 words\" }),\n\ttimeout: Type.Optional(\n\t\tType.Number({\n\t\t\tdescription: `Hard timeout in seconds. Process is killed if still running after this duration. Defaults to ${DEFAULT_TIMEOUT_SECONDS}s (5 minutes). Acts as a safety net to prevent zombie processes.`,\n\t\t}),\n\t),\n\tbackgroundAfter: Type.Optional(\n\t\tType.Number({\n\t\t\tdescription:\n\t\t\t\t\"Soft limit in seconds. If the command runs longer than this, it is automatically moved to background instead of blocking the agent. The process continues running; the agent receives a background notification and can proceed with other work. Must be less than timeout if both are set. Use for long-running tasks like builds or installs.\",\n\t\t}),\n\t),\n});\n\nexport type BashToolInput = Static<typeof bashSchema>;\n\nexport interface BashToolDetails {\n\ttruncation?: TruncationResult;\n\tfullOutputPath?: string;\n}\n\n/**\n * Pluggable operations for the bash tool.\n * Override these to delegate command execution to remote systems (for example SSH).\n */\nexport interface BashOperations {\n\t/**\n\t * Execute a command and stream output.\n\t * @param command The command to execute\n\t * @param cwd Working directory\n\t * @param options Execution options\n\t * @returns Promise resolving to exit code (null if killed)\n\t */\n\texec: (\n\t\tcommand: string,\n\t\tcwd: string,\n\t\toptions: {\n\t\t\tonData: (data: Buffer) => void;\n\t\t\tsignal?: AbortSignal;\n\t\t\ttimeout?: number;\n\t\t\tenv?: NodeJS.ProcessEnv;\n\t\t},\n\t) => Promise<{ exitCode: number | null }>;\n}\n\n/**\n * Create bash operations using pi's built-in local shell execution backend.\n *\n * This is useful for extensions that intercept user_bash and still want pi's\n * standard local shell behavior while wrapping or rewriting commands.\n */\nexport function createLocalBashOperations(options?: { shellPath?: string }): BashOperations {\n\treturn {\n\t\texec: (command, cwd, { onData, signal, timeout, env }) => {\n\t\t\treturn new Promise((resolve, reject) => {\n\t\t\t\tconst result = spawnManagedProcess({\n\t\t\t\t\tcommand,\n\t\t\t\t\tcwd,\n\t\t\t\t\ttimeout,\n\t\t\t\t\tsignal,\n\t\t\t\t\tstdin: \"ignore\",\n\t\t\t\t\tenv,\n\t\t\t\t\tshellPath: options?.shellPath,\n\t\t\t\t});\n\n\t\t\t\tif (result instanceof Error) {\n\t\t\t\t\treject(result);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tconst { child, cleanup, isTimedOut } = result;\n\n\t\t\t\tchild.stdout?.on(\"data\", onData);\n\t\t\t\tchild.stderr?.on(\"data\", onData);\n\n\t\t\t\twaitForChildProcess(child)\n\t\t\t\t\t.then((code) => {\n\t\t\t\t\t\tcleanup();\n\t\t\t\t\t\tif (signal?.aborted) {\n\t\t\t\t\t\t\treject(new Error(\"aborted\"));\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (isTimedOut()) {\n\t\t\t\t\t\t\treject(new Error(`timeout:${timeout}`));\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tresolve({ exitCode: code });\n\t\t\t\t\t})\n\t\t\t\t\t.catch((err) => {\n\t\t\t\t\t\tcleanup();\n\t\t\t\t\t\treject(err);\n\t\t\t\t\t});\n\t\t\t});\n\t\t},\n\t};\n}\n\nexport interface BashSpawnContext {\n\tcommand: string;\n\tcwd: string;\n\tenv: NodeJS.ProcessEnv;\n}\n\nexport type BashSpawnHook = (context: BashSpawnContext) => BashSpawnContext;\n\nfunction resolveSpawnContext(command: string, cwd: string, spawnHook?: BashSpawnHook): BashSpawnContext {\n\tconst baseContext: BashSpawnContext = { command, cwd, env: { ...getShellEnv() } };\n\treturn spawnHook ? spawnHook(baseContext) : baseContext;\n}\n\nexport interface BashToolOptions {\n\t/** Custom operations for command execution. Default: local shell */\n\toperations?: BashOperations;\n\t/** Command prefix prepended to every command (for example shell setup commands) */\n\tcommandPrefix?: string;\n\t/** Optional explicit shell path from settings */\n\tshellPath?: string;\n\t/** Hook to adjust command, cwd, or env before execution */\n\tspawnHook?: BashSpawnHook;\n}\n\nconst BASH_PREVIEW_LINES = 5;\n\ntype BashRenderState = {\n\tstartedAt: number | undefined;\n\tendedAt: number | undefined;\n\tinterval: NodeJS.Timeout | undefined;\n};\n\ntype BashResultRenderState = {\n\tcachedWidth: number | undefined;\n\tcachedLines: string[] | undefined;\n\tcachedSkipped: number | undefined;\n};\n\nclass BashResultRenderComponent extends Container {\n\tstate: BashResultRenderState = {\n\t\tcachedWidth: undefined,\n\t\tcachedLines: undefined,\n\t\tcachedSkipped: undefined,\n\t};\n}\n\nfunction formatDuration(ms: number): string {\n\treturn `${(ms / 1000).toFixed(1)}s`;\n}\n\nfunction formatBashCall(args: { command?: string; description?: string; timeout?: number } | undefined): string {\n\tconst command = str(args?.command);\n\tconst description = str(args?.description);\n\tconst timeout = args?.timeout as number | undefined;\n\tconst timeoutSuffix = timeout ? theme.fg(\"muted\", ` (timeout ${timeout}s)`) : \"\";\n\tconst commandDisplay = command === null ? invalidArgText(theme) : command ? command : theme.fg(\"toolOutput\", \"...\");\n\tconst descPrefix = description ? theme.fg(\"muted\", `${description} `) : \"\";\n\treturn descPrefix + theme.fg(\"toolTitle\", theme.bold(`$ ${commandDisplay}`)) + timeoutSuffix;\n}\n\nfunction rebuildBashResultRenderComponent(\n\tcomponent: BashResultRenderComponent,\n\tresult: {\n\t\tcontent: Array<{ type: string; text?: string; data?: string; mimeType?: string }>;\n\t\tdetails?: BashToolDetails;\n\t},\n\toptions: ToolRenderResultOptions,\n\tshowImages: boolean,\n\tstartedAt: number | undefined,\n\tendedAt: number | undefined,\n): void {\n\tconst state = component.state;\n\tcomponent.clear();\n\n\tconst output = getTextOutput(result as any, showImages).trim();\n\n\tif (output) {\n\t\tconst styledOutput = output\n\t\t\t.split(\"\\n\")\n\t\t\t.map((line) => theme.fg(\"toolOutput\", line))\n\t\t\t.join(\"\\n\");\n\n\t\tif (options.expanded) {\n\t\t\tcomponent.addChild(new Text(`\\n${styledOutput}`, 0, 0));\n\t\t} else {\n\t\t\tcomponent.addChild({\n\t\t\t\trender: (width: number) => {\n\t\t\t\t\tif (state.cachedLines === undefined || state.cachedWidth !== width) {\n\t\t\t\t\t\tconst preview = truncateToVisualLines(styledOutput, BASH_PREVIEW_LINES, width);\n\t\t\t\t\t\tstate.cachedLines = preview.visualLines;\n\t\t\t\t\t\tstate.cachedSkipped = preview.skippedCount;\n\t\t\t\t\t\tstate.cachedWidth = width;\n\t\t\t\t\t}\n\t\t\t\t\tif (state.cachedSkipped && state.cachedSkipped > 0) {\n\t\t\t\t\t\tconst hint =\n\t\t\t\t\t\t\ttheme.fg(\"muted\", `... (${state.cachedSkipped} earlier lines,`) +\n\t\t\t\t\t\t\t` ${keyHint(\"app.tools.expand\", \"to expand\")})`;\n\t\t\t\t\t\treturn [\"\", truncateToWidth(hint, width, \"...\"), ...(state.cachedLines ?? [])];\n\t\t\t\t\t}\n\t\t\t\t\treturn [\"\", ...(state.cachedLines ?? [])];\n\t\t\t\t},\n\t\t\t\tinvalidate: () => {\n\t\t\t\t\tstate.cachedWidth = undefined;\n\t\t\t\t\tstate.cachedLines = undefined;\n\t\t\t\t\tstate.cachedSkipped = undefined;\n\t\t\t\t},\n\t\t\t});\n\t\t}\n\t}\n\n\tconst truncation = result.details?.truncation;\n\tconst fullOutputPath = result.details?.fullOutputPath;\n\tif (truncation?.truncated || fullOutputPath) {\n\t\tconst warnings: string[] = [];\n\t\tif (fullOutputPath) {\n\t\t\twarnings.push(`Full output: ${fullOutputPath}`);\n\t\t}\n\t\tif (truncation?.truncated) {\n\t\t\tif (truncation.truncatedBy === \"lines\") {\n\t\t\t\twarnings.push(`Truncated: showing ${truncation.outputLines} of ${truncation.totalLines} lines`);\n\t\t\t} else {\n\t\t\t\twarnings.push(\n\t\t\t\t\t`Truncated: ${truncation.outputLines} lines shown (${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit)`,\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\t\tcomponent.addChild(new Text(`\\n${theme.fg(\"warning\", `[${warnings.join(\". \")}]`)}`, 0, 0));\n\t}\n\n\tif (startedAt !== undefined) {\n\t\tconst label = options.isPartial ? \"Elapsed\" : \"Took\";\n\t\tconst endTime = endedAt ?? Date.now();\n\t\tcomponent.addChild(new Text(`\\n${theme.fg(\"muted\", `${label} ${formatDuration(endTime - startedAt)}`)}`, 0, 0));\n\t}\n}\n\nexport function createBashToolDefinition(\n\tcwd: string,\n\toptions?: BashToolOptions,\n): ToolDefinition<typeof bashSchema, BashToolDetails | undefined, BashRenderState> {\n\tconst ops = options?.operations ?? createLocalBashOperations({ shellPath: options?.shellPath });\n\tconst commandPrefix = options?.commandPrefix;\n\tconst spawnHook = options?.spawnHook;\n\treturn {\n\t\tname: \"bash\",\n\t\tlabel: \"bash\",\n\t\tdescription: [\n\t\t\t`Execute a bash command in the current working directory. Returns stdout and stderr. Output is truncated to last ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). If truncated, full output is saved to a temp file.`,\n\t\t\t\"\",\n\t\t\t\"Timeout and background behavior:\",\n\t\t\t`- timeout: Hard limit in seconds. Process is killed after this duration. Default: ${DEFAULT_TIMEOUT_SECONDS}s (5 min).`,\n\t\t\t\"- backgroundAfter: Soft limit in seconds. If the command runs longer, it is automatically moved to background. The process keeps running, the agent receives a notification and can continue other work.\",\n\t\t\t\"- If backgroundAfter < timeout: command goes to background first, then gets killed if it reaches timeout.\",\n\t\t\t\"- If backgroundAfter >= timeout (or not set): command runs until timeout, then gets killed.\",\n\t\t\t\"\",\n\t\t\t\"When to use backgroundAfter:\",\n\t\t\t\"- Long builds (npm install, cargo build, docker build): set backgroundAfter to a reasonable time so the agent stays productive.\",\n\t\t\t\"- Quick commands (ls, grep, echo): no need for backgroundAfter.\",\n\t\t\t\"\",\n\t\t\t\"Rules:\",\n\t\t\t\"- ALWAYS provide a description (5-10 words explaining what the command does).\",\n\t\t\t\"- Use the workdir parameter to run commands in a specific directory instead of cd.\",\n\t\t].join(\"\\n\"),\n\t\tpromptSnippet: \"Execute bash commands (ls, grep, find, etc.)\",\n\t\tparameters: bashSchema,\n\t\tasync execute(\n\t\t\t_toolCallId,\n\t\t\t{\n\t\t\t\tcommand,\n\t\t\t\tdescription: _description,\n\t\t\t\ttimeout,\n\t\t\t\tbackgroundAfter: _backgroundAfter,\n\t\t\t}: { command: string; description: string; timeout?: number; backgroundAfter?: number },\n\t\t\tsignal?: AbortSignal,\n\t\t\tonUpdate?,\n\t\t\t_ctx?,\n\t\t) {\n\t\t\tconst effectiveTimeout = timeout ?? DEFAULT_TIMEOUT_SECONDS;\n\t\t\tconst resolvedCommand = commandPrefix ? `${commandPrefix}\\n${command}` : command;\n\t\t\tconst spawnContext = resolveSpawnContext(resolvedCommand, cwd, spawnHook);\n\t\t\tif (onUpdate) {\n\t\t\t\tonUpdate({ content: [], details: undefined });\n\t\t\t}\n\t\t\treturn new Promise((resolve, reject) => {\n\t\t\t\tconst collector = new OutputCollector();\n\n\t\t\t\tconst handleData = (data: Buffer) => {\n\t\t\t\t\tcollector.push(data);\n\t\t\t\t\tif (onUpdate) {\n\t\t\t\t\t\tconst truncation = collector.getTruncation();\n\t\t\t\t\t\tonUpdate({\n\t\t\t\t\t\t\tcontent: [{ type: \"text\", text: truncation.content || \"\" }],\n\t\t\t\t\t\t\tdetails: {\n\t\t\t\t\t\t\t\ttruncation: truncation.truncated ? truncation : undefined,\n\t\t\t\t\t\t\t\tfullOutputPath: collector.fullOutputPath,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t};\n\n\t\t\t\tops.exec(spawnContext.command, spawnContext.cwd, {\n\t\t\t\t\tonData: handleData,\n\t\t\t\t\tsignal,\n\t\t\t\t\ttimeout: effectiveTimeout,\n\t\t\t\t\tenv: spawnContext.env,\n\t\t\t\t})\n\t\t\t\t\t.then(({ exitCode }) => {\n\t\t\t\t\t\tconst truncation = collector.finalize();\n\t\t\t\t\t\tconst fullOutput = collector.getBufferedText();\n\t\t\t\t\t\tlet outputText = truncation.content || \"(no output)\";\n\t\t\t\t\t\tlet details: BashToolDetails | undefined;\n\t\t\t\t\t\tif (truncation.truncated) {\n\t\t\t\t\t\t\tdetails = { truncation, fullOutputPath: collector.fullOutputPath };\n\t\t\t\t\t\t\tconst startLine = truncation.totalLines - truncation.outputLines + 1;\n\t\t\t\t\t\t\tconst endLine = truncation.totalLines;\n\t\t\t\t\t\t\tif (truncation.lastLinePartial) {\n\t\t\t\t\t\t\t\tconst lastLineSize = formatSize(Buffer.byteLength(fullOutput.split(\"\\n\").pop() || \"\", \"utf-8\"));\n\t\t\t\t\t\t\t\toutputText += `\\n\\n[Showing last ${formatSize(truncation.outputBytes)} of line ${endLine} (line is ${lastLineSize}). Full output: ${collector.fullOutputPath}]`;\n\t\t\t\t\t\t\t} else if (truncation.truncatedBy === \"lines\") {\n\t\t\t\t\t\t\t\toutputText += `\\n\\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines}. Full output: ${collector.fullOutputPath}]`;\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\toutputText += `\\n\\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines} (${formatSize(DEFAULT_MAX_BYTES)} limit). Full output: ${collector.fullOutputPath}]`;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (exitCode !== 0 && exitCode !== null) {\n\t\t\t\t\t\t\toutputText += `\\n\\nCommand exited with code ${exitCode}`;\n\t\t\t\t\t\t\treject(new Error(outputText));\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tresolve({ content: [{ type: \"text\", text: outputText }], details });\n\t\t\t\t\t\t}\n\t\t\t\t\t})\n\t\t\t\t\t.catch((err: Error) => {\n\t\t\t\t\t\tcollector.close();\n\t\t\t\t\t\tlet output = collector.getBufferedText();\n\t\t\t\t\t\tif (err.message === \"aborted\") {\n\t\t\t\t\t\t\tif (output) output += \"\\n\\n\";\n\t\t\t\t\t\t\toutput += \"Command aborted\";\n\t\t\t\t\t\t\treject(new Error(output));\n\t\t\t\t\t\t} else if (err.message.startsWith(\"timeout:\")) {\n\t\t\t\t\t\t\tconst timeoutSecs = err.message.split(\":\")[1];\n\t\t\t\t\t\t\tif (output) output += \"\\n\\n\";\n\t\t\t\t\t\t\toutput += `Command timed out after ${timeoutSecs} seconds`;\n\t\t\t\t\t\t\treject(new Error(output));\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\treject(err);\n\t\t\t\t\t\t}\n\t\t\t\t\t});\n\t\t\t});\n\t\t},\n\t\trenderCall(args, _theme, context) {\n\t\t\tconst state = context.state;\n\t\t\tif (context.executionStarted && state.startedAt === undefined) {\n\t\t\t\tstate.startedAt = Date.now();\n\t\t\t\tstate.endedAt = undefined;\n\t\t\t}\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\ttext.setText(formatBashCall(args));\n\t\t\treturn text;\n\t\t},\n\t\trenderResult(result, options, _theme, context) {\n\t\t\tconst state = context.state;\n\t\t\tif (state.startedAt !== undefined && options.isPartial && !state.interval) {\n\t\t\t\tstate.interval = setInterval(() => context.invalidate(), 1000);\n\t\t\t}\n\t\t\tif (!options.isPartial || context.isError) {\n\t\t\t\tstate.endedAt ??= Date.now();\n\t\t\t\tif (state.interval) {\n\t\t\t\t\tclearInterval(state.interval);\n\t\t\t\t\tstate.interval = undefined;\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst component =\n\t\t\t\t(context.lastComponent as BashResultRenderComponent | undefined) ?? new BashResultRenderComponent();\n\t\t\trebuildBashResultRenderComponent(\n\t\t\t\tcomponent,\n\t\t\t\tresult as any,\n\t\t\t\toptions,\n\t\t\t\tcontext.showImages,\n\t\t\t\tstate.startedAt,\n\t\t\t\tstate.endedAt,\n\t\t\t);\n\t\t\tcomponent.invalidate();\n\t\t\treturn component;\n\t\t},\n\t};\n}\n\nexport function createBashTool(cwd: string, options?: BashToolOptions): AgentTool<typeof bashSchema> {\n\treturn wrapToolDefinition(createBashToolDefinition(cwd, options));\n}\n"]}
1
+ {"version":3,"file":"bash.d.ts","sourceRoot":"","sources":["../../../src/core/tools/bash.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AAGzD,OAAO,EAAE,KAAK,MAAM,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AAY5C,OAAO,KAAK,EAAE,cAAc,EAA2B,MAAM,wBAAwB,CAAC;AAItF,OAAO,EAAoD,KAAK,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAExG,QAAA,MAAM,UAAU;;;EAGd,CAAC;AAEH,MAAM,MAAM,aAAa,GAAG,MAAM,CAAC,OAAO,UAAU,CAAC,CAAC;AAEtD,MAAM,WAAW,eAAe;IAC/B,UAAU,CAAC,EAAE,gBAAgB,CAAC;IAC9B,cAAc,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;GAGG;AACH,MAAM,WAAW,cAAc;IAC9B;;;;;;OAMG;IACH,IAAI,EAAE,CACL,OAAO,EAAE,MAAM,EACf,GAAG,EAAE,MAAM,EACX,OAAO,EAAE;QACR,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;QAC/B,MAAM,CAAC,EAAE,WAAW,CAAC;QACrB,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;KACxB,KACG,OAAO,CAAC;QAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC,CAAC;CAC1C;AAED;;;;;GAKG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,CAAC,EAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,cAAc,CA8D1F;AAED,MAAM,WAAW,gBAAgB;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC,UAAU,CAAC;CACvB;AAED,MAAM,MAAM,aAAa,GAAG,CAAC,OAAO,EAAE,gBAAgB,KAAK,gBAAgB,CAAC;AAO5E,MAAM,WAAW,eAAe;IAC/B,oEAAoE;IACpE,UAAU,CAAC,EAAE,cAAc,CAAC;IAC5B,mFAAmF;IACnF,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,iDAAiD;IACjD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,2DAA2D;IAC3D,SAAS,CAAC,EAAE,aAAa,CAAC;CAC1B;AAKD,KAAK,eAAe,GAAG;IACtB,SAAS,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9B,OAAO,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,QAAQ,EAAE,MAAM,CAAC,OAAO,GAAG,SAAS,CAAC;CACrC,CAAC;AAwGF,wBAAgB,wBAAwB,CACvC,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,eAAe,GACvB,cAAc,CAAC,OAAO,UAAU,EAAE,eAAe,GAAG,SAAS,EAAE,eAAe,CAAC,CAyKjF;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,eAAe,GAAG,SAAS,CAAC,OAAO,UAAU,CAAC,CAEnG","sourcesContent":["import { existsSync } from \"node:fs\";\nimport type { AgentTool } from \"@dyyz1993/pi-agent-core\";\nimport { Container, Text, truncateToWidth } from \"@dyyz1993/pi-tui\";\nimport { spawn } from \"child_process\";\nimport { type Static, Type } from \"typebox\";\nimport { keyHint } from \"../../modes/interactive/components/keybinding-hints.js\";\nimport { truncateToVisualLines } from \"../../modes/interactive/components/visual-truncate.js\";\nimport { theme } from \"../../modes/interactive/theme/theme.js\";\nimport { waitForChildProcess } from \"../../utils/child-process.js\";\nimport {\n\tgetShellConfig,\n\tgetShellEnv,\n\tkillProcessTree,\n\ttrackDetachedChildPid,\n\tuntrackDetachedChildPid,\n} from \"../../utils/shell.js\";\nimport type { ToolDefinition, ToolRenderResultOptions } from \"../extensions/types.js\";\nimport { OutputAccumulator } from \"./output-accumulator.js\";\nimport { getTextOutput, invalidArgText, str } from \"./render-utils.js\";\nimport { wrapToolDefinition } from \"./tool-definition-wrapper.js\";\nimport { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, type TruncationResult } from \"./truncate.js\";\n\nconst bashSchema = Type.Object({\n\tcommand: Type.String({ description: \"Bash command to execute\" }),\n\ttimeout: Type.Optional(Type.Number({ description: \"Timeout in seconds (optional, no default timeout)\" })),\n});\n\nexport type BashToolInput = Static<typeof bashSchema>;\n\nexport interface BashToolDetails {\n\ttruncation?: TruncationResult;\n\tfullOutputPath?: string;\n}\n\n/**\n * Pluggable operations for the bash tool.\n * Override these to delegate command execution to remote systems (for example SSH).\n */\nexport interface BashOperations {\n\t/**\n\t * Execute a command and stream output.\n\t * @param command The command to execute\n\t * @param cwd Working directory\n\t * @param options Execution options\n\t * @returns Promise resolving to exit code (null if killed)\n\t */\n\texec: (\n\t\tcommand: string,\n\t\tcwd: string,\n\t\toptions: {\n\t\t\tonData: (data: Buffer) => void;\n\t\t\tsignal?: AbortSignal;\n\t\t\ttimeout?: number;\n\t\t\tenv?: NodeJS.ProcessEnv;\n\t\t},\n\t) => Promise<{ exitCode: number | null }>;\n}\n\n/**\n * Create bash operations using pi's built-in local shell execution backend.\n *\n * This is useful for extensions that intercept user_bash and still want pi's\n * standard local shell behavior while wrapping or rewriting commands.\n */\nexport function createLocalBashOperations(options?: { shellPath?: string }): BashOperations {\n\treturn {\n\t\texec: (command, cwd, { onData, signal, timeout, env }) => {\n\t\t\treturn new Promise((resolve, reject) => {\n\t\t\t\tconst { shell, args } = getShellConfig(options?.shellPath);\n\t\t\t\tif (!existsSync(cwd)) {\n\t\t\t\t\treject(new Error(`Working directory does not exist: ${cwd}\\nCannot execute bash commands.`));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst child = spawn(shell, [...args, command], {\n\t\t\t\t\tcwd,\n\t\t\t\t\tdetached: process.platform !== \"win32\",\n\t\t\t\t\tenv: env ?? getShellEnv(),\n\t\t\t\t\tstdio: [\"ignore\", \"pipe\", \"pipe\"],\n\t\t\t\t});\n\t\t\t\tif (child.pid) trackDetachedChildPid(child.pid);\n\t\t\t\tlet timedOut = false;\n\t\t\t\tlet timeoutHandle: NodeJS.Timeout | undefined;\n\t\t\t\t// Set timeout if provided.\n\t\t\t\tif (timeout !== undefined && timeout > 0) {\n\t\t\t\t\ttimeoutHandle = setTimeout(() => {\n\t\t\t\t\t\ttimedOut = true;\n\t\t\t\t\t\tif (child.pid) killProcessTree(child.pid);\n\t\t\t\t\t}, timeout * 1000);\n\t\t\t\t}\n\t\t\t\t// Stream stdout and stderr.\n\t\t\t\tchild.stdout?.on(\"data\", onData);\n\t\t\t\tchild.stderr?.on(\"data\", onData);\n\t\t\t\t// Handle abort signal by killing the entire process tree.\n\t\t\t\tconst onAbort = () => {\n\t\t\t\t\tif (child.pid) killProcessTree(child.pid);\n\t\t\t\t};\n\t\t\t\tif (signal) {\n\t\t\t\t\tif (signal.aborted) onAbort();\n\t\t\t\t\telse signal.addEventListener(\"abort\", onAbort, { once: true });\n\t\t\t\t}\n\t\t\t\t// Handle shell spawn errors and wait for the process to terminate without hanging\n\t\t\t\t// on inherited stdio handles held by detached descendants.\n\t\t\t\twaitForChildProcess(child)\n\t\t\t\t\t.then((code) => {\n\t\t\t\t\t\tif (child.pid) untrackDetachedChildPid(child.pid);\n\t\t\t\t\t\tif (timeoutHandle) clearTimeout(timeoutHandle);\n\t\t\t\t\t\tif (signal) signal.removeEventListener(\"abort\", onAbort);\n\t\t\t\t\t\tif (signal?.aborted) {\n\t\t\t\t\t\t\treject(new Error(\"aborted\"));\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (timedOut) {\n\t\t\t\t\t\t\treject(new Error(`timeout:${timeout}`));\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tresolve({ exitCode: code });\n\t\t\t\t\t})\n\t\t\t\t\t.catch((err) => {\n\t\t\t\t\t\tif (child.pid) untrackDetachedChildPid(child.pid);\n\t\t\t\t\t\tif (timeoutHandle) clearTimeout(timeoutHandle);\n\t\t\t\t\t\tif (signal) signal.removeEventListener(\"abort\", onAbort);\n\t\t\t\t\t\treject(err);\n\t\t\t\t\t});\n\t\t\t});\n\t\t},\n\t};\n}\n\nexport interface BashSpawnContext {\n\tcommand: string;\n\tcwd: string;\n\tenv: NodeJS.ProcessEnv;\n}\n\nexport type BashSpawnHook = (context: BashSpawnContext) => BashSpawnContext;\n\nfunction resolveSpawnContext(command: string, cwd: string, spawnHook?: BashSpawnHook): BashSpawnContext {\n\tconst baseContext: BashSpawnContext = { command, cwd, env: { ...getShellEnv() } };\n\treturn spawnHook ? spawnHook(baseContext) : baseContext;\n}\n\nexport interface BashToolOptions {\n\t/** Custom operations for command execution. Default: local shell */\n\toperations?: BashOperations;\n\t/** Command prefix prepended to every command (for example shell setup commands) */\n\tcommandPrefix?: string;\n\t/** Optional explicit shell path from settings */\n\tshellPath?: string;\n\t/** Hook to adjust command, cwd, or env before execution */\n\tspawnHook?: BashSpawnHook;\n}\n\nconst BASH_PREVIEW_LINES = 5;\nconst BASH_UPDATE_THROTTLE_MS = 100;\n\ntype BashRenderState = {\n\tstartedAt: number | undefined;\n\tendedAt: number | undefined;\n\tinterval: NodeJS.Timeout | undefined;\n};\n\ntype BashResultRenderState = {\n\tcachedWidth: number | undefined;\n\tcachedLines: string[] | undefined;\n\tcachedSkipped: number | undefined;\n};\n\nclass BashResultRenderComponent extends Container {\n\tstate: BashResultRenderState = {\n\t\tcachedWidth: undefined,\n\t\tcachedLines: undefined,\n\t\tcachedSkipped: undefined,\n\t};\n}\n\nfunction formatDuration(ms: number): string {\n\treturn `${(ms / 1000).toFixed(1)}s`;\n}\n\nfunction formatBashCall(args: { command?: string; timeout?: number } | undefined): string {\n\tconst command = str(args?.command);\n\tconst timeout = args?.timeout as number | undefined;\n\tconst timeoutSuffix = timeout ? theme.fg(\"muted\", ` (timeout ${timeout}s)`) : \"\";\n\tconst commandDisplay = command === null ? invalidArgText(theme) : command ? command : theme.fg(\"toolOutput\", \"...\");\n\treturn theme.fg(\"toolTitle\", theme.bold(`$ ${commandDisplay}`)) + timeoutSuffix;\n}\n\nfunction rebuildBashResultRenderComponent(\n\tcomponent: BashResultRenderComponent,\n\tresult: {\n\t\tcontent: Array<{ type: string; text?: string; data?: string; mimeType?: string }>;\n\t\tdetails?: BashToolDetails;\n\t},\n\toptions: ToolRenderResultOptions,\n\tshowImages: boolean,\n\tstartedAt: number | undefined,\n\tendedAt: number | undefined,\n): void {\n\tconst state = component.state;\n\tcomponent.clear();\n\n\tconst output = getTextOutput(result as any, showImages).trim();\n\n\tif (output) {\n\t\tconst styledOutput = output\n\t\t\t.split(\"\\n\")\n\t\t\t.map((line) => theme.fg(\"toolOutput\", line))\n\t\t\t.join(\"\\n\");\n\n\t\tif (options.expanded) {\n\t\t\tcomponent.addChild(new Text(`\\n${styledOutput}`, 0, 0));\n\t\t} else {\n\t\t\tcomponent.addChild({\n\t\t\t\trender: (width: number) => {\n\t\t\t\t\tif (state.cachedLines === undefined || state.cachedWidth !== width) {\n\t\t\t\t\t\tconst preview = truncateToVisualLines(styledOutput, BASH_PREVIEW_LINES, width);\n\t\t\t\t\t\tstate.cachedLines = preview.visualLines;\n\t\t\t\t\t\tstate.cachedSkipped = preview.skippedCount;\n\t\t\t\t\t\tstate.cachedWidth = width;\n\t\t\t\t\t}\n\t\t\t\t\tif (state.cachedSkipped && state.cachedSkipped > 0) {\n\t\t\t\t\t\tconst hint =\n\t\t\t\t\t\t\ttheme.fg(\"muted\", `... (${state.cachedSkipped} earlier lines,`) +\n\t\t\t\t\t\t\t` ${keyHint(\"app.tools.expand\", \"to expand\")})`;\n\t\t\t\t\t\treturn [\"\", truncateToWidth(hint, width, \"...\"), ...(state.cachedLines ?? [])];\n\t\t\t\t\t}\n\t\t\t\t\treturn [\"\", ...(state.cachedLines ?? [])];\n\t\t\t\t},\n\t\t\t\tinvalidate: () => {\n\t\t\t\t\tstate.cachedWidth = undefined;\n\t\t\t\t\tstate.cachedLines = undefined;\n\t\t\t\t\tstate.cachedSkipped = undefined;\n\t\t\t\t},\n\t\t\t});\n\t\t}\n\t}\n\n\tconst truncation = result.details?.truncation;\n\tconst fullOutputPath = result.details?.fullOutputPath;\n\tif (truncation?.truncated || fullOutputPath) {\n\t\tconst warnings: string[] = [];\n\t\tif (fullOutputPath) {\n\t\t\twarnings.push(`Full output: ${fullOutputPath}`);\n\t\t}\n\t\tif (truncation?.truncated) {\n\t\t\tif (truncation.truncatedBy === \"lines\") {\n\t\t\t\twarnings.push(`Truncated: showing ${truncation.outputLines} of ${truncation.totalLines} lines`);\n\t\t\t} else {\n\t\t\t\twarnings.push(\n\t\t\t\t\t`Truncated: ${truncation.outputLines} lines shown (${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit)`,\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\t\tcomponent.addChild(new Text(`\\n${theme.fg(\"warning\", `[${warnings.join(\". \")}]`)}`, 0, 0));\n\t}\n\n\tif (startedAt !== undefined) {\n\t\tconst label = options.isPartial ? \"Elapsed\" : \"Took\";\n\t\tconst endTime = endedAt ?? Date.now();\n\t\tcomponent.addChild(new Text(`\\n${theme.fg(\"muted\", `${label} ${formatDuration(endTime - startedAt)}`)}`, 0, 0));\n\t}\n}\n\nexport function createBashToolDefinition(\n\tcwd: string,\n\toptions?: BashToolOptions,\n): ToolDefinition<typeof bashSchema, BashToolDetails | undefined, BashRenderState> {\n\tconst ops = options?.operations ?? createLocalBashOperations({ shellPath: options?.shellPath });\n\tconst commandPrefix = options?.commandPrefix;\n\tconst spawnHook = options?.spawnHook;\n\treturn {\n\t\tname: \"bash\",\n\t\tlabel: \"bash\",\n\t\tdescription: `Execute a bash command in the current working directory. Returns stdout and stderr. Output is truncated to last ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). If truncated, full output is saved to a temp file. Optionally provide a timeout in seconds.`,\n\t\tpromptSnippet: \"Execute bash commands (ls, grep, find, etc.)\",\n\t\tparameters: bashSchema,\n\t\tasync execute(\n\t\t\t_toolCallId,\n\t\t\t{ command, timeout }: { command: string; timeout?: number },\n\t\t\tsignal?: AbortSignal,\n\t\t\tonUpdate?,\n\t\t\t_ctx?,\n\t\t) {\n\t\t\tconst resolvedCommand = commandPrefix ? `${commandPrefix}\\n${command}` : command;\n\t\t\tconst spawnContext = resolveSpawnContext(resolvedCommand, cwd, spawnHook);\n\t\t\tconst output = new OutputAccumulator({ tempFilePrefix: \"pi-bash\" });\n\t\t\tlet updateTimer: NodeJS.Timeout | undefined;\n\t\t\tlet updateDirty = false;\n\t\t\tlet lastUpdateAt = 0;\n\n\t\t\tconst emitOutputUpdate = () => {\n\t\t\t\tif (!onUpdate || !updateDirty) return;\n\t\t\t\tupdateDirty = false;\n\t\t\t\tlastUpdateAt = Date.now();\n\t\t\t\tconst snapshot = output.snapshot({ persistIfTruncated: true });\n\t\t\t\tonUpdate({\n\t\t\t\t\tcontent: [{ type: \"text\", text: snapshot.content || \"\" }],\n\t\t\t\t\tdetails: {\n\t\t\t\t\t\ttruncation: snapshot.truncation.truncated ? snapshot.truncation : undefined,\n\t\t\t\t\t\tfullOutputPath: snapshot.fullOutputPath,\n\t\t\t\t\t},\n\t\t\t\t});\n\t\t\t};\n\n\t\t\tconst clearUpdateTimer = () => {\n\t\t\t\tif (updateTimer) {\n\t\t\t\t\tclearTimeout(updateTimer);\n\t\t\t\t\tupdateTimer = undefined;\n\t\t\t\t}\n\t\t\t};\n\n\t\t\tconst scheduleOutputUpdate = () => {\n\t\t\t\tif (!onUpdate) return;\n\t\t\t\tupdateDirty = true;\n\t\t\t\tconst delay = BASH_UPDATE_THROTTLE_MS - (Date.now() - lastUpdateAt);\n\t\t\t\tif (delay <= 0) {\n\t\t\t\t\tclearUpdateTimer();\n\t\t\t\t\temitOutputUpdate();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tupdateTimer ??= setTimeout(() => {\n\t\t\t\t\tupdateTimer = undefined;\n\t\t\t\t\temitOutputUpdate();\n\t\t\t\t}, delay);\n\t\t\t};\n\n\t\t\tif (onUpdate) {\n\t\t\t\tonUpdate({ content: [], details: undefined });\n\t\t\t}\n\n\t\t\tconst handleData = (data: Buffer) => {\n\t\t\t\toutput.append(data);\n\t\t\t\tscheduleOutputUpdate();\n\t\t\t};\n\n\t\t\tconst finishOutput = async () => {\n\t\t\t\toutput.finish();\n\t\t\t\tclearUpdateTimer();\n\t\t\t\temitOutputUpdate();\n\t\t\t\tconst snapshot = output.snapshot({ persistIfTruncated: true });\n\t\t\t\tawait output.closeTempFile();\n\t\t\t\treturn snapshot;\n\t\t\t};\n\n\t\t\tconst formatOutput = (snapshot: Awaited<ReturnType<typeof finishOutput>>, emptyText = \"(no output)\") => {\n\t\t\t\tconst truncation = snapshot.truncation;\n\t\t\t\tlet text = snapshot.content || emptyText;\n\t\t\t\tlet details: BashToolDetails | undefined;\n\t\t\t\tif (truncation.truncated) {\n\t\t\t\t\tdetails = { truncation, fullOutputPath: snapshot.fullOutputPath };\n\t\t\t\t\tconst startLine = truncation.totalLines - truncation.outputLines + 1;\n\t\t\t\t\tconst endLine = truncation.totalLines;\n\t\t\t\t\tif (truncation.lastLinePartial) {\n\t\t\t\t\t\tconst lastLineSize = formatSize(output.getLastLineBytes());\n\t\t\t\t\t\ttext += `\\n\\n[Showing last ${formatSize(truncation.outputBytes)} of line ${endLine} (line is ${lastLineSize}). Full output: ${snapshot.fullOutputPath}]`;\n\t\t\t\t\t} else if (truncation.truncatedBy === \"lines\") {\n\t\t\t\t\t\ttext += `\\n\\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines}. Full output: ${snapshot.fullOutputPath}]`;\n\t\t\t\t\t} else {\n\t\t\t\t\t\ttext += `\\n\\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines} (${formatSize(DEFAULT_MAX_BYTES)} limit). Full output: ${snapshot.fullOutputPath}]`;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn { text, details };\n\t\t\t};\n\n\t\t\tconst appendStatus = (text: string, status: string) => `${text ? `${text}\\n\\n` : \"\"}${status}`;\n\n\t\t\ttry {\n\t\t\t\tlet exitCode: number | null;\n\t\t\t\ttry {\n\t\t\t\t\tconst result = await ops.exec(spawnContext.command, spawnContext.cwd, {\n\t\t\t\t\t\tonData: handleData,\n\t\t\t\t\t\tsignal,\n\t\t\t\t\t\ttimeout,\n\t\t\t\t\t\tenv: spawnContext.env,\n\t\t\t\t\t});\n\t\t\t\t\texitCode = result.exitCode;\n\t\t\t\t} catch (err) {\n\t\t\t\t\tconst snapshot = await finishOutput();\n\t\t\t\t\tconst { text } = formatOutput(snapshot, \"\");\n\t\t\t\t\tif (err instanceof Error && err.message === \"aborted\") {\n\t\t\t\t\t\tthrow new Error(appendStatus(text, \"Command aborted\"));\n\t\t\t\t\t}\n\t\t\t\t\tif (err instanceof Error && err.message.startsWith(\"timeout:\")) {\n\t\t\t\t\t\tconst timeoutSecs = err.message.split(\":\")[1];\n\t\t\t\t\t\tthrow new Error(appendStatus(text, `Command timed out after ${timeoutSecs} seconds`));\n\t\t\t\t\t}\n\t\t\t\t\tthrow err;\n\t\t\t\t}\n\n\t\t\t\tconst snapshot = await finishOutput();\n\t\t\t\tconst { text: outputText, details } = formatOutput(snapshot);\n\t\t\t\tif (exitCode !== 0 && exitCode !== null) {\n\t\t\t\t\tthrow new Error(appendStatus(outputText, `Command exited with code ${exitCode}`));\n\t\t\t\t}\n\t\t\t\treturn { content: [{ type: \"text\", text: outputText }], details };\n\t\t\t} finally {\n\t\t\t\tclearUpdateTimer();\n\t\t\t}\n\t\t},\n\t\trenderCall(args, _theme, context) {\n\t\t\tconst state = context.state;\n\t\t\tif (context.executionStarted && state.startedAt === undefined) {\n\t\t\t\tstate.startedAt = Date.now();\n\t\t\t\tstate.endedAt = undefined;\n\t\t\t}\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\ttext.setText(formatBashCall(args));\n\t\t\treturn text;\n\t\t},\n\t\trenderResult(result, options, _theme, context) {\n\t\t\tconst state = context.state;\n\t\t\tif (state.startedAt !== undefined && options.isPartial && !state.interval) {\n\t\t\t\tstate.interval = setInterval(() => context.invalidate(), 1000);\n\t\t\t}\n\t\t\tif (!options.isPartial || context.isError) {\n\t\t\t\tstate.endedAt ??= Date.now();\n\t\t\t\tif (state.interval) {\n\t\t\t\t\tclearInterval(state.interval);\n\t\t\t\t\tstate.interval = undefined;\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst component =\n\t\t\t\t(context.lastComponent as BashResultRenderComponent | undefined) ?? new BashResultRenderComponent();\n\t\t\trebuildBashResultRenderComponent(\n\t\t\t\tcomponent,\n\t\t\t\tresult as any,\n\t\t\t\toptions,\n\t\t\t\tcontext.showImages,\n\t\t\t\tstate.startedAt,\n\t\t\t\tstate.endedAt,\n\t\t\t);\n\t\t\tcomponent.invalidate();\n\t\t\treturn component;\n\t\t},\n\t};\n}\n\nexport function createBashTool(cwd: string, options?: BashToolOptions): AgentTool<typeof bashSchema> {\n\treturn wrapToolDefinition(createBashToolDefinition(cwd, options));\n}\n"]}
@@ -1,25 +1,19 @@
1
+ import { existsSync } from "node:fs";
1
2
  import { Container, Text, truncateToWidth } from "@dyyz1993/pi-tui";
3
+ import { spawn } from "child_process";
2
4
  import { Type } from "typebox";
3
5
  import { keyHint } from "../../modes/interactive/components/keybinding-hints.js";
4
6
  import { truncateToVisualLines } from "../../modes/interactive/components/visual-truncate.js";
5
7
  import { theme } from "../../modes/interactive/theme/theme.js";
6
8
  import { waitForChildProcess } from "../../utils/child-process.js";
7
- import { getShellEnv } from "../../utils/shell.js";
8
- import { OutputCollector } from "./output-collector.js";
9
+ import { getShellConfig, getShellEnv, killProcessTree, trackDetachedChildPid, untrackDetachedChildPid, } from "../../utils/shell.js";
10
+ import { OutputAccumulator } from "./output-accumulator.js";
9
11
  import { getTextOutput, invalidArgText, str } from "./render-utils.js";
10
- import { spawnManagedProcess } from "./spawn-managed.js";
11
12
  import { wrapToolDefinition } from "./tool-definition-wrapper.js";
12
13
  import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize } from "./truncate.js";
13
- const DEFAULT_TIMEOUT_SECONDS = 300;
14
14
  const bashSchema = Type.Object({
15
15
  command: Type.String({ description: "Bash command to execute" }),
16
- description: Type.String({ description: "Clear, concise description of what this command does in 5-10 words" }),
17
- timeout: Type.Optional(Type.Number({
18
- description: `Hard timeout in seconds. Process is killed if still running after this duration. Defaults to ${DEFAULT_TIMEOUT_SECONDS}s (5 minutes). Acts as a safety net to prevent zombie processes.`,
19
- })),
20
- backgroundAfter: Type.Optional(Type.Number({
21
- description: "Soft limit in seconds. If the command runs longer than this, it is automatically moved to background instead of blocking the agent. The process continues running; the agent receives a background notification and can proceed with other work. Must be less than timeout if both are set. Use for long-running tasks like builds or installs.",
22
- })),
16
+ timeout: Type.Optional(Type.Number({ description: "Timeout in seconds (optional, no default timeout)" })),
23
17
  });
24
18
  /**
25
19
  * Create bash operations using pi's built-in local shell execution backend.
@@ -31,37 +25,70 @@ export function createLocalBashOperations(options) {
31
25
  return {
32
26
  exec: (command, cwd, { onData, signal, timeout, env }) => {
33
27
  return new Promise((resolve, reject) => {
34
- const result = spawnManagedProcess({
35
- command,
28
+ const { shell, args } = getShellConfig(options?.shellPath);
29
+ if (!existsSync(cwd)) {
30
+ reject(new Error(`Working directory does not exist: ${cwd}\nCannot execute bash commands.`));
31
+ return;
32
+ }
33
+ const child = spawn(shell, [...args, command], {
36
34
  cwd,
37
- timeout,
38
- signal,
39
- stdin: "ignore",
40
- env,
41
- shellPath: options?.shellPath,
35
+ detached: process.platform !== "win32",
36
+ env: env ?? getShellEnv(),
37
+ stdio: ["ignore", "pipe", "pipe"],
42
38
  });
43
- if (result instanceof Error) {
44
- reject(result);
45
- return;
39
+ if (child.pid)
40
+ trackDetachedChildPid(child.pid);
41
+ let timedOut = false;
42
+ let timeoutHandle;
43
+ // Set timeout if provided.
44
+ if (timeout !== undefined && timeout > 0) {
45
+ timeoutHandle = setTimeout(() => {
46
+ timedOut = true;
47
+ if (child.pid)
48
+ killProcessTree(child.pid);
49
+ }, timeout * 1000);
46
50
  }
47
- const { child, cleanup, isTimedOut } = result;
51
+ // Stream stdout and stderr.
48
52
  child.stdout?.on("data", onData);
49
53
  child.stderr?.on("data", onData);
54
+ // Handle abort signal by killing the entire process tree.
55
+ const onAbort = () => {
56
+ if (child.pid)
57
+ killProcessTree(child.pid);
58
+ };
59
+ if (signal) {
60
+ if (signal.aborted)
61
+ onAbort();
62
+ else
63
+ signal.addEventListener("abort", onAbort, { once: true });
64
+ }
65
+ // Handle shell spawn errors and wait for the process to terminate without hanging
66
+ // on inherited stdio handles held by detached descendants.
50
67
  waitForChildProcess(child)
51
68
  .then((code) => {
52
- cleanup();
69
+ if (child.pid)
70
+ untrackDetachedChildPid(child.pid);
71
+ if (timeoutHandle)
72
+ clearTimeout(timeoutHandle);
73
+ if (signal)
74
+ signal.removeEventListener("abort", onAbort);
53
75
  if (signal?.aborted) {
54
76
  reject(new Error("aborted"));
55
77
  return;
56
78
  }
57
- if (isTimedOut()) {
79
+ if (timedOut) {
58
80
  reject(new Error(`timeout:${timeout}`));
59
81
  return;
60
82
  }
61
83
  resolve({ exitCode: code });
62
84
  })
63
85
  .catch((err) => {
64
- cleanup();
86
+ if (child.pid)
87
+ untrackDetachedChildPid(child.pid);
88
+ if (timeoutHandle)
89
+ clearTimeout(timeoutHandle);
90
+ if (signal)
91
+ signal.removeEventListener("abort", onAbort);
65
92
  reject(err);
66
93
  });
67
94
  });
@@ -73,6 +100,7 @@ function resolveSpawnContext(command, cwd, spawnHook) {
73
100
  return spawnHook ? spawnHook(baseContext) : baseContext;
74
101
  }
75
102
  const BASH_PREVIEW_LINES = 5;
103
+ const BASH_UPDATE_THROTTLE_MS = 100;
76
104
  class BashResultRenderComponent extends Container {
77
105
  state = {
78
106
  cachedWidth: undefined,
@@ -85,12 +113,10 @@ function formatDuration(ms) {
85
113
  }
86
114
  function formatBashCall(args) {
87
115
  const command = str(args?.command);
88
- const description = str(args?.description);
89
116
  const timeout = args?.timeout;
90
117
  const timeoutSuffix = timeout ? theme.fg("muted", ` (timeout ${timeout}s)`) : "";
91
118
  const commandDisplay = command === null ? invalidArgText(theme) : command ? command : theme.fg("toolOutput", "...");
92
- const descPrefix = description ? theme.fg("muted", `${description} `) : "";
93
- return descPrefix + theme.fg("toolTitle", theme.bold(`$ ${commandDisplay}`)) + timeoutSuffix;
119
+ return theme.fg("toolTitle", theme.bold(`$ ${commandDisplay}`)) + timeoutSuffix;
94
120
  }
95
121
  function rebuildBashResultRenderComponent(component, result, options, showImages, startedAt, endedAt) {
96
122
  const state = component.state;
@@ -158,102 +184,121 @@ export function createBashToolDefinition(cwd, options) {
158
184
  return {
159
185
  name: "bash",
160
186
  label: "bash",
161
- description: [
162
- `Execute a bash command in the current working directory. Returns stdout and stderr. Output is truncated to last ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). If truncated, full output is saved to a temp file.`,
163
- "",
164
- "Timeout and background behavior:",
165
- `- timeout: Hard limit in seconds. Process is killed after this duration. Default: ${DEFAULT_TIMEOUT_SECONDS}s (5 min).`,
166
- "- backgroundAfter: Soft limit in seconds. If the command runs longer, it is automatically moved to background. The process keeps running, the agent receives a notification and can continue other work.",
167
- "- If backgroundAfter < timeout: command goes to background first, then gets killed if it reaches timeout.",
168
- "- If backgroundAfter >= timeout (or not set): command runs until timeout, then gets killed.",
169
- "",
170
- "When to use backgroundAfter:",
171
- "- Long builds (npm install, cargo build, docker build): set backgroundAfter to a reasonable time so the agent stays productive.",
172
- "- Quick commands (ls, grep, echo): no need for backgroundAfter.",
173
- "",
174
- "Rules:",
175
- "- ALWAYS provide a description (5-10 words explaining what the command does).",
176
- "- Use the workdir parameter to run commands in a specific directory instead of cd.",
177
- ].join("\n"),
187
+ description: `Execute a bash command in the current working directory. Returns stdout and stderr. Output is truncated to last ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). If truncated, full output is saved to a temp file. Optionally provide a timeout in seconds.`,
178
188
  promptSnippet: "Execute bash commands (ls, grep, find, etc.)",
179
189
  parameters: bashSchema,
180
- async execute(_toolCallId, { command, description: _description, timeout, backgroundAfter: _backgroundAfter, }, signal, onUpdate, _ctx) {
181
- const effectiveTimeout = timeout ?? DEFAULT_TIMEOUT_SECONDS;
190
+ async execute(_toolCallId, { command, timeout }, signal, onUpdate, _ctx) {
182
191
  const resolvedCommand = commandPrefix ? `${commandPrefix}\n${command}` : command;
183
192
  const spawnContext = resolveSpawnContext(resolvedCommand, cwd, spawnHook);
193
+ const output = new OutputAccumulator({ tempFilePrefix: "pi-bash" });
194
+ let updateTimer;
195
+ let updateDirty = false;
196
+ let lastUpdateAt = 0;
197
+ const emitOutputUpdate = () => {
198
+ if (!onUpdate || !updateDirty)
199
+ return;
200
+ updateDirty = false;
201
+ lastUpdateAt = Date.now();
202
+ const snapshot = output.snapshot({ persistIfTruncated: true });
203
+ onUpdate({
204
+ content: [{ type: "text", text: snapshot.content || "" }],
205
+ details: {
206
+ truncation: snapshot.truncation.truncated ? snapshot.truncation : undefined,
207
+ fullOutputPath: snapshot.fullOutputPath,
208
+ },
209
+ });
210
+ };
211
+ const clearUpdateTimer = () => {
212
+ if (updateTimer) {
213
+ clearTimeout(updateTimer);
214
+ updateTimer = undefined;
215
+ }
216
+ };
217
+ const scheduleOutputUpdate = () => {
218
+ if (!onUpdate)
219
+ return;
220
+ updateDirty = true;
221
+ const delay = BASH_UPDATE_THROTTLE_MS - (Date.now() - lastUpdateAt);
222
+ if (delay <= 0) {
223
+ clearUpdateTimer();
224
+ emitOutputUpdate();
225
+ return;
226
+ }
227
+ updateTimer ??= setTimeout(() => {
228
+ updateTimer = undefined;
229
+ emitOutputUpdate();
230
+ }, delay);
231
+ };
184
232
  if (onUpdate) {
185
233
  onUpdate({ content: [], details: undefined });
186
234
  }
187
- return new Promise((resolve, reject) => {
188
- const collector = new OutputCollector();
189
- const handleData = (data) => {
190
- collector.push(data);
191
- if (onUpdate) {
192
- const truncation = collector.getTruncation();
193
- onUpdate({
194
- content: [{ type: "text", text: truncation.content || "" }],
195
- details: {
196
- truncation: truncation.truncated ? truncation : undefined,
197
- fullOutputPath: collector.fullOutputPath,
198
- },
199
- });
235
+ const handleData = (data) => {
236
+ output.append(data);
237
+ scheduleOutputUpdate();
238
+ };
239
+ const finishOutput = async () => {
240
+ output.finish();
241
+ clearUpdateTimer();
242
+ emitOutputUpdate();
243
+ const snapshot = output.snapshot({ persistIfTruncated: true });
244
+ await output.closeTempFile();
245
+ return snapshot;
246
+ };
247
+ const formatOutput = (snapshot, emptyText = "(no output)") => {
248
+ const truncation = snapshot.truncation;
249
+ let text = snapshot.content || emptyText;
250
+ let details;
251
+ if (truncation.truncated) {
252
+ details = { truncation, fullOutputPath: snapshot.fullOutputPath };
253
+ const startLine = truncation.totalLines - truncation.outputLines + 1;
254
+ const endLine = truncation.totalLines;
255
+ if (truncation.lastLinePartial) {
256
+ const lastLineSize = formatSize(output.getLastLineBytes());
257
+ text += `\n\n[Showing last ${formatSize(truncation.outputBytes)} of line ${endLine} (line is ${lastLineSize}). Full output: ${snapshot.fullOutputPath}]`;
200
258
  }
201
- };
202
- ops.exec(spawnContext.command, spawnContext.cwd, {
203
- onData: handleData,
204
- signal,
205
- timeout: effectiveTimeout,
206
- env: spawnContext.env,
207
- })
208
- .then(({ exitCode }) => {
209
- const truncation = collector.finalize();
210
- const fullOutput = collector.getBufferedText();
211
- let outputText = truncation.content || "(no output)";
212
- let details;
213
- if (truncation.truncated) {
214
- details = { truncation, fullOutputPath: collector.fullOutputPath };
215
- const startLine = truncation.totalLines - truncation.outputLines + 1;
216
- const endLine = truncation.totalLines;
217
- if (truncation.lastLinePartial) {
218
- const lastLineSize = formatSize(Buffer.byteLength(fullOutput.split("\n").pop() || "", "utf-8"));
219
- outputText += `\n\n[Showing last ${formatSize(truncation.outputBytes)} of line ${endLine} (line is ${lastLineSize}). Full output: ${collector.fullOutputPath}]`;
220
- }
221
- else if (truncation.truncatedBy === "lines") {
222
- outputText += `\n\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines}. Full output: ${collector.fullOutputPath}]`;
223
- }
224
- else {
225
- outputText += `\n\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines} (${formatSize(DEFAULT_MAX_BYTES)} limit). Full output: ${collector.fullOutputPath}]`;
226
- }
227
- }
228
- if (exitCode !== 0 && exitCode !== null) {
229
- outputText += `\n\nCommand exited with code ${exitCode}`;
230
- reject(new Error(outputText));
259
+ else if (truncation.truncatedBy === "lines") {
260
+ text += `\n\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines}. Full output: ${snapshot.fullOutputPath}]`;
231
261
  }
232
262
  else {
233
- resolve({ content: [{ type: "text", text: outputText }], details });
263
+ text += `\n\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines} (${formatSize(DEFAULT_MAX_BYTES)} limit). Full output: ${snapshot.fullOutputPath}]`;
234
264
  }
235
- })
236
- .catch((err) => {
237
- collector.close();
238
- let output = collector.getBufferedText();
239
- if (err.message === "aborted") {
240
- if (output)
241
- output += "\n\n";
242
- output += "Command aborted";
243
- reject(new Error(output));
265
+ }
266
+ return { text, details };
267
+ };
268
+ const appendStatus = (text, status) => `${text ? `${text}\n\n` : ""}${status}`;
269
+ try {
270
+ let exitCode;
271
+ try {
272
+ const result = await ops.exec(spawnContext.command, spawnContext.cwd, {
273
+ onData: handleData,
274
+ signal,
275
+ timeout,
276
+ env: spawnContext.env,
277
+ });
278
+ exitCode = result.exitCode;
279
+ }
280
+ catch (err) {
281
+ const snapshot = await finishOutput();
282
+ const { text } = formatOutput(snapshot, "");
283
+ if (err instanceof Error && err.message === "aborted") {
284
+ throw new Error(appendStatus(text, "Command aborted"));
244
285
  }
245
- else if (err.message.startsWith("timeout:")) {
286
+ if (err instanceof Error && err.message.startsWith("timeout:")) {
246
287
  const timeoutSecs = err.message.split(":")[1];
247
- if (output)
248
- output += "\n\n";
249
- output += `Command timed out after ${timeoutSecs} seconds`;
250
- reject(new Error(output));
251
- }
252
- else {
253
- reject(err);
288
+ throw new Error(appendStatus(text, `Command timed out after ${timeoutSecs} seconds`));
254
289
  }
255
- });
256
- });
290
+ throw err;
291
+ }
292
+ const snapshot = await finishOutput();
293
+ const { text: outputText, details } = formatOutput(snapshot);
294
+ if (exitCode !== 0 && exitCode !== null) {
295
+ throw new Error(appendStatus(outputText, `Command exited with code ${exitCode}`));
296
+ }
297
+ return { content: [{ type: "text", text: outputText }], details };
298
+ }
299
+ finally {
300
+ clearUpdateTimer();
301
+ }
257
302
  },
258
303
  renderCall(args, _theme, context) {
259
304
  const state = context.state;