@draht/coding-agent 2026.3.14 → 2026.3.25-1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (345) hide show
  1. package/README.md +45 -30
  2. package/dist/bun/cli.d.ts +3 -0
  3. package/dist/bun/cli.d.ts.map +1 -0
  4. package/dist/bun/cli.js +7 -0
  5. package/dist/bun/cli.js.map +1 -0
  6. package/dist/bun/register-bedrock.d.ts +2 -0
  7. package/dist/bun/register-bedrock.d.ts.map +1 -0
  8. package/dist/bun/register-bedrock.js +4 -0
  9. package/dist/bun/register-bedrock.js.map +1 -0
  10. package/dist/cli/args.d.ts +1 -0
  11. package/dist/cli/args.d.ts.map +1 -1
  12. package/dist/cli/args.js +11 -6
  13. package/dist/cli/args.js.map +1 -1
  14. package/dist/cli/file-processor.d.ts.map +1 -1
  15. package/dist/cli/file-processor.js +4 -0
  16. package/dist/cli/file-processor.js.map +1 -1
  17. package/dist/cli/initial-message.d.ts +18 -0
  18. package/dist/cli/initial-message.d.ts.map +1 -0
  19. package/dist/cli/initial-message.js +22 -0
  20. package/dist/cli/initial-message.js.map +1 -0
  21. package/dist/cli/session-picker.d.ts.map +1 -1
  22. package/dist/cli/session-picker.js +2 -1
  23. package/dist/cli/session-picker.js.map +1 -1
  24. package/dist/cli.d.ts.map +1 -1
  25. package/dist/cli.js +1 -3
  26. package/dist/cli.js.map +1 -1
  27. package/dist/core/agent-session.d.ts +38 -5
  28. package/dist/core/agent-session.d.ts.map +1 -1
  29. package/dist/core/agent-session.js +201 -73
  30. package/dist/core/agent-session.js.map +1 -1
  31. package/dist/core/bash-executor.d.ts +6 -7
  32. package/dist/core/bash-executor.d.ts.map +1 -1
  33. package/dist/core/bash-executor.js +8 -107
  34. package/dist/core/bash-executor.js.map +1 -1
  35. package/dist/core/compaction/branch-summarization.d.ts.map +1 -1
  36. package/dist/core/compaction/branch-summarization.js +1 -0
  37. package/dist/core/compaction/branch-summarization.js.map +1 -1
  38. package/dist/core/compaction/compaction.d.ts.map +1 -1
  39. package/dist/core/compaction/compaction.js +2 -0
  40. package/dist/core/compaction/compaction.js.map +1 -1
  41. package/dist/core/exec.d.ts.map +1 -1
  42. package/dist/core/exec.js +7 -3
  43. package/dist/core/exec.js.map +1 -1
  44. package/dist/core/export-html/index.d.ts +2 -2
  45. package/dist/core/export-html/index.d.ts.map +1 -1
  46. package/dist/core/export-html/index.js +7 -6
  47. package/dist/core/export-html/index.js.map +1 -1
  48. package/dist/core/export-html/template.css +43 -13
  49. package/dist/core/export-html/template.html +1 -0
  50. package/dist/core/export-html/template.js +107 -0
  51. package/dist/core/export-html/tool-renderer.d.ts +2 -2
  52. package/dist/core/export-html/tool-renderer.d.ts.map +1 -1
  53. package/dist/core/export-html/tool-renderer.js +41 -16
  54. package/dist/core/export-html/tool-renderer.js.map +1 -1
  55. package/dist/core/extensions/index.d.ts +4 -3
  56. package/dist/core/extensions/index.d.ts.map +1 -1
  57. package/dist/core/extensions/index.js +1 -1
  58. package/dist/core/extensions/index.js.map +1 -1
  59. package/dist/core/extensions/loader.d.ts.map +1 -1
  60. package/dist/core/extensions/loader.js +16 -6
  61. package/dist/core/extensions/loader.js.map +1 -1
  62. package/dist/core/extensions/runner.d.ts +9 -9
  63. package/dist/core/extensions/runner.d.ts.map +1 -1
  64. package/dist/core/extensions/runner.js +89 -71
  65. package/dist/core/extensions/runner.js.map +1 -1
  66. package/dist/core/extensions/types.d.ts +49 -13
  67. package/dist/core/extensions/types.d.ts.map +1 -1
  68. package/dist/core/extensions/types.js.map +1 -1
  69. package/dist/core/extensions/wrapper.d.ts +4 -11
  70. package/dist/core/extensions/wrapper.d.ts.map +1 -1
  71. package/dist/core/extensions/wrapper.js +6 -86
  72. package/dist/core/extensions/wrapper.js.map +1 -1
  73. package/dist/core/footer-data-provider.d.ts +13 -1
  74. package/dist/core/footer-data-provider.d.ts.map +1 -1
  75. package/dist/core/footer-data-provider.js +155 -37
  76. package/dist/core/footer-data-provider.js.map +1 -1
  77. package/dist/core/index.d.ts +2 -1
  78. package/dist/core/index.d.ts.map +1 -1
  79. package/dist/core/index.js +2 -1
  80. package/dist/core/index.js.map +1 -1
  81. package/dist/core/keybindings.d.ts +270 -50
  82. package/dist/core/keybindings.d.ts.map +1 -1
  83. package/dist/core/keybindings.js +222 -134
  84. package/dist/core/keybindings.js.map +1 -1
  85. package/dist/core/model-registry.d.ts +1 -0
  86. package/dist/core/model-registry.d.ts.map +1 -1
  87. package/dist/core/model-registry.js +49 -23
  88. package/dist/core/model-registry.js.map +1 -1
  89. package/dist/core/model-resolver.d.ts +6 -0
  90. package/dist/core/model-resolver.d.ts.map +1 -1
  91. package/dist/core/model-resolver.js +41 -17
  92. package/dist/core/model-resolver.js.map +1 -1
  93. package/dist/core/output-guard.d.ts +6 -0
  94. package/dist/core/output-guard.d.ts.map +1 -0
  95. package/dist/core/output-guard.js +59 -0
  96. package/dist/core/output-guard.js.map +1 -0
  97. package/dist/core/package-manager.d.ts +22 -1
  98. package/dist/core/package-manager.d.ts.map +1 -1
  99. package/dist/core/package-manager.js +373 -53
  100. package/dist/core/package-manager.js.map +1 -1
  101. package/dist/core/prompt-templates.d.ts +2 -1
  102. package/dist/core/prompt-templates.d.ts.map +1 -1
  103. package/dist/core/prompt-templates.js +39 -39
  104. package/dist/core/prompt-templates.js.map +1 -1
  105. package/dist/core/resolve-config-value.d.ts.map +1 -1
  106. package/dist/core/resolve-config-value.js +43 -8
  107. package/dist/core/resolve-config-value.js.map +1 -1
  108. package/dist/core/resource-loader.d.ts +6 -7
  109. package/dist/core/resource-loader.d.ts.map +1 -1
  110. package/dist/core/resource-loader.js +141 -118
  111. package/dist/core/resource-loader.js.map +1 -1
  112. package/dist/core/sdk.d.ts +3 -3
  113. package/dist/core/sdk.d.ts.map +1 -1
  114. package/dist/core/sdk.js +4 -4
  115. package/dist/core/sdk.js.map +1 -1
  116. package/dist/core/session-manager.d.ts +6 -0
  117. package/dist/core/session-manager.d.ts.map +1 -1
  118. package/dist/core/session-manager.js +9 -10
  119. package/dist/core/session-manager.js.map +1 -1
  120. package/dist/core/settings-manager.d.ts +3 -0
  121. package/dist/core/settings-manager.d.ts.map +1 -1
  122. package/dist/core/settings-manager.js +8 -0
  123. package/dist/core/settings-manager.js.map +1 -1
  124. package/dist/core/skills.d.ts +5 -3
  125. package/dist/core/skills.d.ts.map +1 -1
  126. package/dist/core/skills.js +54 -9
  127. package/dist/core/skills.js.map +1 -1
  128. package/dist/core/slash-commands.d.ts +2 -3
  129. package/dist/core/slash-commands.d.ts.map +1 -1
  130. package/dist/core/slash-commands.js +3 -2
  131. package/dist/core/slash-commands.js.map +1 -1
  132. package/dist/core/source-info.d.ts +18 -0
  133. package/dist/core/source-info.d.ts.map +1 -0
  134. package/dist/core/source-info.js +19 -0
  135. package/dist/core/source-info.js.map +1 -0
  136. package/dist/core/system-prompt.d.ts.map +1 -1
  137. package/dist/core/system-prompt.js +17 -60
  138. package/dist/core/system-prompt.js.map +1 -1
  139. package/dist/core/tools/bash.d.ts +24 -6
  140. package/dist/core/tools/bash.d.ts.map +1 -1
  141. package/dist/core/tools/bash.js +210 -110
  142. package/dist/core/tools/bash.js.map +1 -1
  143. package/dist/core/tools/edit-diff.d.ts.map +1 -1
  144. package/dist/core/tools/edit-diff.js +1 -0
  145. package/dist/core/tools/edit-diff.js.map +1 -1
  146. package/dist/core/tools/edit.d.ts +14 -2
  147. package/dist/core/tools/edit.d.ts.map +1 -1
  148. package/dist/core/tools/edit.js +95 -23
  149. package/dist/core/tools/edit.js.map +1 -1
  150. package/dist/core/tools/file-mutation-queue.d.ts +6 -0
  151. package/dist/core/tools/file-mutation-queue.d.ts.map +1 -0
  152. package/dist/core/tools/file-mutation-queue.js +37 -0
  153. package/dist/core/tools/file-mutation-queue.js.map +1 -0
  154. package/dist/core/tools/find.d.ts +11 -4
  155. package/dist/core/tools/find.d.ts.map +1 -1
  156. package/dist/core/tools/find.js +82 -30
  157. package/dist/core/tools/find.js.map +1 -1
  158. package/dist/core/tools/grep.d.ts +15 -4
  159. package/dist/core/tools/grep.d.ts.map +1 -1
  160. package/dist/core/tools/grep.js +83 -29
  161. package/dist/core/tools/grep.js.map +1 -1
  162. package/dist/core/tools/index.d.ts +58 -19
  163. package/dist/core/tools/index.d.ts.map +1 -1
  164. package/dist/core/tools/index.js +51 -26
  165. package/dist/core/tools/index.js.map +1 -1
  166. package/dist/core/tools/ls.d.ts +9 -3
  167. package/dist/core/tools/ls.d.ts.map +1 -1
  168. package/dist/core/tools/ls.js +67 -13
  169. package/dist/core/tools/ls.js.map +1 -1
  170. package/dist/core/tools/read.d.ts +10 -3
  171. package/dist/core/tools/read.d.ts.map +1 -1
  172. package/dist/core/tools/read.js +110 -51
  173. package/dist/core/tools/read.js.map +1 -1
  174. package/dist/core/tools/render-utils.d.ts +21 -0
  175. package/dist/core/tools/render-utils.d.ts.map +1 -0
  176. package/dist/core/tools/render-utils.js +49 -0
  177. package/dist/core/tools/render-utils.js.map +1 -0
  178. package/dist/core/tools/tool-definition-wrapper.d.ts +14 -0
  179. package/dist/core/tools/tool-definition-wrapper.d.ts.map +1 -0
  180. package/dist/core/tools/tool-definition-wrapper.js +30 -0
  181. package/dist/core/tools/tool-definition-wrapper.js.map +1 -0
  182. package/dist/core/tools/write.d.ts +9 -3
  183. package/dist/core/tools/write.d.ts.map +1 -1
  184. package/dist/core/tools/write.js +168 -30
  185. package/dist/core/tools/write.js.map +1 -1
  186. package/dist/index.d.ts +5 -4
  187. package/dist/index.d.ts.map +1 -1
  188. package/dist/index.js +4 -3
  189. package/dist/index.js.map +1 -1
  190. package/dist/main.d.ts.map +1 -1
  191. package/dist/main.js +105 -226
  192. package/dist/main.js.map +1 -1
  193. package/dist/modes/interactive/components/bash-execution.d.ts +0 -1
  194. package/dist/modes/interactive/components/bash-execution.d.ts.map +1 -1
  195. package/dist/modes/interactive/components/bash-execution.js +22 -9
  196. package/dist/modes/interactive/components/bash-execution.js.map +1 -1
  197. package/dist/modes/interactive/components/bordered-loader.d.ts.map +1 -1
  198. package/dist/modes/interactive/components/bordered-loader.js +1 -1
  199. package/dist/modes/interactive/components/bordered-loader.js.map +1 -1
  200. package/dist/modes/interactive/components/branch-summary-message.d.ts.map +1 -1
  201. package/dist/modes/interactive/components/branch-summary-message.js +2 -2
  202. package/dist/modes/interactive/components/branch-summary-message.js.map +1 -1
  203. package/dist/modes/interactive/components/compaction-summary-message.d.ts.map +1 -1
  204. package/dist/modes/interactive/components/compaction-summary-message.js +2 -2
  205. package/dist/modes/interactive/components/compaction-summary-message.js.map +1 -1
  206. package/dist/modes/interactive/components/config-selector.d.ts.map +1 -1
  207. package/dist/modes/interactive/components/config-selector.js +8 -8
  208. package/dist/modes/interactive/components/config-selector.js.map +1 -1
  209. package/dist/modes/interactive/components/custom-editor.d.ts +3 -3
  210. package/dist/modes/interactive/components/custom-editor.d.ts.map +1 -1
  211. package/dist/modes/interactive/components/custom-editor.js +6 -6
  212. package/dist/modes/interactive/components/custom-editor.js.map +1 -1
  213. package/dist/modes/interactive/components/extension-editor.d.ts.map +1 -1
  214. package/dist/modes/interactive/components/extension-editor.js +9 -9
  215. package/dist/modes/interactive/components/extension-editor.js.map +1 -1
  216. package/dist/modes/interactive/components/extension-input.d.ts.map +1 -1
  217. package/dist/modes/interactive/components/extension-input.js +5 -5
  218. package/dist/modes/interactive/components/extension-input.js.map +1 -1
  219. package/dist/modes/interactive/components/extension-selector.d.ts.map +1 -1
  220. package/dist/modes/interactive/components/extension-selector.js +8 -8
  221. package/dist/modes/interactive/components/extension-selector.js.map +1 -1
  222. package/dist/modes/interactive/components/index.d.ts +1 -1
  223. package/dist/modes/interactive/components/index.d.ts.map +1 -1
  224. package/dist/modes/interactive/components/index.js +1 -1
  225. package/dist/modes/interactive/components/index.js.map +1 -1
  226. package/dist/modes/interactive/components/keybinding-hints.d.ts +3 -36
  227. package/dist/modes/interactive/components/keybinding-hints.d.ts.map +1 -1
  228. package/dist/modes/interactive/components/keybinding-hints.js +5 -44
  229. package/dist/modes/interactive/components/keybinding-hints.js.map +1 -1
  230. package/dist/modes/interactive/components/login-dialog.d.ts.map +1 -1
  231. package/dist/modes/interactive/components/login-dialog.js +6 -6
  232. package/dist/modes/interactive/components/login-dialog.js.map +1 -1
  233. package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
  234. package/dist/modes/interactive/components/model-selector.js +13 -9
  235. package/dist/modes/interactive/components/model-selector.js.map +1 -1
  236. package/dist/modes/interactive/components/oauth-selector.d.ts.map +1 -1
  237. package/dist/modes/interactive/components/oauth-selector.js +6 -6
  238. package/dist/modes/interactive/components/oauth-selector.js.map +1 -1
  239. package/dist/modes/interactive/components/scoped-models-selector.d.ts.map +1 -1
  240. package/dist/modes/interactive/components/scoped-models-selector.js +4 -4
  241. package/dist/modes/interactive/components/scoped-models-selector.js.map +1 -1
  242. package/dist/modes/interactive/components/session-selector.d.ts.map +1 -1
  243. package/dist/modes/interactive/components/session-selector.js +32 -35
  244. package/dist/modes/interactive/components/session-selector.js.map +1 -1
  245. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  246. package/dist/modes/interactive/components/settings-selector.js +5 -1
  247. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  248. package/dist/modes/interactive/components/show-images-selector.d.ts.map +1 -1
  249. package/dist/modes/interactive/components/show-images-selector.js +5 -1
  250. package/dist/modes/interactive/components/show-images-selector.js.map +1 -1
  251. package/dist/modes/interactive/components/skill-invocation-message.d.ts.map +1 -1
  252. package/dist/modes/interactive/components/skill-invocation-message.js +2 -2
  253. package/dist/modes/interactive/components/skill-invocation-message.js.map +1 -1
  254. package/dist/modes/interactive/components/theme-selector.d.ts.map +1 -1
  255. package/dist/modes/interactive/components/theme-selector.js +5 -1
  256. package/dist/modes/interactive/components/theme-selector.js.map +1 -1
  257. package/dist/modes/interactive/components/thinking-selector.d.ts.map +1 -1
  258. package/dist/modes/interactive/components/thinking-selector.js +5 -1
  259. package/dist/modes/interactive/components/thinking-selector.js.map +1 -1
  260. package/dist/modes/interactive/components/tool-execution.d.ts +16 -34
  261. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  262. package/dist/modes/interactive/components/tool-execution.js +128 -636
  263. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  264. package/dist/modes/interactive/components/tree-selector.d.ts.map +1 -1
  265. package/dist/modes/interactive/components/tree-selector.js +27 -16
  266. package/dist/modes/interactive/components/tree-selector.js.map +1 -1
  267. package/dist/modes/interactive/components/user-message-selector.d.ts.map +1 -1
  268. package/dist/modes/interactive/components/user-message-selector.js +6 -6
  269. package/dist/modes/interactive/components/user-message-selector.js.map +1 -1
  270. package/dist/modes/interactive/components/user-message.d.ts.map +1 -1
  271. package/dist/modes/interactive/components/user-message.js +2 -1
  272. package/dist/modes/interactive/components/user-message.js.map +1 -1
  273. package/dist/modes/interactive/interactive-mode.d.ts +7 -11
  274. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  275. package/dist/modes/interactive/interactive-mode.js +353 -214
  276. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  277. package/dist/modes/interactive/theme/theme.d.ts +3 -0
  278. package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  279. package/dist/modes/interactive/theme/theme.js +63 -37
  280. package/dist/modes/interactive/theme/theme.js.map +1 -1
  281. package/dist/modes/print-mode.d.ts.map +1 -1
  282. package/dist/modes/print-mode.js +5 -11
  283. package/dist/modes/print-mode.js.map +1 -1
  284. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  285. package/dist/modes/rpc/rpc-mode.js +27 -17
  286. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  287. package/dist/modes/rpc/rpc-types.d.ts +3 -4
  288. package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  289. package/dist/modes/rpc/rpc-types.js.map +1 -1
  290. package/dist/utils/child-process.d.ts +11 -0
  291. package/dist/utils/child-process.d.ts.map +1 -0
  292. package/dist/utils/child-process.js +78 -0
  293. package/dist/utils/child-process.js.map +1 -0
  294. package/dist/utils/clipboard-image.d.ts.map +1 -1
  295. package/dist/utils/clipboard-image.js +94 -11
  296. package/dist/utils/clipboard-image.js.map +1 -1
  297. package/dist/utils/clipboard-native.d.ts +1 -0
  298. package/dist/utils/clipboard-native.d.ts.map +1 -1
  299. package/dist/utils/clipboard-native.js.map +1 -1
  300. package/dist/utils/clipboard.d.ts +1 -1
  301. package/dist/utils/clipboard.d.ts.map +1 -1
  302. package/dist/utils/clipboard.js +27 -16
  303. package/dist/utils/clipboard.js.map +1 -1
  304. package/dist/utils/exif-orientation.d.ts +5 -0
  305. package/dist/utils/exif-orientation.d.ts.map +1 -0
  306. package/dist/utils/exif-orientation.js +158 -0
  307. package/dist/utils/exif-orientation.js.map +1 -0
  308. package/dist/utils/image-convert.d.ts.map +1 -1
  309. package/dist/utils/image-convert.js +5 -1
  310. package/dist/utils/image-convert.js.map +1 -1
  311. package/dist/utils/image-resize.d.ts +5 -5
  312. package/dist/utils/image-resize.d.ts.map +1 -1
  313. package/dist/utils/image-resize.js +51 -95
  314. package/dist/utils/image-resize.js.map +1 -1
  315. package/dist/utils/tools-manager.d.ts.map +1 -1
  316. package/dist/utils/tools-manager.js +5 -4
  317. package/dist/utils/tools-manager.js.map +1 -1
  318. package/docs/custom-provider.md +6 -2
  319. package/docs/extensions.md +108 -21
  320. package/docs/keybindings.md +103 -112
  321. package/docs/models.md +39 -1
  322. package/docs/packages.md +9 -0
  323. package/docs/providers.md +7 -0
  324. package/docs/rpc.md +15 -6
  325. package/docs/sdk.md +2 -2
  326. package/docs/settings.md +9 -0
  327. package/docs/terminal-setup.md +11 -0
  328. package/docs/tui.md +2 -2
  329. package/examples/extensions/README.md +2 -2
  330. package/examples/extensions/antigravity-image-gen.ts +5 -3
  331. package/examples/extensions/built-in-tool-renderer.ts +8 -8
  332. package/examples/extensions/commands.ts +3 -3
  333. package/examples/extensions/minimal-mode.ts +14 -14
  334. package/examples/extensions/question.ts +2 -2
  335. package/examples/extensions/questionnaire.ts +2 -2
  336. package/examples/extensions/subagent/index.ts +30 -8
  337. package/examples/extensions/titlebar-spinner.ts +2 -2
  338. package/examples/extensions/todo.ts +2 -2
  339. package/examples/extensions/tool-override.ts +9 -7
  340. package/examples/extensions/truncated-tool.ts +8 -5
  341. package/examples/sdk/04-skills.ts +8 -2
  342. package/examples/sdk/08-prompt-templates.ts +8 -2
  343. package/examples/sdk/12-full-control.ts +0 -1
  344. package/examples/sdk/README.md +1 -1
  345. package/package.json +4 -4
@@ -2,12 +2,19 @@ import { randomBytes } from "node:crypto";
2
2
  import { createWriteStream, existsSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
+ import { Container, Text, truncateToWidth } from "@draht/tui";
5
6
  import { Type } from "@sinclair/typebox";
6
7
  import { spawn } from "child_process";
8
+ import { keyHint } from "../../modes/interactive/components/keybinding-hints.js";
9
+ import { truncateToVisualLines } from "../../modes/interactive/components/visual-truncate.js";
10
+ import { theme } from "../../modes/interactive/theme/theme.js";
11
+ import { waitForChildProcess } from "../../utils/child-process.js";
7
12
  import { getShellConfig, getShellEnv, killProcessTree } from "../../utils/shell.js";
13
+ import { getTextOutput, invalidArgText, str } from "./render-utils.js";
14
+ import { wrapToolDefinition } from "./tool-definition-wrapper.js";
8
15
  import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, truncateTail } from "./truncate.js";
9
16
  /**
10
- * Generate a unique temp file path for bash output
17
+ * Generate a unique temp file path for bash output.
11
18
  */
12
19
  function getTempFilePath() {
13
20
  const id = randomBytes(8).toString("hex");
@@ -18,136 +25,205 @@ const bashSchema = Type.Object({
18
25
  timeout: Type.Optional(Type.Number({ description: "Timeout in seconds (optional, no default timeout)" })),
19
26
  });
20
27
  /**
21
- * Default bash operations using local shell
28
+ * Create bash operations using pi's built-in local shell execution backend.
29
+ *
30
+ * This is useful for extensions that intercept user_bash and still want pi's
31
+ * standard local shell behavior while wrapping or rewriting commands.
22
32
  */
23
- const defaultBashOperations = {
24
- exec: (command, cwd, { onData, signal, timeout, env }) => {
25
- return new Promise((resolve, reject) => {
26
- const { shell, args } = getShellConfig();
27
- if (!existsSync(cwd)) {
28
- reject(new Error(`Working directory does not exist: ${cwd}\nCannot execute bash commands.`));
29
- return;
30
- }
31
- const child = spawn(shell, [...args, command], {
32
- cwd,
33
- detached: true,
34
- env: env ?? getShellEnv(),
35
- stdio: ["ignore", "pipe", "pipe"],
36
- });
37
- let timedOut = false;
38
- // Set timeout if provided
39
- let timeoutHandle;
40
- if (timeout !== undefined && timeout > 0) {
41
- timeoutHandle = setTimeout(() => {
42
- timedOut = true;
43
- if (child.pid) {
44
- killProcessTree(child.pid);
45
- }
46
- }, timeout * 1000);
47
- }
48
- // Stream stdout and stderr
49
- if (child.stdout) {
50
- child.stdout.on("data", onData);
51
- }
52
- if (child.stderr) {
53
- child.stderr.on("data", onData);
54
- }
55
- // Handle shell spawn errors
56
- child.on("error", (err) => {
57
- if (timeoutHandle)
58
- clearTimeout(timeoutHandle);
59
- if (signal)
60
- signal.removeEventListener("abort", onAbort);
61
- reject(err);
62
- });
63
- // Handle abort signal - kill entire process tree
64
- const onAbort = () => {
65
- if (child.pid) {
66
- killProcessTree(child.pid);
67
- }
68
- };
69
- if (signal) {
70
- if (signal.aborted) {
71
- onAbort();
72
- }
73
- else {
74
- signal.addEventListener("abort", onAbort, { once: true });
75
- }
76
- }
77
- // Handle process exit
78
- child.on("close", (code) => {
79
- if (timeoutHandle)
80
- clearTimeout(timeoutHandle);
81
- if (signal)
82
- signal.removeEventListener("abort", onAbort);
83
- if (signal?.aborted) {
84
- reject(new Error("aborted"));
33
+ export function createLocalBashOperations() {
34
+ return {
35
+ exec: (command, cwd, { onData, signal, timeout, env }) => {
36
+ return new Promise((resolve, reject) => {
37
+ const { shell, args } = getShellConfig();
38
+ if (!existsSync(cwd)) {
39
+ reject(new Error(`Working directory does not exist: ${cwd}\nCannot execute bash commands.`));
85
40
  return;
86
41
  }
87
- if (timedOut) {
88
- reject(new Error(`timeout:${timeout}`));
89
- return;
42
+ const child = spawn(shell, [...args, command], {
43
+ cwd,
44
+ detached: true,
45
+ env: env ?? getShellEnv(),
46
+ stdio: ["ignore", "pipe", "pipe"],
47
+ });
48
+ let timedOut = false;
49
+ let timeoutHandle;
50
+ // Set timeout if provided.
51
+ if (timeout !== undefined && timeout > 0) {
52
+ timeoutHandle = setTimeout(() => {
53
+ timedOut = true;
54
+ if (child.pid)
55
+ killProcessTree(child.pid);
56
+ }, timeout * 1000);
90
57
  }
91
- resolve({ exitCode: code });
58
+ // Stream stdout and stderr.
59
+ child.stdout?.on("data", onData);
60
+ child.stderr?.on("data", onData);
61
+ // Handle abort signal by killing the entire process tree.
62
+ const onAbort = () => {
63
+ if (child.pid)
64
+ killProcessTree(child.pid);
65
+ };
66
+ if (signal) {
67
+ if (signal.aborted)
68
+ onAbort();
69
+ else
70
+ signal.addEventListener("abort", onAbort, { once: true });
71
+ }
72
+ // Handle shell spawn errors and wait for the process to terminate without hanging
73
+ // on inherited stdio handles held by detached descendants.
74
+ waitForChildProcess(child)
75
+ .then((code) => {
76
+ if (timeoutHandle)
77
+ clearTimeout(timeoutHandle);
78
+ if (signal)
79
+ signal.removeEventListener("abort", onAbort);
80
+ if (signal?.aborted) {
81
+ reject(new Error("aborted"));
82
+ return;
83
+ }
84
+ if (timedOut) {
85
+ reject(new Error(`timeout:${timeout}`));
86
+ return;
87
+ }
88
+ resolve({ exitCode: code });
89
+ })
90
+ .catch((err) => {
91
+ if (timeoutHandle)
92
+ clearTimeout(timeoutHandle);
93
+ if (signal)
94
+ signal.removeEventListener("abort", onAbort);
95
+ reject(err);
96
+ });
92
97
  });
93
- });
94
- },
95
- };
96
- function resolveSpawnContext(command, cwd, spawnHook) {
97
- const baseContext = {
98
- command,
99
- cwd,
100
- env: { ...getShellEnv() },
98
+ },
101
99
  };
100
+ }
101
+ function resolveSpawnContext(command, cwd, spawnHook) {
102
+ const baseContext = { command, cwd, env: { ...getShellEnv() } };
102
103
  return spawnHook ? spawnHook(baseContext) : baseContext;
103
104
  }
104
- export function createBashTool(cwd, options) {
105
- const ops = options?.operations ?? defaultBashOperations;
105
+ const BASH_PREVIEW_LINES = 5;
106
+ class BashResultRenderComponent extends Container {
107
+ state = {
108
+ cachedWidth: undefined,
109
+ cachedLines: undefined,
110
+ cachedSkipped: undefined,
111
+ };
112
+ }
113
+ function formatDuration(ms) {
114
+ return `${(ms / 1000).toFixed(1)}s`;
115
+ }
116
+ function formatBashCall(args) {
117
+ const command = str(args?.command);
118
+ const timeout = args?.timeout;
119
+ const timeoutSuffix = timeout ? theme.fg("muted", ` (timeout ${timeout}s)`) : "";
120
+ const commandDisplay = command === null ? invalidArgText(theme) : command ? command : theme.fg("toolOutput", "...");
121
+ return theme.fg("toolTitle", theme.bold(`$ ${commandDisplay}`)) + timeoutSuffix;
122
+ }
123
+ function rebuildBashResultRenderComponent(component, result, options, showImages, startedAt, endedAt) {
124
+ const state = component.state;
125
+ component.clear();
126
+ const output = getTextOutput(result, showImages).trim();
127
+ if (output) {
128
+ const styledOutput = output
129
+ .split("\n")
130
+ .map((line) => theme.fg("toolOutput", line))
131
+ .join("\n");
132
+ if (options.expanded) {
133
+ component.addChild(new Text(`\n${styledOutput}`, 0, 0));
134
+ }
135
+ else {
136
+ component.addChild({
137
+ render: (width) => {
138
+ if (state.cachedLines === undefined || state.cachedWidth !== width) {
139
+ const preview = truncateToVisualLines(styledOutput, BASH_PREVIEW_LINES, width);
140
+ state.cachedLines = preview.visualLines;
141
+ state.cachedSkipped = preview.skippedCount;
142
+ state.cachedWidth = width;
143
+ }
144
+ if (state.cachedSkipped && state.cachedSkipped > 0) {
145
+ const hint = theme.fg("muted", `... (${state.cachedSkipped} earlier lines,`) +
146
+ ` ${keyHint("app.tools.expand", "to expand")})`;
147
+ return ["", truncateToWidth(hint, width, "..."), ...(state.cachedLines ?? [])];
148
+ }
149
+ return ["", ...(state.cachedLines ?? [])];
150
+ },
151
+ invalidate: () => {
152
+ state.cachedWidth = undefined;
153
+ state.cachedLines = undefined;
154
+ state.cachedSkipped = undefined;
155
+ },
156
+ });
157
+ }
158
+ }
159
+ const truncation = result.details?.truncation;
160
+ const fullOutputPath = result.details?.fullOutputPath;
161
+ if (truncation?.truncated || fullOutputPath) {
162
+ const warnings = [];
163
+ if (fullOutputPath) {
164
+ warnings.push(`Full output: ${fullOutputPath}`);
165
+ }
166
+ if (truncation?.truncated) {
167
+ if (truncation.truncatedBy === "lines") {
168
+ warnings.push(`Truncated: showing ${truncation.outputLines} of ${truncation.totalLines} lines`);
169
+ }
170
+ else {
171
+ warnings.push(`Truncated: ${truncation.outputLines} lines shown (${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit)`);
172
+ }
173
+ }
174
+ component.addChild(new Text(`\n${theme.fg("warning", `[${warnings.join(". ")}]`)}`, 0, 0));
175
+ }
176
+ if (startedAt !== undefined) {
177
+ const label = options.isPartial ? "Elapsed" : "Took";
178
+ const endTime = endedAt ?? Date.now();
179
+ component.addChild(new Text(`\n${theme.fg("muted", `${label} ${formatDuration(endTime - startedAt)}`)}`, 0, 0));
180
+ }
181
+ }
182
+ export function createBashToolDefinition(cwd, options) {
183
+ const ops = options?.operations ?? createLocalBashOperations();
106
184
  const commandPrefix = options?.commandPrefix;
107
185
  const spawnHook = options?.spawnHook;
108
186
  return {
109
187
  name: "bash",
110
188
  label: "bash",
111
189
  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.`,
190
+ promptSnippet: "Execute bash commands (ls, grep, find, etc.)",
112
191
  parameters: bashSchema,
113
- execute: async (_toolCallId, { command, timeout }, signal, onUpdate) => {
114
- // Apply command prefix if configured (e.g., "shopt -s expand_aliases" for alias support)
192
+ async execute(_toolCallId, { command, timeout }, signal, onUpdate, _ctx) {
115
193
  const resolvedCommand = commandPrefix ? `${commandPrefix}\n${command}` : command;
116
194
  const spawnContext = resolveSpawnContext(resolvedCommand, cwd, spawnHook);
195
+ if (onUpdate) {
196
+ onUpdate({ content: [], details: undefined });
197
+ }
117
198
  return new Promise((resolve, reject) => {
118
- // We'll stream to a temp file if output gets large
119
199
  let tempFilePath;
120
200
  let tempFileStream;
121
201
  let totalBytes = 0;
122
- // Keep a rolling buffer of the last chunk for tail truncation
123
202
  const chunks = [];
124
203
  let chunksBytes = 0;
125
- // Keep more than we need so we have enough for truncation
126
204
  const maxChunksBytes = DEFAULT_MAX_BYTES * 2;
127
205
  const handleData = (data) => {
128
206
  totalBytes += data.length;
129
- // Start writing to temp file once we exceed the threshold
207
+ // Start writing to a temp file once output exceeds the in-memory threshold.
130
208
  if (totalBytes > DEFAULT_MAX_BYTES && !tempFilePath) {
131
209
  tempFilePath = getTempFilePath();
132
210
  tempFileStream = createWriteStream(tempFilePath);
133
- // Write all buffered chunks to the file
134
- for (const chunk of chunks) {
211
+ // Write all buffered chunks to the file.
212
+ for (const chunk of chunks)
135
213
  tempFileStream.write(chunk);
136
- }
137
214
  }
138
- // Write to temp file if we have one
139
- if (tempFileStream) {
215
+ // Write to temp file if we have one.
216
+ if (tempFileStream)
140
217
  tempFileStream.write(data);
141
- }
142
- // Keep rolling buffer of recent data
218
+ // Keep a rolling buffer of recent output for tail truncation.
143
219
  chunks.push(data);
144
220
  chunksBytes += data.length;
145
- // Trim old chunks if buffer is too large
221
+ // Trim old chunks if the rolling buffer grows too large.
146
222
  while (chunksBytes > maxChunksBytes && chunks.length > 1) {
147
223
  const removed = chunks.shift();
148
224
  chunksBytes -= removed.length;
149
225
  }
150
- // Stream partial output to callback (truncated rolling buffer)
226
+ // Stream partial output using the rolling tail buffer.
151
227
  if (onUpdate) {
152
228
  const fullBuffer = Buffer.concat(chunks);
153
229
  const fullText = fullBuffer.toString("utf-8");
@@ -168,28 +244,23 @@ export function createBashTool(cwd, options) {
168
244
  env: spawnContext.env,
169
245
  })
170
246
  .then(({ exitCode }) => {
171
- // Close temp file stream
172
- if (tempFileStream) {
247
+ // Close temp file stream before building the final result.
248
+ if (tempFileStream)
173
249
  tempFileStream.end();
174
- }
175
- // Combine all buffered chunks
250
+ // Combine the rolling buffer chunks.
176
251
  const fullBuffer = Buffer.concat(chunks);
177
252
  const fullOutput = fullBuffer.toString("utf-8");
178
- // Apply tail truncation
253
+ // Apply tail truncation for the final display payload.
179
254
  const truncation = truncateTail(fullOutput);
180
255
  let outputText = truncation.content || "(no output)";
181
- // Build details with truncation info
182
256
  let details;
183
257
  if (truncation.truncated) {
184
- details = {
185
- truncation,
186
- fullOutputPath: tempFilePath,
187
- };
188
- // Build actionable notice
258
+ // Build truncation details and an actionable notice.
259
+ details = { truncation, fullOutputPath: tempFilePath };
189
260
  const startLine = truncation.totalLines - truncation.outputLines + 1;
190
261
  const endLine = truncation.totalLines;
191
262
  if (truncation.lastLinePartial) {
192
- // Edge case: last line alone > 30KB
263
+ // Edge case: the last line alone is larger than the byte limit.
193
264
  const lastLineSize = formatSize(Buffer.byteLength(fullOutput.split("\n").pop() || "", "utf-8"));
194
265
  outputText += `\n\n[Showing last ${formatSize(truncation.outputBytes)} of line ${endLine} (line is ${lastLineSize}). Full output: ${tempFilePath}]`;
195
266
  }
@@ -209,11 +280,9 @@ export function createBashTool(cwd, options) {
209
280
  }
210
281
  })
211
282
  .catch((err) => {
212
- // Close temp file stream
213
- if (tempFileStream) {
283
+ // Close temp file stream and include buffered output in the error message.
284
+ if (tempFileStream)
214
285
  tempFileStream.end();
215
- }
216
- // Combine all buffered chunks for error output
217
286
  const fullBuffer = Buffer.concat(chunks);
218
287
  let output = fullBuffer.toString("utf-8");
219
288
  if (err.message === "aborted") {
@@ -235,8 +304,39 @@ export function createBashTool(cwd, options) {
235
304
  });
236
305
  });
237
306
  },
307
+ renderCall(args, _theme, context) {
308
+ const state = context.state;
309
+ if (context.executionStarted && state.startedAt === undefined) {
310
+ state.startedAt = Date.now();
311
+ state.endedAt = undefined;
312
+ }
313
+ const text = context.lastComponent ?? new Text("", 0, 0);
314
+ text.setText(formatBashCall(args));
315
+ return text;
316
+ },
317
+ renderResult(result, options, _theme, context) {
318
+ const state = context.state;
319
+ if (state.startedAt !== undefined && options.isPartial && !state.interval) {
320
+ state.interval = setInterval(() => context.invalidate(), 1000);
321
+ }
322
+ if (!options.isPartial || context.isError) {
323
+ state.endedAt ??= Date.now();
324
+ if (state.interval) {
325
+ clearInterval(state.interval);
326
+ state.interval = undefined;
327
+ }
328
+ }
329
+ const component = context.lastComponent ?? new BashResultRenderComponent();
330
+ rebuildBashResultRenderComponent(component, result, options, context.showImages, state.startedAt, state.endedAt);
331
+ component.invalidate();
332
+ return component;
333
+ },
238
334
  };
239
335
  }
240
- /** Default bash tool using process.cwd() - for backwards compatibility */
336
+ export function createBashTool(cwd, options) {
337
+ return wrapToolDefinition(createBashToolDefinition(cwd, options));
338
+ }
339
+ /** Default bash tool using process.cwd() for backwards compatibility. */
340
+ export const bashToolDefinition = createBashToolDefinition(process.cwd());
241
341
  export const bashTool = createBashTool(process.cwd());
242
342
  //# sourceMappingURL=bash.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"bash.js","sourceRoot":"","sources":["../../../src/core/tools/bash.ts"],"names":[],"mappings":"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","sourcesContent":["import { randomBytes } from \"node:crypto\";\nimport { createWriteStream, existsSync } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport type { AgentTool } from \"@draht/agent-core\";\nimport { type Static, Type } from \"@sinclair/typebox\";\nimport { spawn } from \"child_process\";\nimport { getShellConfig, getShellEnv, killProcessTree } from \"../../utils/shell.js\";\nimport { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, type TruncationResult, truncateTail } from \"./truncate.js\";\n\n/**\n * Generate a unique temp file path for bash output\n */\nfunction getTempFilePath(): string {\n\tconst id = randomBytes(8).toString(\"hex\");\n\treturn join(tmpdir(), `pi-bash-${id}.log`);\n}\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 (e.g., 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 * Default bash operations using local shell\n */\nconst defaultBashOperations: BashOperations = {\n\texec: (command, cwd, { onData, signal, timeout, env }) => {\n\t\treturn new Promise((resolve, reject) => {\n\t\t\tconst { shell, args } = getShellConfig();\n\n\t\t\tif (!existsSync(cwd)) {\n\t\t\t\treject(new Error(`Working directory does not exist: ${cwd}\\nCannot execute bash commands.`));\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst child = spawn(shell, [...args, command], {\n\t\t\t\tcwd,\n\t\t\t\tdetached: true,\n\t\t\t\tenv: env ?? getShellEnv(),\n\t\t\t\tstdio: [\"ignore\", \"pipe\", \"pipe\"],\n\t\t\t});\n\n\t\t\tlet timedOut = false;\n\n\t\t\t// Set timeout if provided\n\t\t\tlet timeoutHandle: NodeJS.Timeout | undefined;\n\t\t\tif (timeout !== undefined && timeout > 0) {\n\t\t\t\ttimeoutHandle = setTimeout(() => {\n\t\t\t\t\ttimedOut = true;\n\t\t\t\t\tif (child.pid) {\n\t\t\t\t\t\tkillProcessTree(child.pid);\n\t\t\t\t\t}\n\t\t\t\t}, timeout * 1000);\n\t\t\t}\n\n\t\t\t// Stream stdout and stderr\n\t\t\tif (child.stdout) {\n\t\t\t\tchild.stdout.on(\"data\", onData);\n\t\t\t}\n\t\t\tif (child.stderr) {\n\t\t\t\tchild.stderr.on(\"data\", onData);\n\t\t\t}\n\n\t\t\t// Handle shell spawn errors\n\t\t\tchild.on(\"error\", (err) => {\n\t\t\t\tif (timeoutHandle) clearTimeout(timeoutHandle);\n\t\t\t\tif (signal) signal.removeEventListener(\"abort\", onAbort);\n\t\t\t\treject(err);\n\t\t\t});\n\n\t\t\t// Handle abort signal - kill entire process tree\n\t\t\tconst onAbort = () => {\n\t\t\t\tif (child.pid) {\n\t\t\t\t\tkillProcessTree(child.pid);\n\t\t\t\t}\n\t\t\t};\n\n\t\t\tif (signal) {\n\t\t\t\tif (signal.aborted) {\n\t\t\t\t\tonAbort();\n\t\t\t\t} else {\n\t\t\t\t\tsignal.addEventListener(\"abort\", onAbort, { once: true });\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Handle process exit\n\t\t\tchild.on(\"close\", (code) => {\n\t\t\t\tif (timeoutHandle) clearTimeout(timeoutHandle);\n\t\t\t\tif (signal) signal.removeEventListener(\"abort\", onAbort);\n\n\t\t\t\tif (signal?.aborted) {\n\t\t\t\t\treject(new Error(\"aborted\"));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tif (timedOut) {\n\t\t\t\t\treject(new Error(`timeout:${timeout}`));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tresolve({ exitCode: code });\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 = {\n\t\tcommand,\n\t\tcwd,\n\t\tenv: { ...getShellEnv() },\n\t};\n\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 (e.g., \"shopt -s expand_aliases\" for alias support) */\n\tcommandPrefix?: string;\n\t/** Hook to adjust command, cwd, or env before execution */\n\tspawnHook?: BashSpawnHook;\n}\n\nexport function createBashTool(cwd: string, options?: BashToolOptions): AgentTool<typeof bashSchema> {\n\tconst ops = options?.operations ?? defaultBashOperations;\n\tconst commandPrefix = options?.commandPrefix;\n\tconst spawnHook = options?.spawnHook;\n\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\tparameters: bashSchema,\n\t\texecute: async (\n\t\t\t_toolCallId: string,\n\t\t\t{ command, timeout }: { command: string; timeout?: number },\n\t\t\tsignal?: AbortSignal,\n\t\t\tonUpdate?,\n\t\t) => {\n\t\t\t// Apply command prefix if configured (e.g., \"shopt -s expand_aliases\" for alias support)\n\t\t\tconst resolvedCommand = commandPrefix ? `${commandPrefix}\\n${command}` : command;\n\t\t\tconst spawnContext = resolveSpawnContext(resolvedCommand, cwd, spawnHook);\n\n\t\t\treturn new Promise((resolve, reject) => {\n\t\t\t\t// We'll stream to a temp file if output gets large\n\t\t\t\tlet tempFilePath: string | undefined;\n\t\t\t\tlet tempFileStream: ReturnType<typeof createWriteStream> | undefined;\n\t\t\t\tlet totalBytes = 0;\n\n\t\t\t\t// Keep a rolling buffer of the last chunk for tail truncation\n\t\t\t\tconst chunks: Buffer[] = [];\n\t\t\t\tlet chunksBytes = 0;\n\t\t\t\t// Keep more than we need so we have enough for truncation\n\t\t\t\tconst maxChunksBytes = DEFAULT_MAX_BYTES * 2;\n\n\t\t\t\tconst handleData = (data: Buffer) => {\n\t\t\t\t\ttotalBytes += data.length;\n\n\t\t\t\t\t// Start writing to temp file once we exceed the threshold\n\t\t\t\t\tif (totalBytes > DEFAULT_MAX_BYTES && !tempFilePath) {\n\t\t\t\t\t\ttempFilePath = getTempFilePath();\n\t\t\t\t\t\ttempFileStream = createWriteStream(tempFilePath);\n\t\t\t\t\t\t// Write all buffered chunks to the file\n\t\t\t\t\t\tfor (const chunk of chunks) {\n\t\t\t\t\t\t\ttempFileStream.write(chunk);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\t// Write to temp file if we have one\n\t\t\t\t\tif (tempFileStream) {\n\t\t\t\t\t\ttempFileStream.write(data);\n\t\t\t\t\t}\n\n\t\t\t\t\t// Keep rolling buffer of recent data\n\t\t\t\t\tchunks.push(data);\n\t\t\t\t\tchunksBytes += data.length;\n\n\t\t\t\t\t// Trim old chunks if buffer is too large\n\t\t\t\t\twhile (chunksBytes > maxChunksBytes && chunks.length > 1) {\n\t\t\t\t\t\tconst removed = chunks.shift()!;\n\t\t\t\t\t\tchunksBytes -= removed.length;\n\t\t\t\t\t}\n\n\t\t\t\t\t// Stream partial output to callback (truncated rolling buffer)\n\t\t\t\t\tif (onUpdate) {\n\t\t\t\t\t\tconst fullBuffer = Buffer.concat(chunks);\n\t\t\t\t\t\tconst fullText = fullBuffer.toString(\"utf-8\");\n\t\t\t\t\t\tconst truncation = truncateTail(fullText);\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: tempFilePath,\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,\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\t// Close temp file stream\n\t\t\t\t\t\tif (tempFileStream) {\n\t\t\t\t\t\t\ttempFileStream.end();\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Combine all buffered chunks\n\t\t\t\t\t\tconst fullBuffer = Buffer.concat(chunks);\n\t\t\t\t\t\tconst fullOutput = fullBuffer.toString(\"utf-8\");\n\n\t\t\t\t\t\t// Apply tail truncation\n\t\t\t\t\t\tconst truncation = truncateTail(fullOutput);\n\t\t\t\t\t\tlet outputText = truncation.content || \"(no output)\";\n\n\t\t\t\t\t\t// Build details with truncation info\n\t\t\t\t\t\tlet details: BashToolDetails | undefined;\n\n\t\t\t\t\t\tif (truncation.truncated) {\n\t\t\t\t\t\t\tdetails = {\n\t\t\t\t\t\t\t\ttruncation,\n\t\t\t\t\t\t\t\tfullOutputPath: tempFilePath,\n\t\t\t\t\t\t\t};\n\n\t\t\t\t\t\t\t// Build actionable notice\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\n\t\t\t\t\t\t\tif (truncation.lastLinePartial) {\n\t\t\t\t\t\t\t\t// Edge case: last line alone > 30KB\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: ${tempFilePath}]`;\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: ${tempFilePath}]`;\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: ${tempFilePath}]`;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\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\t// Close temp file stream\n\t\t\t\t\t\tif (tempFileStream) {\n\t\t\t\t\t\t\ttempFileStream.end();\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Combine all buffered chunks for error output\n\t\t\t\t\t\tconst fullBuffer = Buffer.concat(chunks);\n\t\t\t\t\t\tlet output = fullBuffer.toString(\"utf-8\");\n\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};\n}\n\n/** Default bash tool using process.cwd() - for backwards compatibility */\nexport const bashTool = createBashTool(process.cwd());\n"]}
1
+ {"version":3,"file":"bash.js","sourceRoot":"","sources":["../../../src/core/tools/bash.ts"],"names":[],"mappings":"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","sourcesContent":["import { randomBytes } from \"node:crypto\";\nimport { createWriteStream, existsSync } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport type { AgentTool } from \"@draht/agent-core\";\nimport { Container, Text, truncateToWidth } from \"@draht/tui\";\nimport { type Static, Type } from \"@sinclair/typebox\";\nimport { spawn } from \"child_process\";\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 { getShellConfig, getShellEnv, killProcessTree } from \"../../utils/shell.js\";\nimport type { ToolDefinition, ToolRenderResultOptions } from \"../extensions/types.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, truncateTail } from \"./truncate.js\";\n\n/**\n * Generate a unique temp file path for bash output.\n */\nfunction getTempFilePath(): string {\n\tconst id = randomBytes(8).toString(\"hex\");\n\treturn join(tmpdir(), `pi-bash-${id}.log`);\n}\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(): 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();\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: true,\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\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 (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 (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/** 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; 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();\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\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\tlet tempFilePath: string | undefined;\n\t\t\t\tlet tempFileStream: ReturnType<typeof createWriteStream> | undefined;\n\t\t\t\tlet totalBytes = 0;\n\t\t\t\tconst chunks: Buffer[] = [];\n\t\t\t\tlet chunksBytes = 0;\n\t\t\t\tconst maxChunksBytes = DEFAULT_MAX_BYTES * 2;\n\n\t\t\t\tconst handleData = (data: Buffer) => {\n\t\t\t\t\ttotalBytes += data.length;\n\t\t\t\t\t// Start writing to a temp file once output exceeds the in-memory threshold.\n\t\t\t\t\tif (totalBytes > DEFAULT_MAX_BYTES && !tempFilePath) {\n\t\t\t\t\t\ttempFilePath = getTempFilePath();\n\t\t\t\t\t\ttempFileStream = createWriteStream(tempFilePath);\n\t\t\t\t\t\t// Write all buffered chunks to the file.\n\t\t\t\t\t\tfor (const chunk of chunks) tempFileStream.write(chunk);\n\t\t\t\t\t}\n\t\t\t\t\t// Write to temp file if we have one.\n\t\t\t\t\tif (tempFileStream) tempFileStream.write(data);\n\t\t\t\t\t// Keep a rolling buffer of recent output for tail truncation.\n\t\t\t\t\tchunks.push(data);\n\t\t\t\t\tchunksBytes += data.length;\n\t\t\t\t\t// Trim old chunks if the rolling buffer grows too large.\n\t\t\t\t\twhile (chunksBytes > maxChunksBytes && chunks.length > 1) {\n\t\t\t\t\t\tconst removed = chunks.shift()!;\n\t\t\t\t\t\tchunksBytes -= removed.length;\n\t\t\t\t\t}\n\t\t\t\t\t// Stream partial output using the rolling tail buffer.\n\t\t\t\t\tif (onUpdate) {\n\t\t\t\t\t\tconst fullBuffer = Buffer.concat(chunks);\n\t\t\t\t\t\tconst fullText = fullBuffer.toString(\"utf-8\");\n\t\t\t\t\t\tconst truncation = truncateTail(fullText);\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: tempFilePath,\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,\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\t// Close temp file stream before building the final result.\n\t\t\t\t\t\tif (tempFileStream) tempFileStream.end();\n\t\t\t\t\t\t// Combine the rolling buffer chunks.\n\t\t\t\t\t\tconst fullBuffer = Buffer.concat(chunks);\n\t\t\t\t\t\tconst fullOutput = fullBuffer.toString(\"utf-8\");\n\t\t\t\t\t\t// Apply tail truncation for the final display payload.\n\t\t\t\t\t\tconst truncation = truncateTail(fullOutput);\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\t// Build truncation details and an actionable notice.\n\t\t\t\t\t\t\tdetails = { truncation, fullOutputPath: tempFilePath };\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\t// Edge case: the last line alone is larger than the byte limit.\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: ${tempFilePath}]`;\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: ${tempFilePath}]`;\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: ${tempFilePath}]`;\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\t// Close temp file stream and include buffered output in the error message.\n\t\t\t\t\t\tif (tempFileStream) tempFileStream.end();\n\t\t\t\t\t\tconst fullBuffer = Buffer.concat(chunks);\n\t\t\t\t\t\tlet output = fullBuffer.toString(\"utf-8\");\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\n/** Default bash tool using process.cwd() for backwards compatibility. */\nexport const bashToolDefinition = createBashToolDefinition(process.cwd());\nexport const bashTool = createBashTool(process.cwd());\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"edit-diff.d.ts","sourceRoot":"","sources":["../../../src/core/tools/edit-diff.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAOH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAM/D;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAElD;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CAE9E;AAED;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAoB3D;AAED,MAAM,WAAW,gBAAgB;IAChC,gCAAgC;IAChC,KAAK,EAAE,OAAO,CAAC;IACf,4FAA4F;IAC5F,KAAK,EAAE,MAAM,CAAC;IACd,iCAAiC;IACjC,WAAW,EAAE,MAAM,CAAC;IACpB,4DAA4D;IAC5D,cAAc,EAAE,OAAO,CAAC;IACxB;;;OAGG;IACH,qBAAqB,EAAE,MAAM,CAAC;CAC9B;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,gBAAgB,CAsChF;AAED,uFAAuF;AACvF,wBAAgB,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAEvE;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CACjC,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,EAClB,YAAY,SAAI,GACd;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,gBAAgB,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,CAgGxD;AAED,MAAM,WAAW,cAAc;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,gBAAgB,EAAE,MAAM,GAAG,SAAS,CAAC;CACrC;AAED,MAAM,WAAW,aAAa;IAC7B,KAAK,EAAE,MAAM,CAAC;CACd;AAED;;;GAGG;AACH,wBAAsB,eAAe,CACpC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,EACf,GAAG,EAAE,MAAM,GACT,OAAO,CAAC,cAAc,GAAG,aAAa,CAAC,CA6DzC","sourcesContent":["/**\n * Shared diff computation utilities for the edit tool.\n * Used by both edit.ts (for execution) and tool-execution.ts (for preview rendering).\n */\n\nimport * as Diff from \"diff\";\nimport { constants } from \"fs\";\nimport { access, readFile } from \"fs/promises\";\nimport { resolveToCwd } from \"./path-utils.js\";\n\nexport function detectLineEnding(content: string): \"\\r\\n\" | \"\\n\" {\n\tconst crlfIdx = content.indexOf(\"\\r\\n\");\n\tconst lfIdx = content.indexOf(\"\\n\");\n\tif (lfIdx === -1) return \"\\n\";\n\tif (crlfIdx === -1) return \"\\n\";\n\treturn crlfIdx < lfIdx ? \"\\r\\n\" : \"\\n\";\n}\n\nexport function normalizeToLF(text: string): string {\n\treturn text.replace(/\\r\\n/g, \"\\n\").replace(/\\r/g, \"\\n\");\n}\n\nexport function restoreLineEndings(text: string, ending: \"\\r\\n\" | \"\\n\"): string {\n\treturn ending === \"\\r\\n\" ? text.replace(/\\n/g, \"\\r\\n\") : text;\n}\n\n/**\n * Normalize text for fuzzy matching. Applies progressive transformations:\n * - Strip trailing whitespace from each line\n * - Normalize smart quotes to ASCII equivalents\n * - Normalize Unicode dashes/hyphens to ASCII hyphen\n * - Normalize special Unicode spaces to regular space\n */\nexport function normalizeForFuzzyMatch(text: string): string {\n\treturn (\n\t\ttext\n\t\t\t// Strip trailing whitespace per line\n\t\t\t.split(\"\\n\")\n\t\t\t.map((line) => line.trimEnd())\n\t\t\t.join(\"\\n\")\n\t\t\t// Smart single quotes → '\n\t\t\t.replace(/[\\u2018\\u2019\\u201A\\u201B]/g, \"'\")\n\t\t\t// Smart double quotes → \"\n\t\t\t.replace(/[\\u201C\\u201D\\u201E\\u201F]/g, '\"')\n\t\t\t// Various dashes/hyphens → -\n\t\t\t// U+2010 hyphen, U+2011 non-breaking hyphen, U+2012 figure dash,\n\t\t\t// U+2013 en-dash, U+2014 em-dash, U+2015 horizontal bar, U+2212 minus\n\t\t\t.replace(/[\\u2010\\u2011\\u2012\\u2013\\u2014\\u2015\\u2212]/g, \"-\")\n\t\t\t// Special spaces → regular space\n\t\t\t// U+00A0 NBSP, U+2002-U+200A various spaces, U+202F narrow NBSP,\n\t\t\t// U+205F medium math space, U+3000 ideographic space\n\t\t\t.replace(/[\\u00A0\\u2002-\\u200A\\u202F\\u205F\\u3000]/g, \" \")\n\t);\n}\n\nexport interface FuzzyMatchResult {\n\t/** Whether a match was found */\n\tfound: boolean;\n\t/** The index where the match starts (in the content that should be used for replacement) */\n\tindex: number;\n\t/** Length of the matched text */\n\tmatchLength: number;\n\t/** Whether fuzzy matching was used (false = exact match) */\n\tusedFuzzyMatch: boolean;\n\t/**\n\t * The content to use for replacement operations.\n\t * When exact match: original content. When fuzzy match: normalized content.\n\t */\n\tcontentForReplacement: string;\n}\n\n/**\n * Find oldText in content, trying exact match first, then fuzzy match.\n * When fuzzy matching is used, the returned contentForReplacement is the\n * fuzzy-normalized version of the content (trailing whitespace stripped,\n * Unicode quotes/dashes normalized to ASCII).\n */\nexport function fuzzyFindText(content: string, oldText: string): FuzzyMatchResult {\n\t// Try exact match first\n\tconst exactIndex = content.indexOf(oldText);\n\tif (exactIndex !== -1) {\n\t\treturn {\n\t\t\tfound: true,\n\t\t\tindex: exactIndex,\n\t\t\tmatchLength: oldText.length,\n\t\t\tusedFuzzyMatch: false,\n\t\t\tcontentForReplacement: content,\n\t\t};\n\t}\n\n\t// Try fuzzy match - work entirely in normalized space\n\tconst fuzzyContent = normalizeForFuzzyMatch(content);\n\tconst fuzzyOldText = normalizeForFuzzyMatch(oldText);\n\tconst fuzzyIndex = fuzzyContent.indexOf(fuzzyOldText);\n\n\tif (fuzzyIndex === -1) {\n\t\treturn {\n\t\t\tfound: false,\n\t\t\tindex: -1,\n\t\t\tmatchLength: 0,\n\t\t\tusedFuzzyMatch: false,\n\t\t\tcontentForReplacement: content,\n\t\t};\n\t}\n\n\t// When fuzzy matching, we work in the normalized space for replacement.\n\t// This means the output will have normalized whitespace/quotes/dashes,\n\t// which is acceptable since we're fixing minor formatting differences anyway.\n\treturn {\n\t\tfound: true,\n\t\tindex: fuzzyIndex,\n\t\tmatchLength: fuzzyOldText.length,\n\t\tusedFuzzyMatch: true,\n\t\tcontentForReplacement: fuzzyContent,\n\t};\n}\n\n/** Strip UTF-8 BOM if present, return both the BOM (if any) and the text without it */\nexport function stripBom(content: string): { bom: string; text: string } {\n\treturn content.startsWith(\"\\uFEFF\") ? { bom: \"\\uFEFF\", text: content.slice(1) } : { bom: \"\", text: content };\n}\n\n/**\n * Generate a unified diff string with line numbers and context.\n * Returns both the diff string and the first changed line number (in the new file).\n */\nexport function generateDiffString(\n\toldContent: string,\n\tnewContent: string,\n\tcontextLines = 4,\n): { diff: string; firstChangedLine: number | undefined } {\n\tconst parts = Diff.diffLines(oldContent, newContent);\n\tconst output: string[] = [];\n\n\tconst oldLines = oldContent.split(\"\\n\");\n\tconst newLines = newContent.split(\"\\n\");\n\tconst maxLineNum = Math.max(oldLines.length, newLines.length);\n\tconst lineNumWidth = String(maxLineNum).length;\n\n\tlet oldLineNum = 1;\n\tlet newLineNum = 1;\n\tlet lastWasChange = false;\n\tlet firstChangedLine: number | undefined;\n\n\tfor (let i = 0; i < parts.length; i++) {\n\t\tconst part = parts[i];\n\t\tconst raw = part.value.split(\"\\n\");\n\t\tif (raw[raw.length - 1] === \"\") {\n\t\t\traw.pop();\n\t\t}\n\n\t\tif (part.added || part.removed) {\n\t\t\t// Capture the first changed line (in the new file)\n\t\t\tif (firstChangedLine === undefined) {\n\t\t\t\tfirstChangedLine = newLineNum;\n\t\t\t}\n\n\t\t\t// Show the change\n\t\t\tfor (const line of raw) {\n\t\t\t\tif (part.added) {\n\t\t\t\t\tconst lineNum = String(newLineNum).padStart(lineNumWidth, \" \");\n\t\t\t\t\toutput.push(`+${lineNum} ${line}`);\n\t\t\t\t\tnewLineNum++;\n\t\t\t\t} else {\n\t\t\t\t\t// removed\n\t\t\t\t\tconst lineNum = String(oldLineNum).padStart(lineNumWidth, \" \");\n\t\t\t\t\toutput.push(`-${lineNum} ${line}`);\n\t\t\t\t\toldLineNum++;\n\t\t\t\t}\n\t\t\t}\n\t\t\tlastWasChange = true;\n\t\t} else {\n\t\t\t// Context lines - only show a few before/after changes\n\t\t\tconst nextPartIsChange = i < parts.length - 1 && (parts[i + 1].added || parts[i + 1].removed);\n\n\t\t\tif (lastWasChange || nextPartIsChange) {\n\t\t\t\t// Show context\n\t\t\t\tlet linesToShow = raw;\n\t\t\t\tlet skipStart = 0;\n\t\t\t\tlet skipEnd = 0;\n\n\t\t\t\tif (!lastWasChange) {\n\t\t\t\t\t// Show only last N lines as leading context\n\t\t\t\t\tskipStart = Math.max(0, raw.length - contextLines);\n\t\t\t\t\tlinesToShow = raw.slice(skipStart);\n\t\t\t\t}\n\n\t\t\t\tif (!nextPartIsChange && linesToShow.length > contextLines) {\n\t\t\t\t\t// Show only first N lines as trailing context\n\t\t\t\t\tskipEnd = linesToShow.length - contextLines;\n\t\t\t\t\tlinesToShow = linesToShow.slice(0, contextLines);\n\t\t\t\t}\n\n\t\t\t\t// Add ellipsis if we skipped lines at start\n\t\t\t\tif (skipStart > 0) {\n\t\t\t\t\toutput.push(` ${\"\".padStart(lineNumWidth, \" \")} ...`);\n\t\t\t\t\t// Update line numbers for the skipped leading context\n\t\t\t\t\toldLineNum += skipStart;\n\t\t\t\t\tnewLineNum += skipStart;\n\t\t\t\t}\n\n\t\t\t\tfor (const line of linesToShow) {\n\t\t\t\t\tconst lineNum = String(oldLineNum).padStart(lineNumWidth, \" \");\n\t\t\t\t\toutput.push(` ${lineNum} ${line}`);\n\t\t\t\t\toldLineNum++;\n\t\t\t\t\tnewLineNum++;\n\t\t\t\t}\n\n\t\t\t\t// Add ellipsis if we skipped lines at end\n\t\t\t\tif (skipEnd > 0) {\n\t\t\t\t\toutput.push(` ${\"\".padStart(lineNumWidth, \" \")} ...`);\n\t\t\t\t\t// Update line numbers for the skipped trailing context\n\t\t\t\t\toldLineNum += skipEnd;\n\t\t\t\t\tnewLineNum += skipEnd;\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// Skip these context lines entirely\n\t\t\t\toldLineNum += raw.length;\n\t\t\t\tnewLineNum += raw.length;\n\t\t\t}\n\n\t\t\tlastWasChange = false;\n\t\t}\n\t}\n\n\treturn { diff: output.join(\"\\n\"), firstChangedLine };\n}\n\nexport interface EditDiffResult {\n\tdiff: string;\n\tfirstChangedLine: number | undefined;\n}\n\nexport interface EditDiffError {\n\terror: string;\n}\n\n/**\n * Compute the diff for an edit operation without applying it.\n * Used for preview rendering in the TUI before the tool executes.\n */\nexport async function computeEditDiff(\n\tpath: string,\n\toldText: string,\n\tnewText: string,\n\tcwd: string,\n): Promise<EditDiffResult | EditDiffError> {\n\tconst absolutePath = resolveToCwd(path, cwd);\n\n\ttry {\n\t\t// Check if file exists and is readable\n\t\ttry {\n\t\t\tawait access(absolutePath, constants.R_OK);\n\t\t} catch {\n\t\t\treturn { error: `File not found: ${path}` };\n\t\t}\n\n\t\t// Read the file\n\t\tconst rawContent = await readFile(absolutePath, \"utf-8\");\n\n\t\t// Strip BOM before matching (LLM won't include invisible BOM in oldText)\n\t\tconst { text: content } = stripBom(rawContent);\n\n\t\tconst normalizedContent = normalizeToLF(content);\n\t\tconst normalizedOldText = normalizeToLF(oldText);\n\t\tconst normalizedNewText = normalizeToLF(newText);\n\n\t\t// Find the old text using fuzzy matching (tries exact match first, then fuzzy)\n\t\tconst matchResult = fuzzyFindText(normalizedContent, normalizedOldText);\n\n\t\tif (!matchResult.found) {\n\t\t\treturn {\n\t\t\t\terror: `Could not find the exact text in ${path}. The old text must match exactly including all whitespace and newlines.`,\n\t\t\t};\n\t\t}\n\n\t\t// Count occurrences using fuzzy-normalized content for consistency\n\t\tconst fuzzyContent = normalizeForFuzzyMatch(normalizedContent);\n\t\tconst fuzzyOldText = normalizeForFuzzyMatch(normalizedOldText);\n\t\tconst occurrences = fuzzyContent.split(fuzzyOldText).length - 1;\n\n\t\tif (occurrences > 1) {\n\t\t\treturn {\n\t\t\t\terror: `Found ${occurrences} occurrences of the text in ${path}. The text must be unique. Please provide more context to make it unique.`,\n\t\t\t};\n\t\t}\n\n\t\t// Compute the new content using the matched position\n\t\t// When fuzzy matching was used, contentForReplacement is the normalized version\n\t\tconst baseContent = matchResult.contentForReplacement;\n\t\tconst newContent =\n\t\t\tbaseContent.substring(0, matchResult.index) +\n\t\t\tnormalizedNewText +\n\t\t\tbaseContent.substring(matchResult.index + matchResult.matchLength);\n\n\t\t// Check if it would actually change anything\n\t\tif (baseContent === newContent) {\n\t\t\treturn {\n\t\t\t\terror: `No changes would be made to ${path}. The replacement produces identical content.`,\n\t\t\t};\n\t\t}\n\n\t\t// Generate the diff\n\t\treturn generateDiffString(baseContent, newContent);\n\t} catch (err) {\n\t\treturn { error: err instanceof Error ? err.message : String(err) };\n\t}\n}\n"]}
1
+ {"version":3,"file":"edit-diff.d.ts","sourceRoot":"","sources":["../../../src/core/tools/edit-diff.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAOH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAM/D;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAElD;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CAE9E;AAED;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAqB3D;AAED,MAAM,WAAW,gBAAgB;IAChC,gCAAgC;IAChC,KAAK,EAAE,OAAO,CAAC;IACf,4FAA4F;IAC5F,KAAK,EAAE,MAAM,CAAC;IACd,iCAAiC;IACjC,WAAW,EAAE,MAAM,CAAC;IACpB,4DAA4D;IAC5D,cAAc,EAAE,OAAO,CAAC;IACxB;;;OAGG;IACH,qBAAqB,EAAE,MAAM,CAAC;CAC9B;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,gBAAgB,CAsChF;AAED,uFAAuF;AACvF,wBAAgB,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAEvE;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CACjC,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,EAClB,YAAY,SAAI,GACd;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,gBAAgB,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,CAgGxD;AAED,MAAM,WAAW,cAAc;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,gBAAgB,EAAE,MAAM,GAAG,SAAS,CAAC;CACrC;AAED,MAAM,WAAW,aAAa;IAC7B,KAAK,EAAE,MAAM,CAAC;CACd;AAED;;;GAGG;AACH,wBAAsB,eAAe,CACpC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,EACf,GAAG,EAAE,MAAM,GACT,OAAO,CAAC,cAAc,GAAG,aAAa,CAAC,CA6DzC","sourcesContent":["/**\n * Shared diff computation utilities for the edit tool.\n * Used by both edit.ts (for execution) and tool-execution.ts (for preview rendering).\n */\n\nimport * as Diff from \"diff\";\nimport { constants } from \"fs\";\nimport { access, readFile } from \"fs/promises\";\nimport { resolveToCwd } from \"./path-utils.js\";\n\nexport function detectLineEnding(content: string): \"\\r\\n\" | \"\\n\" {\n\tconst crlfIdx = content.indexOf(\"\\r\\n\");\n\tconst lfIdx = content.indexOf(\"\\n\");\n\tif (lfIdx === -1) return \"\\n\";\n\tif (crlfIdx === -1) return \"\\n\";\n\treturn crlfIdx < lfIdx ? \"\\r\\n\" : \"\\n\";\n}\n\nexport function normalizeToLF(text: string): string {\n\treturn text.replace(/\\r\\n/g, \"\\n\").replace(/\\r/g, \"\\n\");\n}\n\nexport function restoreLineEndings(text: string, ending: \"\\r\\n\" | \"\\n\"): string {\n\treturn ending === \"\\r\\n\" ? text.replace(/\\n/g, \"\\r\\n\") : text;\n}\n\n/**\n * Normalize text for fuzzy matching. Applies progressive transformations:\n * - Strip trailing whitespace from each line\n * - Normalize smart quotes to ASCII equivalents\n * - Normalize Unicode dashes/hyphens to ASCII hyphen\n * - Normalize special Unicode spaces to regular space\n */\nexport function normalizeForFuzzyMatch(text: string): string {\n\treturn (\n\t\ttext\n\t\t\t.normalize(\"NFKC\")\n\t\t\t// Strip trailing whitespace per line\n\t\t\t.split(\"\\n\")\n\t\t\t.map((line) => line.trimEnd())\n\t\t\t.join(\"\\n\")\n\t\t\t// Smart single quotes → '\n\t\t\t.replace(/[\\u2018\\u2019\\u201A\\u201B]/g, \"'\")\n\t\t\t// Smart double quotes → \"\n\t\t\t.replace(/[\\u201C\\u201D\\u201E\\u201F]/g, '\"')\n\t\t\t// Various dashes/hyphens → -\n\t\t\t// U+2010 hyphen, U+2011 non-breaking hyphen, U+2012 figure dash,\n\t\t\t// U+2013 en-dash, U+2014 em-dash, U+2015 horizontal bar, U+2212 minus\n\t\t\t.replace(/[\\u2010\\u2011\\u2012\\u2013\\u2014\\u2015\\u2212]/g, \"-\")\n\t\t\t// Special spaces → regular space\n\t\t\t// U+00A0 NBSP, U+2002-U+200A various spaces, U+202F narrow NBSP,\n\t\t\t// U+205F medium math space, U+3000 ideographic space\n\t\t\t.replace(/[\\u00A0\\u2002-\\u200A\\u202F\\u205F\\u3000]/g, \" \")\n\t);\n}\n\nexport interface FuzzyMatchResult {\n\t/** Whether a match was found */\n\tfound: boolean;\n\t/** The index where the match starts (in the content that should be used for replacement) */\n\tindex: number;\n\t/** Length of the matched text */\n\tmatchLength: number;\n\t/** Whether fuzzy matching was used (false = exact match) */\n\tusedFuzzyMatch: boolean;\n\t/**\n\t * The content to use for replacement operations.\n\t * When exact match: original content. When fuzzy match: normalized content.\n\t */\n\tcontentForReplacement: string;\n}\n\n/**\n * Find oldText in content, trying exact match first, then fuzzy match.\n * When fuzzy matching is used, the returned contentForReplacement is the\n * fuzzy-normalized version of the content (trailing whitespace stripped,\n * Unicode quotes/dashes normalized to ASCII).\n */\nexport function fuzzyFindText(content: string, oldText: string): FuzzyMatchResult {\n\t// Try exact match first\n\tconst exactIndex = content.indexOf(oldText);\n\tif (exactIndex !== -1) {\n\t\treturn {\n\t\t\tfound: true,\n\t\t\tindex: exactIndex,\n\t\t\tmatchLength: oldText.length,\n\t\t\tusedFuzzyMatch: false,\n\t\t\tcontentForReplacement: content,\n\t\t};\n\t}\n\n\t// Try fuzzy match - work entirely in normalized space\n\tconst fuzzyContent = normalizeForFuzzyMatch(content);\n\tconst fuzzyOldText = normalizeForFuzzyMatch(oldText);\n\tconst fuzzyIndex = fuzzyContent.indexOf(fuzzyOldText);\n\n\tif (fuzzyIndex === -1) {\n\t\treturn {\n\t\t\tfound: false,\n\t\t\tindex: -1,\n\t\t\tmatchLength: 0,\n\t\t\tusedFuzzyMatch: false,\n\t\t\tcontentForReplacement: content,\n\t\t};\n\t}\n\n\t// When fuzzy matching, we work in the normalized space for replacement.\n\t// This means the output will have normalized whitespace/quotes/dashes,\n\t// which is acceptable since we're fixing minor formatting differences anyway.\n\treturn {\n\t\tfound: true,\n\t\tindex: fuzzyIndex,\n\t\tmatchLength: fuzzyOldText.length,\n\t\tusedFuzzyMatch: true,\n\t\tcontentForReplacement: fuzzyContent,\n\t};\n}\n\n/** Strip UTF-8 BOM if present, return both the BOM (if any) and the text without it */\nexport function stripBom(content: string): { bom: string; text: string } {\n\treturn content.startsWith(\"\\uFEFF\") ? { bom: \"\\uFEFF\", text: content.slice(1) } : { bom: \"\", text: content };\n}\n\n/**\n * Generate a unified diff string with line numbers and context.\n * Returns both the diff string and the first changed line number (in the new file).\n */\nexport function generateDiffString(\n\toldContent: string,\n\tnewContent: string,\n\tcontextLines = 4,\n): { diff: string; firstChangedLine: number | undefined } {\n\tconst parts = Diff.diffLines(oldContent, newContent);\n\tconst output: string[] = [];\n\n\tconst oldLines = oldContent.split(\"\\n\");\n\tconst newLines = newContent.split(\"\\n\");\n\tconst maxLineNum = Math.max(oldLines.length, newLines.length);\n\tconst lineNumWidth = String(maxLineNum).length;\n\n\tlet oldLineNum = 1;\n\tlet newLineNum = 1;\n\tlet lastWasChange = false;\n\tlet firstChangedLine: number | undefined;\n\n\tfor (let i = 0; i < parts.length; i++) {\n\t\tconst part = parts[i];\n\t\tconst raw = part.value.split(\"\\n\");\n\t\tif (raw[raw.length - 1] === \"\") {\n\t\t\traw.pop();\n\t\t}\n\n\t\tif (part.added || part.removed) {\n\t\t\t// Capture the first changed line (in the new file)\n\t\t\tif (firstChangedLine === undefined) {\n\t\t\t\tfirstChangedLine = newLineNum;\n\t\t\t}\n\n\t\t\t// Show the change\n\t\t\tfor (const line of raw) {\n\t\t\t\tif (part.added) {\n\t\t\t\t\tconst lineNum = String(newLineNum).padStart(lineNumWidth, \" \");\n\t\t\t\t\toutput.push(`+${lineNum} ${line}`);\n\t\t\t\t\tnewLineNum++;\n\t\t\t\t} else {\n\t\t\t\t\t// removed\n\t\t\t\t\tconst lineNum = String(oldLineNum).padStart(lineNumWidth, \" \");\n\t\t\t\t\toutput.push(`-${lineNum} ${line}`);\n\t\t\t\t\toldLineNum++;\n\t\t\t\t}\n\t\t\t}\n\t\t\tlastWasChange = true;\n\t\t} else {\n\t\t\t// Context lines - only show a few before/after changes\n\t\t\tconst nextPartIsChange = i < parts.length - 1 && (parts[i + 1].added || parts[i + 1].removed);\n\n\t\t\tif (lastWasChange || nextPartIsChange) {\n\t\t\t\t// Show context\n\t\t\t\tlet linesToShow = raw;\n\t\t\t\tlet skipStart = 0;\n\t\t\t\tlet skipEnd = 0;\n\n\t\t\t\tif (!lastWasChange) {\n\t\t\t\t\t// Show only last N lines as leading context\n\t\t\t\t\tskipStart = Math.max(0, raw.length - contextLines);\n\t\t\t\t\tlinesToShow = raw.slice(skipStart);\n\t\t\t\t}\n\n\t\t\t\tif (!nextPartIsChange && linesToShow.length > contextLines) {\n\t\t\t\t\t// Show only first N lines as trailing context\n\t\t\t\t\tskipEnd = linesToShow.length - contextLines;\n\t\t\t\t\tlinesToShow = linesToShow.slice(0, contextLines);\n\t\t\t\t}\n\n\t\t\t\t// Add ellipsis if we skipped lines at start\n\t\t\t\tif (skipStart > 0) {\n\t\t\t\t\toutput.push(` ${\"\".padStart(lineNumWidth, \" \")} ...`);\n\t\t\t\t\t// Update line numbers for the skipped leading context\n\t\t\t\t\toldLineNum += skipStart;\n\t\t\t\t\tnewLineNum += skipStart;\n\t\t\t\t}\n\n\t\t\t\tfor (const line of linesToShow) {\n\t\t\t\t\tconst lineNum = String(oldLineNum).padStart(lineNumWidth, \" \");\n\t\t\t\t\toutput.push(` ${lineNum} ${line}`);\n\t\t\t\t\toldLineNum++;\n\t\t\t\t\tnewLineNum++;\n\t\t\t\t}\n\n\t\t\t\t// Add ellipsis if we skipped lines at end\n\t\t\t\tif (skipEnd > 0) {\n\t\t\t\t\toutput.push(` ${\"\".padStart(lineNumWidth, \" \")} ...`);\n\t\t\t\t\t// Update line numbers for the skipped trailing context\n\t\t\t\t\toldLineNum += skipEnd;\n\t\t\t\t\tnewLineNum += skipEnd;\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// Skip these context lines entirely\n\t\t\t\toldLineNum += raw.length;\n\t\t\t\tnewLineNum += raw.length;\n\t\t\t}\n\n\t\t\tlastWasChange = false;\n\t\t}\n\t}\n\n\treturn { diff: output.join(\"\\n\"), firstChangedLine };\n}\n\nexport interface EditDiffResult {\n\tdiff: string;\n\tfirstChangedLine: number | undefined;\n}\n\nexport interface EditDiffError {\n\terror: string;\n}\n\n/**\n * Compute the diff for an edit operation without applying it.\n * Used for preview rendering in the TUI before the tool executes.\n */\nexport async function computeEditDiff(\n\tpath: string,\n\toldText: string,\n\tnewText: string,\n\tcwd: string,\n): Promise<EditDiffResult | EditDiffError> {\n\tconst absolutePath = resolveToCwd(path, cwd);\n\n\ttry {\n\t\t// Check if file exists and is readable\n\t\ttry {\n\t\t\tawait access(absolutePath, constants.R_OK);\n\t\t} catch {\n\t\t\treturn { error: `File not found: ${path}` };\n\t\t}\n\n\t\t// Read the file\n\t\tconst rawContent = await readFile(absolutePath, \"utf-8\");\n\n\t\t// Strip BOM before matching (LLM won't include invisible BOM in oldText)\n\t\tconst { text: content } = stripBom(rawContent);\n\n\t\tconst normalizedContent = normalizeToLF(content);\n\t\tconst normalizedOldText = normalizeToLF(oldText);\n\t\tconst normalizedNewText = normalizeToLF(newText);\n\n\t\t// Find the old text using fuzzy matching (tries exact match first, then fuzzy)\n\t\tconst matchResult = fuzzyFindText(normalizedContent, normalizedOldText);\n\n\t\tif (!matchResult.found) {\n\t\t\treturn {\n\t\t\t\terror: `Could not find the exact text in ${path}. The old text must match exactly including all whitespace and newlines.`,\n\t\t\t};\n\t\t}\n\n\t\t// Count occurrences using fuzzy-normalized content for consistency\n\t\tconst fuzzyContent = normalizeForFuzzyMatch(normalizedContent);\n\t\tconst fuzzyOldText = normalizeForFuzzyMatch(normalizedOldText);\n\t\tconst occurrences = fuzzyContent.split(fuzzyOldText).length - 1;\n\n\t\tif (occurrences > 1) {\n\t\t\treturn {\n\t\t\t\terror: `Found ${occurrences} occurrences of the text in ${path}. The text must be unique. Please provide more context to make it unique.`,\n\t\t\t};\n\t\t}\n\n\t\t// Compute the new content using the matched position\n\t\t// When fuzzy matching was used, contentForReplacement is the normalized version\n\t\tconst baseContent = matchResult.contentForReplacement;\n\t\tconst newContent =\n\t\t\tbaseContent.substring(0, matchResult.index) +\n\t\t\tnormalizedNewText +\n\t\t\tbaseContent.substring(matchResult.index + matchResult.matchLength);\n\n\t\t// Check if it would actually change anything\n\t\tif (baseContent === newContent) {\n\t\t\treturn {\n\t\t\t\terror: `No changes would be made to ${path}. The replacement produces identical content.`,\n\t\t\t};\n\t\t}\n\n\t\t// Generate the diff\n\t\treturn generateDiffString(baseContent, newContent);\n\t} catch (err) {\n\t\treturn { error: err instanceof Error ? err.message : String(err) };\n\t}\n}\n"]}
@@ -30,6 +30,7 @@ export function restoreLineEndings(text, ending) {
30
30
  */
31
31
  export function normalizeForFuzzyMatch(text) {
32
32
  return (text
33
+ .normalize("NFKC")
33
34
  // Strip trailing whitespace per line
34
35
  .split("\n")
35
36
  .map((line) => line.trimEnd())