plugsuits 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (560) hide show
  1. package/cli.js +3 -0
  2. package/dist/agent-reasoning-default.test.d.ts +2 -0
  3. package/dist/agent-reasoning-default.test.d.ts.map +1 -0
  4. package/dist/agent-reasoning-default.test.js +38 -0
  5. package/dist/agent-reasoning-default.test.js.map +1 -0
  6. package/dist/agent.d.ts +61 -0
  7. package/dist/agent.d.ts.map +1 -0
  8. package/dist/agent.js +308 -0
  9. package/dist/agent.js.map +1 -0
  10. package/dist/agent.test.d.ts +2 -0
  11. package/dist/agent.test.d.ts.map +1 -0
  12. package/dist/agent.test.js +38 -0
  13. package/dist/agent.test.js.map +1 -0
  14. package/dist/cli-args.d.ts +15 -0
  15. package/dist/cli-args.d.ts.map +1 -0
  16. package/dist/cli-args.js +63 -0
  17. package/dist/cli-args.js.map +1 -0
  18. package/dist/cli-args.test.d.ts +2 -0
  19. package/dist/cli-args.test.d.ts.map +1 -0
  20. package/dist/cli-args.test.js +105 -0
  21. package/dist/cli-args.test.js.map +1 -0
  22. package/dist/commands/aliases-and-tool-fallback.test.d.ts +2 -0
  23. package/dist/commands/aliases-and-tool-fallback.test.d.ts.map +1 -0
  24. package/dist/commands/aliases-and-tool-fallback.test.js +132 -0
  25. package/dist/commands/aliases-and-tool-fallback.test.js.map +1 -0
  26. package/dist/commands/clear.d.ts +3 -0
  27. package/dist/commands/clear.d.ts.map +1 -0
  28. package/dist/commands/clear.js +12 -0
  29. package/dist/commands/clear.js.map +1 -0
  30. package/dist/commands/factories/create-toggle-command.d.ts +13 -0
  31. package/dist/commands/factories/create-toggle-command.d.ts.map +1 -0
  32. package/dist/commands/factories/create-toggle-command.js +38 -0
  33. package/dist/commands/factories/create-toggle-command.js.map +1 -0
  34. package/dist/commands/help.d.ts +3 -0
  35. package/dist/commands/help.d.ts.map +1 -0
  36. package/dist/commands/help.js +23 -0
  37. package/dist/commands/help.js.map +1 -0
  38. package/dist/commands/index.d.ts +18 -0
  39. package/dist/commands/index.d.ts.map +1 -0
  40. package/dist/commands/index.js +82 -0
  41. package/dist/commands/index.js.map +1 -0
  42. package/dist/commands/model.d.ts +15 -0
  43. package/dist/commands/model.d.ts.map +1 -0
  44. package/dist/commands/model.js +100 -0
  45. package/dist/commands/model.js.map +1 -0
  46. package/dist/commands/reasoning-mode.d.ts +3 -0
  47. package/dist/commands/reasoning-mode.d.ts.map +1 -0
  48. package/dist/commands/reasoning-mode.js +47 -0
  49. package/dist/commands/reasoning-mode.js.map +1 -0
  50. package/dist/commands/render.d.ts +19 -0
  51. package/dist/commands/render.d.ts.map +1 -0
  52. package/dist/commands/render.js +140 -0
  53. package/dist/commands/render.js.map +1 -0
  54. package/dist/commands/render.test.d.ts +2 -0
  55. package/dist/commands/render.test.d.ts.map +1 -0
  56. package/dist/commands/render.test.js +36 -0
  57. package/dist/commands/render.test.js.map +1 -0
  58. package/dist/commands/tool-fallback.d.ts +3 -0
  59. package/dist/commands/tool-fallback.d.ts.map +1 -0
  60. package/dist/commands/tool-fallback.js +38 -0
  61. package/dist/commands/tool-fallback.js.map +1 -0
  62. package/dist/commands/translate.d.ts +3 -0
  63. package/dist/commands/translate.d.ts.map +1 -0
  64. package/dist/commands/translate.js +12 -0
  65. package/dist/commands/translate.js.map +1 -0
  66. package/dist/commands/translate.test.d.ts +2 -0
  67. package/dist/commands/translate.test.d.ts.map +1 -0
  68. package/dist/commands/translate.test.js +49 -0
  69. package/dist/commands/translate.test.js.map +1 -0
  70. package/dist/commands/types.d.ts +17 -0
  71. package/dist/commands/types.d.ts.map +1 -0
  72. package/dist/commands/types.js +2 -0
  73. package/dist/commands/types.js.map +1 -0
  74. package/dist/context/environment-context.d.ts +2 -0
  75. package/dist/context/environment-context.d.ts.map +1 -0
  76. package/dist/context/environment-context.js +11 -0
  77. package/dist/context/environment-context.js.map +1 -0
  78. package/dist/context/paths.d.ts +3 -0
  79. package/dist/context/paths.d.ts.map +1 -0
  80. package/dist/context/paths.js +3 -0
  81. package/dist/context/paths.js.map +1 -0
  82. package/dist/context/session.d.ts +4 -0
  83. package/dist/context/session.d.ts.map +1 -0
  84. package/dist/context/session.js +16 -0
  85. package/dist/context/session.js.map +1 -0
  86. package/dist/context/skill-command-prefix.d.ts +4 -0
  87. package/dist/context/skill-command-prefix.d.ts.map +1 -0
  88. package/dist/context/skill-command-prefix.js +15 -0
  89. package/dist/context/skill-command-prefix.js.map +1 -0
  90. package/dist/context/skills-integration.test.d.ts +2 -0
  91. package/dist/context/skills-integration.test.d.ts.map +1 -0
  92. package/dist/context/skills-integration.test.js +87 -0
  93. package/dist/context/skills-integration.test.js.map +1 -0
  94. package/dist/context/skills.d.ts +18 -0
  95. package/dist/context/skills.d.ts.map +1 -0
  96. package/dist/context/skills.js +431 -0
  97. package/dist/context/skills.js.map +1 -0
  98. package/dist/context/skills.test.d.ts +2 -0
  99. package/dist/context/skills.test.d.ts.map +1 -0
  100. package/dist/context/skills.test.js +20 -0
  101. package/dist/context/skills.test.js.map +1 -0
  102. package/dist/context/system-prompt.d.ts +2 -0
  103. package/dist/context/system-prompt.d.ts.map +1 -0
  104. package/dist/context/system-prompt.js +100 -0
  105. package/dist/context/system-prompt.js.map +1 -0
  106. package/dist/context/translation-integration.test.d.ts +2 -0
  107. package/dist/context/translation-integration.test.d.ts.map +1 -0
  108. package/dist/context/translation-integration.test.js +138 -0
  109. package/dist/context/translation-integration.test.js.map +1 -0
  110. package/dist/context/translation.d.ts +21 -0
  111. package/dist/context/translation.d.ts.map +1 -0
  112. package/dist/context/translation.js +82 -0
  113. package/dist/context/translation.js.map +1 -0
  114. package/dist/context/translation.test.d.ts +2 -0
  115. package/dist/context/translation.test.d.ts.map +1 -0
  116. package/dist/context/translation.test.js +129 -0
  117. package/dist/context/translation.test.js.map +1 -0
  118. package/dist/entrypoints/cli-input-rendering.test.d.ts +2 -0
  119. package/dist/entrypoints/cli-input-rendering.test.d.ts.map +1 -0
  120. package/dist/entrypoints/cli-input-rendering.test.js +192 -0
  121. package/dist/entrypoints/cli-input-rendering.test.js.map +1 -0
  122. package/dist/entrypoints/cli.d.ts +3 -0
  123. package/dist/entrypoints/cli.d.ts.map +1 -0
  124. package/dist/entrypoints/cli.js +1268 -0
  125. package/dist/entrypoints/cli.js.map +1 -0
  126. package/dist/entrypoints/headless-agent-config.d.ts +21 -0
  127. package/dist/entrypoints/headless-agent-config.d.ts.map +1 -0
  128. package/dist/entrypoints/headless-agent-config.js +15 -0
  129. package/dist/entrypoints/headless-agent-config.js.map +1 -0
  130. package/dist/entrypoints/headless-agent-config.test.d.ts +2 -0
  131. package/dist/entrypoints/headless-agent-config.test.d.ts.map +1 -0
  132. package/dist/entrypoints/headless-agent-config.test.js +63 -0
  133. package/dist/entrypoints/headless-agent-config.test.js.map +1 -0
  134. package/dist/entrypoints/headless.d.ts +3 -0
  135. package/dist/entrypoints/headless.d.ts.map +1 -0
  136. package/dist/entrypoints/headless.js +396 -0
  137. package/dist/entrypoints/headless.js.map +1 -0
  138. package/dist/env.d.ts +8 -0
  139. package/dist/env.d.ts.map +1 -0
  140. package/dist/env.js +14 -0
  141. package/dist/env.js.map +1 -0
  142. package/dist/friendli-models.d.ts +21 -0
  143. package/dist/friendli-models.d.ts.map +1 -0
  144. package/dist/friendli-models.js +57 -0
  145. package/dist/friendli-models.js.map +1 -0
  146. package/dist/friendli-reasoning.d.ts +10 -0
  147. package/dist/friendli-reasoning.d.ts.map +1 -0
  148. package/dist/friendli-reasoning.js +181 -0
  149. package/dist/friendli-reasoning.js.map +1 -0
  150. package/dist/friendli-reasoning.test.d.ts +2 -0
  151. package/dist/friendli-reasoning.test.d.ts.map +1 -0
  152. package/dist/friendli-reasoning.test.js +77 -0
  153. package/dist/friendli-reasoning.test.js.map +1 -0
  154. package/dist/index.d.ts +3 -0
  155. package/dist/index.d.ts.map +1 -0
  156. package/dist/index.js +3 -0
  157. package/dist/index.js.map +1 -0
  158. package/dist/interaction/colors.d.ts +22 -0
  159. package/dist/interaction/colors.d.ts.map +1 -0
  160. package/dist/interaction/colors.js +24 -0
  161. package/dist/interaction/colors.js.map +1 -0
  162. package/dist/interaction/pi-tui-stream-renderer.d.ts +19 -0
  163. package/dist/interaction/pi-tui-stream-renderer.d.ts.map +1 -0
  164. package/dist/interaction/pi-tui-stream-renderer.js +1509 -0
  165. package/dist/interaction/pi-tui-stream-renderer.js.map +1 -0
  166. package/dist/interaction/pi-tui-stream-renderer.test.d.ts +2 -0
  167. package/dist/interaction/pi-tui-stream-renderer.test.d.ts.map +1 -0
  168. package/dist/interaction/pi-tui-stream-renderer.test.js +1314 -0
  169. package/dist/interaction/pi-tui-stream-renderer.test.js.map +1 -0
  170. package/dist/interaction/spinner.d.ts +13 -0
  171. package/dist/interaction/spinner.d.ts.map +1 -0
  172. package/dist/interaction/spinner.js +51 -0
  173. package/dist/interaction/spinner.js.map +1 -0
  174. package/dist/middleware/index.d.ts +7 -0
  175. package/dist/middleware/index.d.ts.map +1 -0
  176. package/dist/middleware/index.js +15 -0
  177. package/dist/middleware/index.js.map +1 -0
  178. package/dist/middleware/todo-continuation.d.ts +11 -0
  179. package/dist/middleware/todo-continuation.d.ts.map +1 -0
  180. package/dist/middleware/todo-continuation.js +103 -0
  181. package/dist/middleware/todo-continuation.js.map +1 -0
  182. package/dist/middleware/trim-leading-newlines.d.ts +3 -0
  183. package/dist/middleware/trim-leading-newlines.d.ts.map +1 -0
  184. package/dist/middleware/trim-leading-newlines.js +49 -0
  185. package/dist/middleware/trim-leading-newlines.js.map +1 -0
  186. package/dist/reasoning-mode.d.ts +5 -0
  187. package/dist/reasoning-mode.d.ts.map +1 -0
  188. package/dist/reasoning-mode.js +30 -0
  189. package/dist/reasoning-mode.js.map +1 -0
  190. package/dist/reasoning-mode.test.d.ts +2 -0
  191. package/dist/reasoning-mode.test.d.ts.map +1 -0
  192. package/dist/reasoning-mode.test.js +22 -0
  193. package/dist/reasoning-mode.test.js.map +1 -0
  194. package/dist/tool-fallback-mode.d.ts +6 -0
  195. package/dist/tool-fallback-mode.d.ts.map +1 -0
  196. package/dist/tool-fallback-mode.js +25 -0
  197. package/dist/tool-fallback-mode.js.map +1 -0
  198. package/dist/tools/execute/shell-execute.d.ts +17 -0
  199. package/dist/tools/execute/shell-execute.d.ts.map +1 -0
  200. package/dist/tools/execute/shell-execute.js +55 -0
  201. package/dist/tools/execute/shell-execute.js.map +1 -0
  202. package/dist/tools/execute/shell-execute.test.d.ts +2 -0
  203. package/dist/tools/execute/shell-execute.test.d.ts.map +1 -0
  204. package/dist/tools/execute/shell-execute.test.js +86 -0
  205. package/dist/tools/execute/shell-execute.test.js.map +1 -0
  206. package/dist/tools/execute/shell-interact.d.ts +10 -0
  207. package/dist/tools/execute/shell-interact.d.ts.map +1 -0
  208. package/dist/tools/execute/shell-interact.js +122 -0
  209. package/dist/tools/execute/shell-interact.js.map +1 -0
  210. package/dist/tools/execute/shell-interact.test.d.ts +2 -0
  211. package/dist/tools/execute/shell-interact.test.d.ts.map +1 -0
  212. package/dist/tools/execute/shell-interact.test.js +175 -0
  213. package/dist/tools/execute/shell-interact.test.js.map +1 -0
  214. package/dist/tools/explore/glob.d.ts +15 -0
  215. package/dist/tools/explore/glob.d.ts.map +1 -0
  216. package/dist/tools/explore/glob.js +107 -0
  217. package/dist/tools/explore/glob.js.map +1 -0
  218. package/dist/tools/explore/glob.test.d.ts +2 -0
  219. package/dist/tools/explore/glob.test.d.ts.map +1 -0
  220. package/dist/tools/explore/glob.test.js +183 -0
  221. package/dist/tools/explore/glob.test.js.map +1 -0
  222. package/dist/tools/explore/grep.d.ts +27 -0
  223. package/dist/tools/explore/grep.d.ts.map +1 -0
  224. package/dist/tools/explore/grep.js +203 -0
  225. package/dist/tools/explore/grep.js.map +1 -0
  226. package/dist/tools/explore/grep.test.d.ts +2 -0
  227. package/dist/tools/explore/grep.test.d.ts.map +1 -0
  228. package/dist/tools/explore/grep.test.js +132 -0
  229. package/dist/tools/explore/grep.test.js.map +1 -0
  230. package/dist/tools/explore/read-file.d.ts +23 -0
  231. package/dist/tools/explore/read-file.d.ts.map +1 -0
  232. package/dist/tools/explore/read-file.js +84 -0
  233. package/dist/tools/explore/read-file.js.map +1 -0
  234. package/dist/tools/explore/read-file.test.d.ts +2 -0
  235. package/dist/tools/explore/read-file.test.d.ts.map +1 -0
  236. package/dist/tools/explore/read-file.test.js +278 -0
  237. package/dist/tools/explore/read-file.test.js.map +1 -0
  238. package/dist/tools/index.d.ts +71 -0
  239. package/dist/tools/index.d.ts.map +1 -0
  240. package/dist/tools/index.js +26 -0
  241. package/dist/tools/index.js.map +1 -0
  242. package/dist/tools/modify/delete-file.d.ts +19 -0
  243. package/dist/tools/modify/delete-file.d.ts.map +1 -0
  244. package/dist/tools/modify/delete-file.js +71 -0
  245. package/dist/tools/modify/delete-file.js.map +1 -0
  246. package/dist/tools/modify/delete-file.test.d.ts +2 -0
  247. package/dist/tools/modify/delete-file.test.d.ts.map +1 -0
  248. package/dist/tools/modify/delete-file.test.js +136 -0
  249. package/dist/tools/modify/delete-file.test.js.map +1 -0
  250. package/dist/tools/modify/edit-file-diagnostics.d.ts +17 -0
  251. package/dist/tools/modify/edit-file-diagnostics.d.ts.map +1 -0
  252. package/dist/tools/modify/edit-file-diagnostics.js +157 -0
  253. package/dist/tools/modify/edit-file-diagnostics.js.map +1 -0
  254. package/dist/tools/modify/edit-file-repair.d.ts +13 -0
  255. package/dist/tools/modify/edit-file-repair.d.ts.map +1 -0
  256. package/dist/tools/modify/edit-file-repair.js +135 -0
  257. package/dist/tools/modify/edit-file-repair.js.map +1 -0
  258. package/dist/tools/modify/edit-file-stress.test.d.ts +2 -0
  259. package/dist/tools/modify/edit-file-stress.test.d.ts.map +1 -0
  260. package/dist/tools/modify/edit-file-stress.test.js +163 -0
  261. package/dist/tools/modify/edit-file-stress.test.js.map +1 -0
  262. package/dist/tools/modify/edit-file-validation.d.ts +9 -0
  263. package/dist/tools/modify/edit-file-validation.d.ts.map +1 -0
  264. package/dist/tools/modify/edit-file-validation.js +86 -0
  265. package/dist/tools/modify/edit-file-validation.js.map +1 -0
  266. package/dist/tools/modify/edit-file-whitespace.test.d.ts +2 -0
  267. package/dist/tools/modify/edit-file-whitespace.test.d.ts.map +1 -0
  268. package/dist/tools/modify/edit-file-whitespace.test.js +90 -0
  269. package/dist/tools/modify/edit-file-whitespace.test.js.map +1 -0
  270. package/dist/tools/modify/edit-file.d.ts +33 -0
  271. package/dist/tools/modify/edit-file.d.ts.map +1 -0
  272. package/dist/tools/modify/edit-file.js +177 -0
  273. package/dist/tools/modify/edit-file.js.map +1 -0
  274. package/dist/tools/modify/edit-file.test.d.ts +2 -0
  275. package/dist/tools/modify/edit-file.test.d.ts.map +1 -0
  276. package/dist/tools/modify/edit-file.test.js +948 -0
  277. package/dist/tools/modify/edit-file.test.js.map +1 -0
  278. package/dist/tools/modify/write-file.d.ts +17 -0
  279. package/dist/tools/modify/write-file.d.ts.map +1 -0
  280. package/dist/tools/modify/write-file.js +39 -0
  281. package/dist/tools/modify/write-file.js.map +1 -0
  282. package/dist/tools/modify/write-file.test.d.ts +2 -0
  283. package/dist/tools/modify/write-file.test.d.ts.map +1 -0
  284. package/dist/tools/modify/write-file.test.js +168 -0
  285. package/dist/tools/modify/write-file.test.js.map +1 -0
  286. package/dist/tools/planning/load-skill.d.ts +13 -0
  287. package/dist/tools/planning/load-skill.d.ts.map +1 -0
  288. package/dist/tools/planning/load-skill.js +101 -0
  289. package/dist/tools/planning/load-skill.js.map +1 -0
  290. package/dist/tools/planning/load-skill.test.d.ts +2 -0
  291. package/dist/tools/planning/load-skill.test.d.ts.map +1 -0
  292. package/dist/tools/planning/load-skill.test.js +37 -0
  293. package/dist/tools/planning/load-skill.test.js.map +1 -0
  294. package/dist/tools/planning/todo-write.d.ts +49 -0
  295. package/dist/tools/planning/todo-write.d.ts.map +1 -0
  296. package/dist/tools/planning/todo-write.js +118 -0
  297. package/dist/tools/planning/todo-write.js.map +1 -0
  298. package/dist/tools/planning/todo-write.test.d.ts +2 -0
  299. package/dist/tools/planning/todo-write.test.d.ts.map +1 -0
  300. package/dist/tools/planning/todo-write.test.js +82 -0
  301. package/dist/tools/planning/todo-write.test.js.map +1 -0
  302. package/dist/tools/utils/execute/format-utils.d.ts +4 -0
  303. package/dist/tools/utils/execute/format-utils.d.ts.map +1 -0
  304. package/dist/tools/utils/execute/format-utils.js +31 -0
  305. package/dist/tools/utils/execute/format-utils.js.map +1 -0
  306. package/dist/tools/utils/execute/format-utils.test.d.ts +2 -0
  307. package/dist/tools/utils/execute/format-utils.test.d.ts.map +1 -0
  308. package/dist/tools/utils/execute/format-utils.test.js +40 -0
  309. package/dist/tools/utils/execute/format-utils.test.js.map +1 -0
  310. package/dist/tools/utils/execute/noninteractive-wrapper.d.ts +12 -0
  311. package/dist/tools/utils/execute/noninteractive-wrapper.d.ts.map +1 -0
  312. package/dist/tools/utils/execute/noninteractive-wrapper.js +269 -0
  313. package/dist/tools/utils/execute/noninteractive-wrapper.js.map +1 -0
  314. package/dist/tools/utils/execute/noninteractive-wrapper.test.d.ts +2 -0
  315. package/dist/tools/utils/execute/noninteractive-wrapper.test.d.ts.map +1 -0
  316. package/dist/tools/utils/execute/noninteractive-wrapper.test.js +233 -0
  317. package/dist/tools/utils/execute/noninteractive-wrapper.test.js.map +1 -0
  318. package/dist/tools/utils/execute/output-handler.d.ts +14 -0
  319. package/dist/tools/utils/execute/output-handler.d.ts.map +1 -0
  320. package/dist/tools/utils/execute/output-handler.js +71 -0
  321. package/dist/tools/utils/execute/output-handler.js.map +1 -0
  322. package/dist/tools/utils/execute/output-handler.test.d.ts +2 -0
  323. package/dist/tools/utils/execute/output-handler.test.d.ts.map +1 -0
  324. package/dist/tools/utils/execute/output-handler.test.js +58 -0
  325. package/dist/tools/utils/execute/output-handler.test.js.map +1 -0
  326. package/dist/tools/utils/execute/process-manager.d.ts +17 -0
  327. package/dist/tools/utils/execute/process-manager.d.ts.map +1 -0
  328. package/dist/tools/utils/execute/process-manager.js +229 -0
  329. package/dist/tools/utils/execute/process-manager.js.map +1 -0
  330. package/dist/tools/utils/execute/process-manager.test.d.ts +2 -0
  331. package/dist/tools/utils/execute/process-manager.test.d.ts.map +1 -0
  332. package/dist/tools/utils/execute/process-manager.test.js +139 -0
  333. package/dist/tools/utils/execute/process-manager.test.js.map +1 -0
  334. package/dist/tools/utils/execute/shell-detection.d.ts +3 -0
  335. package/dist/tools/utils/execute/shell-detection.d.ts.map +1 -0
  336. package/dist/tools/utils/execute/shell-detection.js +56 -0
  337. package/dist/tools/utils/execute/shell-detection.js.map +1 -0
  338. package/dist/tools/utils/execute/shell-detection.test.d.ts +2 -0
  339. package/dist/tools/utils/execute/shell-detection.test.d.ts.map +1 -0
  340. package/dist/tools/utils/execute/shell-detection.test.js +86 -0
  341. package/dist/tools/utils/execute/shell-detection.test.js.map +1 -0
  342. package/dist/tools/utils/hashline/autocorrect-replacement-lines.d.ts +5 -0
  343. package/dist/tools/utils/hashline/autocorrect-replacement-lines.d.ts.map +1 -0
  344. package/dist/tools/utils/hashline/autocorrect-replacement-lines.js +113 -0
  345. package/dist/tools/utils/hashline/autocorrect-replacement-lines.js.map +1 -0
  346. package/dist/tools/utils/hashline/constants.d.ts +5 -0
  347. package/dist/tools/utils/hashline/constants.d.ts.map +1 -0
  348. package/dist/tools/utils/hashline/constants.js +11 -0
  349. package/dist/tools/utils/hashline/constants.js.map +1 -0
  350. package/dist/tools/utils/hashline/diff-utils.d.ts +7 -0
  351. package/dist/tools/utils/hashline/diff-utils.d.ts.map +1 -0
  352. package/dist/tools/utils/hashline/diff-utils.js +50 -0
  353. package/dist/tools/utils/hashline/diff-utils.js.map +1 -0
  354. package/dist/tools/utils/hashline/diff-utils.test.d.ts +2 -0
  355. package/dist/tools/utils/hashline/diff-utils.test.d.ts.map +1 -0
  356. package/dist/tools/utils/hashline/diff-utils.test.js +46 -0
  357. package/dist/tools/utils/hashline/diff-utils.test.js.map +1 -0
  358. package/dist/tools/utils/hashline/edit-deduplication.d.ts +6 -0
  359. package/dist/tools/utils/hashline/edit-deduplication.d.ts.map +1 -0
  360. package/dist/tools/utils/hashline/edit-deduplication.js +32 -0
  361. package/dist/tools/utils/hashline/edit-deduplication.js.map +1 -0
  362. package/dist/tools/utils/hashline/edit-operation-primitives.d.ts +11 -0
  363. package/dist/tools/utils/hashline/edit-operation-primitives.d.ts.map +1 -0
  364. package/dist/tools/utils/hashline/edit-operation-primitives.js +93 -0
  365. package/dist/tools/utils/hashline/edit-operation-primitives.js.map +1 -0
  366. package/dist/tools/utils/hashline/edit-operations.d.ts +9 -0
  367. package/dist/tools/utils/hashline/edit-operations.d.ts.map +1 -0
  368. package/dist/tools/utils/hashline/edit-operations.js +101 -0
  369. package/dist/tools/utils/hashline/edit-operations.js.map +1 -0
  370. package/dist/tools/utils/hashline/edit-operations.test.d.ts +2 -0
  371. package/dist/tools/utils/hashline/edit-operations.test.d.ts.map +1 -0
  372. package/dist/tools/utils/hashline/edit-operations.test.js +135 -0
  373. package/dist/tools/utils/hashline/edit-operations.test.js.map +1 -0
  374. package/dist/tools/utils/hashline/edit-ordering.d.ts +5 -0
  375. package/dist/tools/utils/hashline/edit-ordering.d.ts.map +1 -0
  376. package/dist/tools/utils/hashline/edit-ordering.js +54 -0
  377. package/dist/tools/utils/hashline/edit-ordering.js.map +1 -0
  378. package/dist/tools/utils/hashline/edit-text-normalization.d.ts +9 -0
  379. package/dist/tools/utils/hashline/edit-text-normalization.d.ts.map +1 -0
  380. package/dist/tools/utils/hashline/edit-text-normalization.js +135 -0
  381. package/dist/tools/utils/hashline/edit-text-normalization.js.map +1 -0
  382. package/dist/tools/utils/hashline/file-text-canonicalization.d.ts +8 -0
  383. package/dist/tools/utils/hashline/file-text-canonicalization.d.ts.map +1 -0
  384. package/dist/tools/utils/hashline/file-text-canonicalization.js +42 -0
  385. package/dist/tools/utils/hashline/file-text-canonicalization.js.map +1 -0
  386. package/dist/tools/utils/hashline/hash-computation.d.ts +14 -0
  387. package/dist/tools/utils/hashline/hash-computation.d.ts.map +1 -0
  388. package/dist/tools/utils/hashline/hash-computation.js +158 -0
  389. package/dist/tools/utils/hashline/hash-computation.js.map +1 -0
  390. package/dist/tools/utils/hashline/hash-computation.test.d.ts +2 -0
  391. package/dist/tools/utils/hashline/hash-computation.test.d.ts.map +1 -0
  392. package/dist/tools/utils/hashline/hash-computation.test.js +63 -0
  393. package/dist/tools/utils/hashline/hash-computation.test.js.map +1 -0
  394. package/dist/tools/utils/hashline/hashline-chunk-formatter.d.ts +11 -0
  395. package/dist/tools/utils/hashline/hashline-chunk-formatter.d.ts.map +1 -0
  396. package/dist/tools/utils/hashline/hashline-chunk-formatter.js +41 -0
  397. package/dist/tools/utils/hashline/hashline-chunk-formatter.js.map +1 -0
  398. package/dist/tools/utils/hashline/hashline-edit-diff.d.ts +2 -0
  399. package/dist/tools/utils/hashline/hashline-edit-diff.d.ts.map +1 -0
  400. package/dist/tools/utils/hashline/hashline-edit-diff.js +27 -0
  401. package/dist/tools/utils/hashline/hashline-edit-diff.js.map +1 -0
  402. package/dist/tools/utils/hashline/index.d.ts +14 -0
  403. package/dist/tools/utils/hashline/index.d.ts.map +1 -0
  404. package/dist/tools/utils/hashline/index.js +11 -0
  405. package/dist/tools/utils/hashline/index.js.map +1 -0
  406. package/dist/tools/utils/hashline/merge-expansion.d.ts +4 -0
  407. package/dist/tools/utils/hashline/merge-expansion.d.ts.map +1 -0
  408. package/dist/tools/utils/hashline/merge-expansion.js +105 -0
  409. package/dist/tools/utils/hashline/merge-expansion.js.map +1 -0
  410. package/dist/tools/utils/hashline/normalize-edits.d.ts +11 -0
  411. package/dist/tools/utils/hashline/normalize-edits.d.ts.map +1 -0
  412. package/dist/tools/utils/hashline/normalize-edits.js +81 -0
  413. package/dist/tools/utils/hashline/normalize-edits.js.map +1 -0
  414. package/dist/tools/utils/hashline/types.d.ts +18 -0
  415. package/dist/tools/utils/hashline/types.d.ts.map +1 -0
  416. package/dist/tools/utils/hashline/types.js +2 -0
  417. package/dist/tools/utils/hashline/types.js.map +1 -0
  418. package/dist/tools/utils/hashline/validation.d.ts +19 -0
  419. package/dist/tools/utils/hashline/validation.d.ts.map +1 -0
  420. package/dist/tools/utils/hashline/validation.js +161 -0
  421. package/dist/tools/utils/hashline/validation.js.map +1 -0
  422. package/dist/tools/utils/hashline/validation.test.d.ts +2 -0
  423. package/dist/tools/utils/hashline/validation.test.d.ts.map +1 -0
  424. package/dist/tools/utils/hashline/validation.test.js +86 -0
  425. package/dist/tools/utils/hashline/validation.test.js.map +1 -0
  426. package/dist/tools/utils/safety-utils.d.ts +66 -0
  427. package/dist/tools/utils/safety-utils.d.ts.map +1 -0
  428. package/dist/tools/utils/safety-utils.js +681 -0
  429. package/dist/tools/utils/safety-utils.js.map +1 -0
  430. package/dist/utils/tools-manager.d.ts +16 -0
  431. package/dist/utils/tools-manager.d.ts.map +1 -0
  432. package/dist/utils/tools-manager.js +257 -0
  433. package/dist/utils/tools-manager.js.map +1 -0
  434. package/package.json +49 -0
  435. package/src/AGENTS.md +52 -0
  436. package/src/agent-reasoning-default.test.ts +48 -0
  437. package/src/agent.test.ts +49 -0
  438. package/src/agent.ts +455 -0
  439. package/src/cli-args.test.ts +152 -0
  440. package/src/cli-args.ts +90 -0
  441. package/src/commands/aliases-and-tool-fallback.test.ts +172 -0
  442. package/src/commands/clear.ts +14 -0
  443. package/src/commands/factories/create-toggle-command.ts +68 -0
  444. package/src/commands/help.ts +30 -0
  445. package/src/commands/index.ts +125 -0
  446. package/src/commands/model.ts +146 -0
  447. package/src/commands/reasoning-mode.ts +55 -0
  448. package/src/commands/render.test.ts +47 -0
  449. package/src/commands/render.ts +205 -0
  450. package/src/commands/tool-fallback.ts +47 -0
  451. package/src/commands/translate.test.ts +62 -0
  452. package/src/commands/translate.ts +14 -0
  453. package/src/commands/types.ts +18 -0
  454. package/src/context/environment-context.ts +11 -0
  455. package/src/context/paths.ts +2 -0
  456. package/src/context/session.ts +18 -0
  457. package/src/context/skill-command-prefix.ts +18 -0
  458. package/src/context/skills-integration.test.ts +113 -0
  459. package/src/context/skills.test.ts +25 -0
  460. package/src/context/skills.ts +566 -0
  461. package/src/context/system-prompt.ts +100 -0
  462. package/src/context/translation-integration.test.ts +194 -0
  463. package/src/context/translation.test.ts +186 -0
  464. package/src/context/translation.ts +122 -0
  465. package/src/entrypoints/AGENTS.md +33 -0
  466. package/src/entrypoints/cli-input-rendering.test.ts +236 -0
  467. package/src/entrypoints/cli.ts +1845 -0
  468. package/src/entrypoints/headless-agent-config.test.ts +82 -0
  469. package/src/entrypoints/headless-agent-config.ts +42 -0
  470. package/src/entrypoints/headless.ts +622 -0
  471. package/src/env.ts +14 -0
  472. package/src/friendli-models.ts +81 -0
  473. package/src/friendli-reasoning.test.ts +147 -0
  474. package/src/friendli-reasoning.ts +280 -0
  475. package/src/index.ts +3 -0
  476. package/src/interaction/colors.ts +24 -0
  477. package/src/interaction/pi-tui-stream-renderer.test.ts +1471 -0
  478. package/src/interaction/pi-tui-stream-renderer.ts +2150 -0
  479. package/src/interaction/spinner.ts +61 -0
  480. package/src/middleware/index.ts +32 -0
  481. package/src/middleware/todo-continuation.ts +128 -0
  482. package/src/middleware/trim-leading-newlines.ts +66 -0
  483. package/src/reasoning-mode.test.ts +24 -0
  484. package/src/reasoning-mode.ts +40 -0
  485. package/src/skills/example/SKILL.md +44 -0
  486. package/src/skills/example/references/api.md +37 -0
  487. package/src/skills/example/scripts/setup.sh +13 -0
  488. package/src/skills/git-workflow.md +405 -0
  489. package/src/tool-fallback-mode.ts +34 -0
  490. package/src/tools/AGENTS.md +44 -0
  491. package/src/tools/execute/shell-execute.test.ts +114 -0
  492. package/src/tools/execute/shell-execute.ts +74 -0
  493. package/src/tools/execute/shell-execute.txt +27 -0
  494. package/src/tools/execute/shell-interact.test.ts +236 -0
  495. package/src/tools/execute/shell-interact.ts +151 -0
  496. package/src/tools/execute/shell-interact.txt +15 -0
  497. package/src/tools/explore/glob-files.txt +8 -0
  498. package/src/tools/explore/glob.test.ts +217 -0
  499. package/src/tools/explore/glob.ts +137 -0
  500. package/src/tools/explore/grep-files.txt +12 -0
  501. package/src/tools/explore/grep.test.ts +183 -0
  502. package/src/tools/explore/grep.ts +266 -0
  503. package/src/tools/explore/read-file.test.ts +355 -0
  504. package/src/tools/explore/read-file.ts +102 -0
  505. package/src/tools/explore/read-file.txt +24 -0
  506. package/src/tools/index.ts +29 -0
  507. package/src/tools/modify/AGENTS.md +38 -0
  508. package/src/tools/modify/delete-file.test.ts +200 -0
  509. package/src/tools/modify/delete-file.ts +95 -0
  510. package/src/tools/modify/delete-file.txt +9 -0
  511. package/src/tools/modify/edit-file-diagnostics.ts +210 -0
  512. package/src/tools/modify/edit-file-repair.ts +183 -0
  513. package/src/tools/modify/edit-file-stress.test.ts +200 -0
  514. package/src/tools/modify/edit-file-validation.ts +134 -0
  515. package/src/tools/modify/edit-file-whitespace.test.ts +117 -0
  516. package/src/tools/modify/edit-file.test.ts +1231 -0
  517. package/src/tools/modify/edit-file.ts +252 -0
  518. package/src/tools/modify/edit-file.txt +73 -0
  519. package/src/tools/modify/write-file.test.ts +240 -0
  520. package/src/tools/modify/write-file.ts +56 -0
  521. package/src/tools/modify/write-file.txt +9 -0
  522. package/src/tools/planning/load-skill.test.ts +48 -0
  523. package/src/tools/planning/load-skill.ts +136 -0
  524. package/src/tools/planning/load-skill.txt +6 -0
  525. package/src/tools/planning/todo-write.test.ts +91 -0
  526. package/src/tools/planning/todo-write.ts +141 -0
  527. package/src/tools/planning/todo-write.txt +7 -0
  528. package/src/tools/utils/execute/format-utils.test.ts +53 -0
  529. package/src/tools/utils/execute/format-utils.ts +37 -0
  530. package/src/tools/utils/execute/noninteractive-wrapper.test.ts +306 -0
  531. package/src/tools/utils/execute/noninteractive-wrapper.ts +314 -0
  532. package/src/tools/utils/execute/output-handler.test.ts +72 -0
  533. package/src/tools/utils/execute/output-handler.ts +101 -0
  534. package/src/tools/utils/execute/process-manager.test.ts +175 -0
  535. package/src/tools/utils/execute/process-manager.ts +310 -0
  536. package/src/tools/utils/execute/shell-detection.test.ts +112 -0
  537. package/src/tools/utils/execute/shell-detection.ts +72 -0
  538. package/src/tools/utils/hashline/autocorrect-replacement-lines.ts +159 -0
  539. package/src/tools/utils/hashline/constants.ts +13 -0
  540. package/src/tools/utils/hashline/diff-utils.test.ts +61 -0
  541. package/src/tools/utils/hashline/diff-utils.ts +64 -0
  542. package/src/tools/utils/hashline/edit-deduplication.ts +40 -0
  543. package/src/tools/utils/hashline/edit-operation-primitives.ts +149 -0
  544. package/src/tools/utils/hashline/edit-operations.test.ts +154 -0
  545. package/src/tools/utils/hashline/edit-operations.ts +132 -0
  546. package/src/tools/utils/hashline/edit-ordering.ts +60 -0
  547. package/src/tools/utils/hashline/edit-text-normalization.ts +180 -0
  548. package/src/tools/utils/hashline/file-text-canonicalization.ts +58 -0
  549. package/src/tools/utils/hashline/hash-computation.test.ts +82 -0
  550. package/src/tools/utils/hashline/hash-computation.ts +199 -0
  551. package/src/tools/utils/hashline/hashline-chunk-formatter.ts +61 -0
  552. package/src/tools/utils/hashline/hashline-edit-diff.ts +35 -0
  553. package/src/tools/utils/hashline/index.ts +55 -0
  554. package/src/tools/utils/hashline/merge-expansion.ts +120 -0
  555. package/src/tools/utils/hashline/normalize-edits.ts +127 -0
  556. package/src/tools/utils/hashline/types.ts +20 -0
  557. package/src/tools/utils/hashline/validation.test.ts +109 -0
  558. package/src/tools/utils/hashline/validation.ts +212 -0
  559. package/src/tools/utils/safety-utils.ts +938 -0
  560. package/src/utils/tools-manager.ts +353 -0
@@ -0,0 +1,252 @@
1
+ import { mkdir, readFile } from "node:fs/promises";
2
+ import { dirname } from "node:path";
3
+ import { tool } from "ai";
4
+ import { z } from "zod";
5
+ import {
6
+ applyHashlineEditsWithReport,
7
+ canonicalizeFileText,
8
+ type HashlineEdit,
9
+ normalizeHashlineEdits,
10
+ restoreFileText,
11
+ } from "../utils/hashline";
12
+ import { assertWriteSafety, safeAtomicWriteFile } from "../utils/safety-utils";
13
+ import EDIT_FILE_DESCRIPTION from "./edit-file.txt";
14
+ import { buildEscalationBailMessage } from "./edit-file-diagnostics";
15
+ import type { HashlineToolEdit } from "./edit-file-repair";
16
+ import {
17
+ assertExpectedFileHash,
18
+ canCreateFromMissingFile,
19
+ validateAndRepairEdits,
20
+ } from "./edit-file-validation";
21
+
22
+ const hashlineToolEditSchema = z
23
+ .object({
24
+ op: z
25
+ .enum(["replace", "append", "prepend"])
26
+ .describe("replace/append/prepend use hashline anchors."),
27
+ pos: z
28
+ .string()
29
+ .optional()
30
+ .describe(
31
+ "Line anchor from read_file (format: {line_number}#{hash_id})."
32
+ ),
33
+ end: z
34
+ .string()
35
+ .optional()
36
+ .describe(
37
+ "Range end anchor for replace operations (format: {line_number}#{hash_id})."
38
+ ),
39
+ lines: z
40
+ .union([z.array(z.string()), z.string(), z.null()])
41
+ .describe(
42
+ "Replacement content. string[] for new lines, string for single line, null or [] to delete."
43
+ ),
44
+ })
45
+ .strict();
46
+
47
+ // Lenient schema: lines optional — fallback when model omits lines
48
+ // Custom validation in validateAndRepairEdits provides better error messages than ZodError
49
+ const lenientEditSchema = z
50
+ .object({
51
+ op: z.enum(["replace", "append", "prepend"]),
52
+ pos: z.string().optional(),
53
+ end: z.string().optional(),
54
+ lines: z.union([z.array(z.string()), z.string(), z.null()]).optional(),
55
+ })
56
+ .strict();
57
+
58
+ const lenientInputSchema = z
59
+ .object({
60
+ path: z.string(),
61
+ edits: z.array(lenientEditSchema).min(1),
62
+ expected_file_hash: z.string().optional(),
63
+ })
64
+ .strict();
65
+
66
+ const inputSchema = z
67
+ .object({
68
+ path: z.string().describe("The path to the file"),
69
+ edits: z
70
+ .array(hashlineToolEditSchema)
71
+ .min(1)
72
+ .describe("Hashline-native edit operations."),
73
+ expected_file_hash: z
74
+ .string()
75
+ .optional()
76
+ .describe("Optional stale-check file hash from read_file output."),
77
+ })
78
+ .strict();
79
+
80
+ export type EditFileInput = z.input<typeof lenientInputSchema>;
81
+
82
+ export interface EditFileOptions {
83
+ /** Override project root for safety checks (defaults to process.cwd()). */
84
+ rootDir?: string;
85
+ }
86
+
87
+ // validateAndRepairEdits, canCreateFromMissingFile, assertExpectedFileHash
88
+ // moved to ./edit-file-validation.ts
89
+
90
+ function formatResult(params: {
91
+ created: boolean;
92
+ editCount: number;
93
+ lineCountDelta: number;
94
+ path: string;
95
+ warningLines?: string[];
96
+ }): string {
97
+ const action = params.created ? "Created" : "Updated";
98
+ const output: string[] = [`${action} ${params.path}`];
99
+ const summaryParts: string[] = [`${params.editCount} edit(s) applied`];
100
+ if (params.lineCountDelta !== 0) {
101
+ const sign = params.lineCountDelta > 0 ? "+" : "";
102
+ summaryParts.push(`${sign}${params.lineCountDelta} line(s)`);
103
+ }
104
+ output.push(summaryParts.join(", "));
105
+ if (params.warningLines && params.warningLines.length > 0) {
106
+ output.push("");
107
+ output.push("Warnings:");
108
+ output.push(...params.warningLines);
109
+ }
110
+ return output.join("\n");
111
+ }
112
+
113
+ async function ensureParentDir(path: string): Promise<void> {
114
+ const directory = dirname(path);
115
+ if (directory !== ".") {
116
+ await mkdir(directory, { recursive: true });
117
+ }
118
+ }
119
+
120
+ async function readExistingContent(path: string): Promise<{
121
+ content: string;
122
+ exists: boolean;
123
+ }> {
124
+ try {
125
+ return {
126
+ content: await readFile(path, "utf-8"),
127
+ exists: true,
128
+ };
129
+ } catch (error) {
130
+ if (
131
+ error instanceof Error &&
132
+ "code" in error &&
133
+ (error as NodeJS.ErrnoException).code === "ENOENT"
134
+ ) {
135
+ return {
136
+ content: "",
137
+ exists: false,
138
+ };
139
+ }
140
+ throw error;
141
+ }
142
+ }
143
+
144
+ // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: complex validation logic
145
+ export async function executeEditFile(input: EditFileInput, options?: EditFileOptions): Promise<string> {
146
+ let parsed: z.infer<typeof inputSchema>;
147
+ try {
148
+ parsed = inputSchema.parse(input);
149
+ } catch (error) {
150
+ if (
151
+ error instanceof z.ZodError &&
152
+ error.issues.some(
153
+ (issue) => issue.path.length >= 2 && issue.path.at(-1) === "lines"
154
+ )
155
+ ) {
156
+ // Fall back to lenient parse — custom validation gives better error messages
157
+ parsed = lenientInputSchema.parse(input) as z.infer<typeof inputSchema>;
158
+ } else {
159
+ throw error;
160
+ }
161
+ }
162
+
163
+ // C-1 + C-2: Path traversal and symlink safety checks
164
+ const safePath = await assertWriteSafety(parsed.path, options?.rootDir);
165
+
166
+ const { content: rawContent, exists } = await readExistingContent(
167
+ safePath
168
+ );
169
+ const oldEnvelope = canonicalizeFileText(rawContent);
170
+ const fileLines = exists ? oldEnvelope.content.split("\n") : [];
171
+
172
+ let repairWarnings: string[];
173
+ let normalizedEdits: HashlineEdit[];
174
+ try {
175
+ const validateResult = validateAndRepairEdits(
176
+ parsed.edits as HashlineToolEdit[],
177
+ fileLines,
178
+ parsed.path
179
+ );
180
+ repairWarnings = validateResult.repairWarnings;
181
+ normalizedEdits = normalizeHashlineEdits(validateResult.edits);
182
+ } catch (parseError) {
183
+ if (
184
+ parseError instanceof Error &&
185
+ parseError.message.includes("explicit 'lines'")
186
+ ) {
187
+ const bailMessage = buildEscalationBailMessage(
188
+ parsed.edits,
189
+ parsed.path,
190
+ fileLines
191
+ );
192
+ if (bailMessage) {
193
+ return bailMessage;
194
+ }
195
+ }
196
+ throw parseError;
197
+ }
198
+
199
+ if (!(exists || canCreateFromMissingFile(normalizedEdits))) {
200
+ throw new Error(`File not found: ${parsed.path}`);
201
+ }
202
+
203
+ assertExpectedFileHash(parsed.expected_file_hash, rawContent);
204
+
205
+ const applyResult = applyHashlineEditsWithReport(
206
+ oldEnvelope.content,
207
+ normalizedEdits
208
+ );
209
+ const canonicalNewContent = applyResult.content;
210
+
211
+ if (canonicalNewContent === oldEnvelope.content) {
212
+ let diagnostic = `No changes made to ${parsed.path}. The edits produced identical content.`;
213
+ if (applyResult.noopEdits > 0) {
214
+ diagnostic += ` No-op edits: ${applyResult.noopEdits}. Re-read the file and provide content that differs from current lines.`;
215
+ }
216
+ return `Error: ${diagnostic}`;
217
+ }
218
+
219
+ const writeContent = restoreFileText(canonicalNewContent, oldEnvelope);
220
+
221
+ await ensureParentDir(safePath);
222
+ await safeAtomicWriteFile(safePath, writeContent);
223
+
224
+ const originalLineCount = rawContent.split("\n").length;
225
+ const newLineCount = writeContent.split("\n").length;
226
+
227
+ const allWarnings: string[] = [...repairWarnings];
228
+ if (applyResult.noopEdits > 0) {
229
+ allWarnings.push(
230
+ `${applyResult.noopEdits} edit(s) were no-ops because replacement text matched existing content.`
231
+ );
232
+ }
233
+ if (applyResult.deduplicatedEdits > 0) {
234
+ allWarnings.push(
235
+ `${applyResult.deduplicatedEdits} duplicate edit(s) were removed.`
236
+ );
237
+ }
238
+
239
+ return formatResult({
240
+ created: !exists,
241
+ editCount: parsed.edits.length,
242
+ lineCountDelta: newLineCount - originalLineCount,
243
+ path: parsed.path,
244
+ warningLines: allWarnings.length > 0 ? allWarnings : undefined,
245
+ });
246
+ }
247
+
248
+ export const editFileTool = tool({
249
+ description: EDIT_FILE_DESCRIPTION,
250
+ inputSchema,
251
+ execute: (input) => executeEditFile(input),
252
+ });
@@ -0,0 +1,73 @@
1
+ Applies precise file edits using `LINE#ID` tags from `read_file` output.
2
+
3
+ **Workflow**:
4
+ 1. Call `read_file` to get fresh `{line_number}#{hash_id}|TEXT` anchors.
5
+ 2. Pick the smallest operation per change site.
6
+ 3. Call `edit_file` once per file with all operations.
7
+ 4. Re-read the file before issuing another edit on the same path.
8
+
9
+ **Input**: `{ path, edits[], expected_file_hash? }`
10
+
11
+ **Operations**:
12
+ Every edit has `op` and `lines`. Most edits also have `pos`; range replaces add `end`.
13
+ Both `pos` and `end` use `"N#ID"` format (e.g. `"23#XY"`).
14
+
15
+ `pos` — the anchor line:
16
+ - `replace`: the line to replace (or start of range)
17
+ - `prepend`: insert new lines before this line
18
+ - `append`: insert new lines after this line
19
+
20
+ `end` — range replace only. The last line of the range (inclusive).
21
+
22
+ `lines` — the replacement content (REQUIRED for every edit):
23
+ - `["line1", "line2"]` — replace with these lines
24
+ - `"line1"` — shorthand for single line
25
+ - `null` or `[]` — delete the line(s) entirely
26
+
27
+ **Line or range replace/delete**:
28
+ - `{ op: "replace", pos: "N#ID", lines: ["new content"] }` — replace one line
29
+ - `{ op: "replace", pos: "N#ID", end: "M#ID", lines: ["new"] }` — replace range
30
+ - `{ op: "replace", pos: "N#ID", lines: null }` — delete one line
31
+ - `{ op: "replace", pos: "N#ID", end: "M#ID", lines: null }` — delete range
32
+
33
+ **Insert new lines**:
34
+ - `{ op: "prepend", pos: "N#ID", lines: ["before"] }` — insert before line
35
+ - `{ op: "append", pos: "N#ID", lines: ["after"] }` — insert after line
36
+ - `{ op: "append", lines: ["at end"] }` — insert at end of file (no pos)
37
+ - `{ op: "prepend", lines: ["at start"] }` — insert at start of file (no pos)
38
+
39
+ **Rules**:
40
+ - Anchor to structural lines (function signatures, braces), not blank lines.
41
+ - Blank-line hashes are position-dependent and change after edits.
42
+ - Prefer insertion at structure boundaries over rewriting neighbors.
43
+ - Allowed keys per edit: `op`, `pos`, `end`, `lines` — nothing else. `lines` is always required.
44
+
45
+ **Recovery**:
46
+ - Tag mismatch (`>>>`): retry using fresh tags from the error snippet.
47
+ - No-op (`identical`): re-read target lines and adjust the edit.
48
+
49
+ **Examples**:
50
+ ```
51
+ edit_file({
52
+ path: "config.ts",
53
+ edits: [{ op: "replace", pos: "12#AB", lines: ["port: 8080,"] }]
54
+ })
55
+
56
+ edit_file({
57
+ path: "routes.ts",
58
+ edits: [
59
+ { op: "prepend", pos: "41#MQ", lines: [" // new route"] },
60
+ { op: "append", pos: "41#MQ", lines: [" app.get('/health', handler);"] }
61
+ ]
62
+ })
63
+
64
+ edit_file({
65
+ path: "log.ts",
66
+ edits: [{ op: "replace", pos: "3#QH", end: "5#KR", lines: ["// consolidated"] }]
67
+ })
68
+
69
+ edit_file({
70
+ path: "old.ts",
71
+ edits: [{ op: "replace", pos: "7#TX", lines: null }]
72
+ })
73
+ ```
@@ -0,0 +1,240 @@
1
+ import { afterAll, beforeAll, describe, expect, it } from "bun:test";
2
+ import {
3
+ existsSync,
4
+ mkdtempSync,
5
+ readFileSync,
6
+ rmSync,
7
+ symlinkSync,
8
+ writeFileSync,
9
+ } from "node:fs";
10
+ import { tmpdir } from "node:os";
11
+ import { join, resolve } from "node:path";
12
+ import { executeWriteFile } from "./write-file";
13
+
14
+ describe("executeWriteFile", () => {
15
+ let tempDir: string;
16
+
17
+ beforeAll(() => {
18
+ tempDir = mkdtempSync(join(tmpdir(), "write-file-test-"));
19
+ });
20
+
21
+ afterAll(() => {
22
+ if (existsSync(tempDir)) {
23
+ rmSync(tempDir, { recursive: true });
24
+ }
25
+ });
26
+
27
+ describe("basic write operations", () => {
28
+ it("creates new file and returns metadata", async () => {
29
+ const testFile = join(tempDir, "new.txt");
30
+ const content = "line1\nline2\nline3";
31
+
32
+ const result = await executeWriteFile({ path: testFile, content }, { rootDir: tempDir });
33
+
34
+ expect(result).toContain("OK - created new.txt");
35
+ expect(result).toContain("bytes:");
36
+ expect(result).toContain("lines: 3");
37
+ expect(result).not.toContain("(preview)");
38
+ expect(result).not.toContain("========");
39
+
40
+ const written = readFileSync(testFile, "utf-8");
41
+ expect(written).toBe(content);
42
+ });
43
+
44
+ it("overwrites existing file and indicates action", async () => {
45
+ const testFile = join(tempDir, "existing.txt");
46
+ writeFileSync(testFile, "old content");
47
+
48
+ const newContent = "new content";
49
+ const result = await executeWriteFile(
50
+ { path: testFile, content: newContent },
51
+ { rootDir: tempDir }
52
+ );
53
+
54
+ expect(result).toContain("OK - overwrote existing.txt");
55
+ expect(result).not.toContain("new content");
56
+
57
+ const written = readFileSync(testFile, "utf-8");
58
+ expect(written).toBe(newContent);
59
+ });
60
+
61
+ it("creates parent directories automatically", async () => {
62
+ const nestedFile = join(tempDir, "deep", "nested", "dir", "file.txt");
63
+ const content = "nested content";
64
+
65
+ const result = await executeWriteFile({ path: nestedFile, content }, { rootDir: tempDir });
66
+
67
+ expect(result).toContain("OK - created file.txt");
68
+ expect(existsSync(nestedFile)).toBe(true);
69
+
70
+ const written = readFileSync(nestedFile, "utf-8");
71
+ expect(written).toBe(content);
72
+ });
73
+ });
74
+
75
+ describe("output formatting", () => {
76
+ it("does not include content for small files", async () => {
77
+ const testFile = join(tempDir, "small.txt");
78
+ const content = "a\nb\nc\nd\ne";
79
+
80
+ const result = await executeWriteFile({ path: testFile, content }, { rootDir: tempDir });
81
+
82
+ expect(result).toContain("bytes:");
83
+ expect(result).toContain("lines: 5");
84
+ expect(result).not.toContain("a\nb\nc\nd\ne");
85
+ expect(result).not.toContain("(preview)");
86
+ });
87
+
88
+ it("does not include content for large files", async () => {
89
+ const testFile = join(tempDir, "large.txt");
90
+ const lines = Array.from({ length: 20 }, (_, i) => `line${i + 1}`);
91
+ const content = lines.join("\n");
92
+
93
+ const result = await executeWriteFile({ path: testFile, content }, { rootDir: tempDir });
94
+
95
+ expect(result).toContain("bytes:");
96
+ expect(result).toContain("lines: 20");
97
+ expect(result).not.toContain("line1");
98
+ expect(result).not.toContain("(preview)");
99
+ });
100
+
101
+ it("includes correct byte count", async () => {
102
+ const testFile = join(tempDir, "bytes.txt");
103
+ const content = "hello";
104
+
105
+ const result = await executeWriteFile({ path: testFile, content }, { rootDir: tempDir });
106
+
107
+ expect(result).toContain("bytes: 5");
108
+ });
109
+
110
+ it("handles unicode content correctly", async () => {
111
+ const testFile = join(tempDir, "unicode.txt");
112
+ const content = "한글 테스트\n이모지 🎉";
113
+
114
+ const result = await executeWriteFile({ path: testFile, content }, { rootDir: tempDir });
115
+
116
+ expect(result).not.toContain("한글 테스트");
117
+ expect(result).not.toContain("이모지 🎉");
118
+ expect(result).toContain("lines: 2");
119
+
120
+ const written = readFileSync(testFile, "utf-8");
121
+ expect(written).toBe(content);
122
+ });
123
+ });
124
+
125
+ describe("edge cases", () => {
126
+ it("handles empty content", async () => {
127
+ const testFile = join(tempDir, "empty.txt");
128
+
129
+ const result = await executeWriteFile({ path: testFile, content: "" }, { rootDir: tempDir });
130
+
131
+ expect(result).toContain("OK - created empty.txt");
132
+ expect(result).toContain("bytes: 0");
133
+ expect(result).toContain("lines: 1");
134
+
135
+ const written = readFileSync(testFile, "utf-8");
136
+ expect(written).toBe("");
137
+ });
138
+
139
+ it("handles single line content", async () => {
140
+ const testFile = join(tempDir, "single.txt");
141
+ const content = "single line without newline";
142
+
143
+ const result = await executeWriteFile({ path: testFile, content }, { rootDir: tempDir });
144
+
145
+ expect(result).toContain("lines: 1");
146
+ expect(result).not.toContain("single line without newline");
147
+ });
148
+
149
+ it("handles content with special characters", async () => {
150
+ const testFile = join(tempDir, "special.txt");
151
+ const content = `const x = { a: 1, b: "test" };\nconst y = \`template \${x}\`;`;
152
+
153
+ await executeWriteFile({ path: testFile, content }, { rootDir: tempDir });
154
+
155
+ const written = readFileSync(testFile, "utf-8");
156
+ expect(written).toBe(content);
157
+ });
158
+ });
159
+
160
+ describe("file safety (C-1, C-2, H-1)", () => {
161
+ it("C-1: blocks path traversal via .. segments", async () => {
162
+ const traversalPath = join(tempDir, "..", "..", "etc", "passwd");
163
+ await expect(
164
+ executeWriteFile({ path: traversalPath, content: "malicious" }, { rootDir: tempDir })
165
+ ).rejects.toThrow(/[Pp]ath traversal blocked/);
166
+ });
167
+
168
+ it("C-1: blocks absolute paths outside project root", async () => {
169
+ await expect(
170
+ executeWriteFile({ path: "/tmp/outside-project.txt", content: "bad" }, { rootDir: tempDir })
171
+ ).rejects.toThrow(/[Pp]ath traversal blocked|outside/);
172
+ });
173
+
174
+ it("C-2: blocks writes through symlinks", async () => {
175
+ const realFile = join(tempDir, "real-target.txt");
176
+ writeFileSync(realFile, "original");
177
+ const symlinkPath = join(tempDir, "symlink-to-real.txt");
178
+ symlinkSync(realFile, symlinkPath);
179
+
180
+ await expect(
181
+ executeWriteFile({ path: symlinkPath, content: "through symlink" }, { rootDir: tempDir })
182
+ ).rejects.toThrow(/symlink/i);
183
+
184
+ // Original file should be unchanged
185
+ expect(readFileSync(realFile, "utf-8")).toBe("original");
186
+ });
187
+
188
+ it("C-2: blocks writes through symlinks pointing outside root", async () => {
189
+ const outsideDir = mkdtempSync(join(tmpdir(), "outside-root-"));
190
+ const outsideFile = join(outsideDir, "secret.txt");
191
+ writeFileSync(outsideFile, "secret data");
192
+ const symlinkPath = join(tempDir, "escape-link.txt");
193
+ symlinkSync(outsideFile, symlinkPath);
194
+
195
+ try {
196
+ await expect(
197
+ executeWriteFile({ path: symlinkPath, content: "overwrite" }, { rootDir: tempDir })
198
+ ).rejects.toThrow(/symlink/i);
199
+ expect(readFileSync(outsideFile, "utf-8")).toBe("secret data");
200
+ } finally {
201
+ rmSync(outsideDir, { recursive: true });
202
+ }
203
+ });
204
+
205
+ it("H-1: atomic write produces correct content (no partial writes)", async () => {
206
+ const testFile = join(tempDir, "atomic-test.txt");
207
+ const content = "line1\nline2\nline3";
208
+
209
+ await executeWriteFile({ path: testFile, content }, { rootDir: tempDir });
210
+
211
+ const written = readFileSync(testFile, "utf-8");
212
+ expect(written).toBe(content);
213
+ });
214
+
215
+ it("H-1: no temp files left after successful write", async () => {
216
+ const testFile = join(tempDir, "no-temp-residue.txt");
217
+ await executeWriteFile({ path: testFile, content: "clean" }, { rootDir: tempDir });
218
+
219
+ const dirEntries = readFileSync(testFile, "utf-8");
220
+ expect(dirEntries).toBe("clean");
221
+
222
+ // Check no .tmp- files remain in the directory
223
+ const { readdirSync } = require("node:fs");
224
+ const files: string[] = readdirSync(tempDir);
225
+ const tmpFiles = files.filter((f: string) => f.includes(".tmp-"));
226
+ expect(tmpFiles.length).toBe(0);
227
+ });
228
+
229
+ it("allows writes within project root (normal operation)", async () => {
230
+ const safeFile = join(tempDir, "safe-subdir", "nested.txt");
231
+ const result = await executeWriteFile(
232
+ { path: safeFile, content: "safe content" },
233
+ { rootDir: tempDir }
234
+ );
235
+
236
+ expect(result).toContain("OK - created nested.txt");
237
+ expect(readFileSync(safeFile, "utf-8")).toBe("safe content");
238
+ });
239
+ });
240
+ });
@@ -0,0 +1,56 @@
1
+ import { mkdir } from "node:fs/promises";
2
+ import { basename, dirname } from "node:path";
3
+ import { tool } from "ai";
4
+ import { z } from "zod";
5
+ import { assertWriteSafety, safeAtomicWriteFile } from "../utils/safety-utils";
6
+ import WRITE_FILE_DESCRIPTION from "./write-file.txt";
7
+
8
+ const inputSchema = z.object({
9
+ path: z.string().describe("File path (absolute or relative)"),
10
+ content: z.string().describe("Content to write"),
11
+ });
12
+
13
+ export type WriteFileInput = z.infer<typeof inputSchema>;
14
+
15
+ export interface WriteFileOptions {
16
+ /** Override project root for safety checks (defaults to process.cwd()). */
17
+ rootDir?: string;
18
+ }
19
+
20
+ export async function executeWriteFile(
21
+ { path, content }: WriteFileInput,
22
+ options?: WriteFileOptions
23
+ ): Promise<string> {
24
+ // C-1 + C-2: Path traversal and symlink safety checks
25
+ const safePath = await assertWriteSafety(path, options?.rootDir);
26
+
27
+ const dir = dirname(safePath);
28
+ if (dir !== ".") {
29
+ await mkdir(dir, { recursive: true });
30
+ }
31
+
32
+ // H-1: Atomic write with O_EXCL temp file + rename.
33
+ // safeAtomicWriteFile handles existence check (lstat), symlink rejection,
34
+ // crypto-random temp names, O_EXCL to prevent pre-creation attacks,
35
+ // and POSIX rename() which does not follow symlinks.
36
+ const { existed } = await safeAtomicWriteFile(safePath, content);
37
+
38
+ const lines = content.split("\n");
39
+ const lineCount = lines.length;
40
+ const byteCount = Buffer.byteLength(content, "utf-8");
41
+ const fileName = basename(path);
42
+ const action = existed ? "overwrote" : "created";
43
+
44
+ const output = [
45
+ `OK - ${action} ${fileName}`,
46
+ `bytes: ${byteCount}, lines: ${lineCount}`,
47
+ ];
48
+
49
+ return output.join("\n");
50
+ }
51
+
52
+ export const writeFileTool = tool({
53
+ description: WRITE_FILE_DESCRIPTION,
54
+ inputSchema,
55
+ execute: (input) => executeWriteFile(input),
56
+ });
@@ -0,0 +1,9 @@
1
+ ### write_file
2
+ **Purpose**: Create new file or completely overwrite existing file
3
+
4
+ **When to use**:
5
+ - Creating new files from scratch
6
+ - Replacing most or all contents of a file
7
+ - Writing generated code/configuration
8
+
9
+ **DO NOT use shell**: Prefer this over `echo >`, `cat <<EOF`, `printf >`
@@ -0,0 +1,48 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { executeLoadSkill } from "./load-skill";
3
+
4
+ describe("executeLoadSkill", () => {
5
+ it("loads git-workflow skill successfully", async () => {
6
+ const result = await executeLoadSkill({ skillName: "git-workflow" });
7
+
8
+ expect(result).toContain("# Skill Loaded: git-workflow");
9
+ expect(result).toContain("# Git Workflow Skill");
10
+ expect(result).toContain("gh pr create");
11
+ });
12
+
13
+ it("returns error for non-existent skill", async () => {
14
+ const result = await executeLoadSkill({ skillName: "non-existent-skill" });
15
+
16
+ expect(result).toContain("Error: Skill 'non-existent-skill' not found");
17
+ expect(result).toContain("system prompt");
18
+ });
19
+
20
+ it("loads skill with frontmatter", async () => {
21
+ const result = await executeLoadSkill({ skillName: "git-workflow" });
22
+
23
+ expect(result).toContain("name: Git Workflow");
24
+ expect(result).toContain("description:");
25
+ expect(result).toContain("triggers:");
26
+ });
27
+
28
+ it("loads skill with prompts prefix", async () => {
29
+ const result = await executeLoadSkill({ skillName: "prompts:example" });
30
+
31
+ expect(result).toContain("# Skill Loaded: prompts:example");
32
+ expect(result).toContain("# Example Skill");
33
+ });
34
+
35
+ it("rejects path traversal attempts with ..", async () => {
36
+ const result = await executeLoadSkill({ skillName: "../../../etc/passwd" });
37
+
38
+ expect(result).toContain("Error: Invalid skill name");
39
+ expect(result).toContain("cannot contain '..' or '/'");
40
+ });
41
+
42
+ it("rejects path traversal attempts with /", async () => {
43
+ const result = await executeLoadSkill({ skillName: "foo/bar" });
44
+
45
+ expect(result).toContain("Error: Invalid skill name");
46
+ expect(result).toContain("cannot contain '..' or '/'");
47
+ });
48
+ });