lonny-agent 0.2.6 → 0.2.7

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 (475) hide show
  1. package/.github/workflows/ci.yml +21 -0
  2. package/AGENTS.md +106 -0
  3. package/CHANGELOG.md +24 -0
  4. package/dist/agent/__tests__/session-restore.test.d.ts +2 -0
  5. package/dist/agent/__tests__/session-restore.test.d.ts.map +1 -0
  6. package/dist/agent/__tests__/session-restore.test.js +260 -0
  7. package/dist/agent/__tests__/session-restore.test.js.map +1 -0
  8. package/dist/agent/commands.d.ts +26 -0
  9. package/dist/agent/commands.d.ts.map +1 -0
  10. package/dist/agent/commands.js +253 -0
  11. package/dist/agent/commands.js.map +1 -0
  12. package/dist/agent/compaction.js +8 -1
  13. package/dist/agent/compaction.js.map +1 -1
  14. package/dist/agent/index.d.ts +2 -1
  15. package/dist/agent/index.d.ts.map +1 -1
  16. package/dist/agent/index.js +2 -2
  17. package/dist/agent/index.js.map +1 -1
  18. package/dist/agent/memory.d.ts +12 -0
  19. package/dist/agent/memory.d.ts.map +1 -0
  20. package/dist/agent/memory.js +51 -0
  21. package/dist/agent/memory.js.map +1 -0
  22. package/dist/agent/prompt-builder.d.ts +6 -1
  23. package/dist/agent/prompt-builder.d.ts.map +1 -1
  24. package/dist/agent/prompt-builder.js +89 -26
  25. package/dist/agent/prompt-builder.js.map +1 -1
  26. package/dist/agent/providers/openai.d.ts +1 -0
  27. package/dist/agent/providers/openai.d.ts.map +1 -1
  28. package/dist/agent/providers/openai.js +16 -3
  29. package/dist/agent/providers/openai.js.map +1 -1
  30. package/dist/agent/session-chat.d.ts +3 -0
  31. package/dist/agent/session-chat.d.ts.map +1 -0
  32. package/dist/agent/session-chat.js +384 -0
  33. package/dist/agent/session-chat.js.map +1 -0
  34. package/dist/agent/session-display.d.ts +29 -0
  35. package/dist/agent/session-display.d.ts.map +1 -0
  36. package/dist/agent/session-display.js +179 -0
  37. package/dist/agent/session-display.js.map +1 -0
  38. package/dist/agent/session-persistence.d.ts +55 -0
  39. package/dist/agent/session-persistence.d.ts.map +1 -0
  40. package/dist/agent/session-persistence.js +101 -0
  41. package/dist/agent/session-persistence.js.map +1 -0
  42. package/dist/agent/session-utils.d.ts +5 -0
  43. package/dist/agent/session-utils.d.ts.map +1 -0
  44. package/dist/agent/session-utils.js +51 -0
  45. package/dist/agent/session-utils.js.map +1 -0
  46. package/dist/agent/session.d.ts +15 -17
  47. package/dist/agent/session.d.ts.map +1 -1
  48. package/dist/agent/session.js +226 -608
  49. package/dist/agent/session.js.map +1 -1
  50. package/dist/cli/index.d.ts +4 -0
  51. package/dist/cli/index.d.ts.map +1 -1
  52. package/dist/cli/index.js +67 -2
  53. package/dist/cli/index.js.map +1 -1
  54. package/dist/config/index.d.ts +2 -2
  55. package/dist/config/index.d.ts.map +1 -1
  56. package/dist/index.js +24 -3
  57. package/dist/index.js.map +1 -1
  58. package/dist/tools/__tests__/bash.test.js +168 -1
  59. package/dist/tools/__tests__/bash.test.js.map +1 -1
  60. package/dist/tools/__tests__/edit.test.js +253 -6
  61. package/dist/tools/__tests__/edit.test.js.map +1 -1
  62. package/dist/tools/__tests__/fetch.test.js +1 -1
  63. package/dist/tools/__tests__/fetch.test.js.map +1 -1
  64. package/dist/tools/__tests__/glob.test.js +1 -1
  65. package/dist/tools/__tests__/glob.test.js.map +1 -1
  66. package/dist/tools/__tests__/grep.test.js +1 -1
  67. package/dist/tools/__tests__/grep.test.js.map +1 -1
  68. package/dist/tools/__tests__/ls.test.js +1 -1
  69. package/dist/tools/__tests__/ls.test.js.map +1 -1
  70. package/dist/tools/__tests__/read.test.js +1 -1
  71. package/dist/tools/__tests__/read.test.js.map +1 -1
  72. package/dist/tools/__tests__/registry.test.js +313 -0
  73. package/dist/tools/__tests__/registry.test.js.map +1 -1
  74. package/dist/tools/__tests__/write_plan.test.js +1 -1
  75. package/dist/tools/__tests__/write_plan.test.js.map +1 -1
  76. package/dist/tools/bash.d.ts.map +1 -1
  77. package/dist/tools/bash.js +659 -39
  78. package/dist/tools/bash.js.map +1 -1
  79. package/dist/tools/codebase/ast/__tests__/ast-edit-demo.d.ts +2 -0
  80. package/dist/tools/codebase/ast/__tests__/ast-edit-demo.d.ts.map +1 -0
  81. package/dist/tools/codebase/ast/__tests__/ast-edit-demo.js +7 -0
  82. package/dist/tools/codebase/ast/__tests__/ast-edit-demo.js.map +1 -0
  83. package/dist/tools/codebase/ast/__tests__/ast-edit-final.d.ts +6 -0
  84. package/dist/tools/codebase/ast/__tests__/ast-edit-final.d.ts.map +1 -0
  85. package/dist/tools/codebase/ast/__tests__/ast-edit-final.js +11 -0
  86. package/dist/tools/codebase/ast/__tests__/ast-edit-final.js.map +1 -0
  87. package/dist/tools/codebase/ast/__tests__/ast-edit-test.d.ts +2 -0
  88. package/dist/tools/codebase/ast/__tests__/ast-edit-test.d.ts.map +1 -0
  89. package/dist/tools/codebase/ast/__tests__/ast-edit-test.js +9 -0
  90. package/dist/tools/codebase/ast/__tests__/ast-edit-test.js.map +1 -0
  91. package/dist/tools/codebase/ast/__tests__/integration.test.d.ts +2 -0
  92. package/dist/tools/codebase/ast/__tests__/integration.test.d.ts.map +1 -0
  93. package/dist/tools/codebase/ast/__tests__/integration.test.js +415 -0
  94. package/dist/tools/codebase/ast/__tests__/integration.test.js.map +1 -0
  95. package/dist/tools/codebase/ast/__tests__/tree-sitter.test.d.ts +2 -0
  96. package/dist/tools/codebase/ast/__tests__/tree-sitter.test.d.ts.map +1 -0
  97. package/dist/tools/codebase/ast/__tests__/tree-sitter.test.js +414 -0
  98. package/dist/tools/codebase/ast/__tests__/tree-sitter.test.js.map +1 -0
  99. package/dist/tools/codebase/ast/adapter.d.ts +23 -0
  100. package/dist/tools/codebase/ast/adapter.d.ts.map +1 -0
  101. package/dist/tools/codebase/ast/adapter.js +11 -0
  102. package/dist/tools/codebase/ast/adapter.js.map +1 -0
  103. package/dist/tools/codebase/ast/tools.d.ts +3 -0
  104. package/dist/tools/codebase/ast/tools.d.ts.map +1 -0
  105. package/dist/tools/codebase/ast/tools.js +286 -0
  106. package/dist/tools/codebase/ast/tools.js.map +1 -0
  107. package/dist/tools/codebase/ast/tree-sitter-adapter.d.ts +3 -0
  108. package/dist/tools/codebase/ast/tree-sitter-adapter.d.ts.map +1 -0
  109. package/dist/tools/codebase/ast/tree-sitter-adapter.js +412 -0
  110. package/dist/tools/codebase/ast/tree-sitter-adapter.js.map +1 -0
  111. package/dist/tools/codebase/ast/tree-sitter-converters.d.ts +4 -0
  112. package/dist/tools/codebase/ast/tree-sitter-converters.d.ts.map +1 -0
  113. package/dist/tools/codebase/ast/tree-sitter-converters.js +447 -0
  114. package/dist/tools/codebase/ast/tree-sitter-converters.js.map +1 -0
  115. package/dist/tools/codebase/ast/types.d.ts +103 -0
  116. package/dist/tools/codebase/ast/types.d.ts.map +1 -0
  117. package/dist/tools/codebase/ast/types.js +2 -0
  118. package/dist/tools/codebase/ast/types.js.map +1 -0
  119. package/dist/tools/codebase/ast/wasm/tree-sitter-python.wasm +0 -0
  120. package/dist/tools/codebase/ast/wasm/tree-sitter-tsx.wasm +0 -0
  121. package/dist/tools/codebase/ast/wasm/tree-sitter-typescript.wasm +0 -0
  122. package/dist/tools/codebase/ast/wasm/tree-sitter.wasm +0 -0
  123. package/dist/tools/codebase/ast/wasm/web-tree-sitter.wasm +0 -0
  124. package/dist/tools/codebase/find.d.ts +3 -0
  125. package/dist/tools/codebase/find.d.ts.map +1 -0
  126. package/dist/tools/codebase/find.js +61 -0
  127. package/dist/tools/codebase/find.js.map +1 -0
  128. package/dist/tools/codebase/glob.d.ts +3 -0
  129. package/dist/tools/codebase/glob.d.ts.map +1 -0
  130. package/dist/tools/codebase/glob.js +37 -0
  131. package/dist/tools/codebase/glob.js.map +1 -0
  132. package/dist/tools/codebase/grep.d.ts +3 -0
  133. package/dist/tools/codebase/grep.d.ts.map +1 -0
  134. package/dist/tools/codebase/grep.js +169 -0
  135. package/dist/tools/codebase/grep.js.map +1 -0
  136. package/dist/tools/codebase/ls.d.ts +3 -0
  137. package/dist/tools/codebase/ls.d.ts.map +1 -0
  138. package/dist/tools/codebase/ls.js +39 -0
  139. package/dist/tools/codebase/ls.js.map +1 -0
  140. package/dist/tools/codebase/read.d.ts +10 -0
  141. package/dist/tools/codebase/read.d.ts.map +1 -0
  142. package/dist/tools/codebase/read.js +81 -0
  143. package/dist/tools/codebase/read.js.map +1 -0
  144. package/dist/tools/delete_memory.d.ts +3 -0
  145. package/dist/tools/delete_memory.d.ts.map +1 -0
  146. package/dist/tools/delete_memory.js +46 -0
  147. package/dist/tools/delete_memory.js.map +1 -0
  148. package/dist/tools/edit/diff-compute.d.ts +4 -0
  149. package/dist/tools/edit/diff-compute.d.ts.map +1 -0
  150. package/dist/tools/edit/diff-compute.js +18 -0
  151. package/dist/tools/edit/diff-compute.js.map +1 -0
  152. package/dist/tools/edit/diff-render.d.ts +18 -0
  153. package/dist/tools/edit/diff-render.d.ts.map +1 -0
  154. package/dist/tools/edit/diff-render.js +128 -0
  155. package/dist/tools/edit/diff-render.js.map +1 -0
  156. package/dist/tools/edit/edit.d.ts +4 -0
  157. package/dist/tools/edit/edit.d.ts.map +1 -0
  158. package/dist/tools/edit/edit.js +440 -0
  159. package/dist/tools/edit/edit.js.map +1 -0
  160. package/dist/tools/edit/matcher.d.ts +24 -0
  161. package/dist/tools/edit/matcher.d.ts.map +1 -0
  162. package/dist/tools/edit/matcher.js +66 -0
  163. package/dist/tools/edit/matcher.js.map +1 -0
  164. package/dist/tools/edit/parser.d.ts +10 -0
  165. package/dist/tools/edit/parser.d.ts.map +1 -0
  166. package/dist/tools/edit/parser.js +120 -0
  167. package/dist/tools/edit/parser.js.map +1 -0
  168. package/dist/tools/edit/types.d.ts +16 -0
  169. package/dist/tools/edit/types.d.ts.map +1 -0
  170. package/dist/tools/edit/types.js +2 -0
  171. package/dist/tools/edit/types.js.map +1 -0
  172. package/dist/tools/edit/write_plan.d.ts +5 -0
  173. package/dist/tools/edit/write_plan.d.ts.map +1 -0
  174. package/dist/tools/edit/write_plan.js +98 -0
  175. package/dist/tools/edit/write_plan.js.map +1 -0
  176. package/dist/tools/edit.d.ts.map +1 -1
  177. package/dist/tools/edit.js +66 -1
  178. package/dist/tools/edit.js.map +1 -1
  179. package/dist/tools/execute/bash/constants.d.ts +24 -0
  180. package/dist/tools/execute/bash/constants.d.ts.map +1 -0
  181. package/dist/tools/execute/bash/constants.js +88 -0
  182. package/dist/tools/execute/bash/constants.js.map +1 -0
  183. package/dist/tools/execute/bash/errors.d.ts +39 -0
  184. package/dist/tools/execute/bash/errors.d.ts.map +1 -0
  185. package/dist/tools/execute/bash/errors.js +249 -0
  186. package/dist/tools/execute/bash/errors.js.map +1 -0
  187. package/dist/tools/execute/bash/execution.d.ts +18 -0
  188. package/dist/tools/execute/bash/execution.d.ts.map +1 -0
  189. package/dist/tools/execute/bash/execution.js +207 -0
  190. package/dist/tools/execute/bash/execution.js.map +1 -0
  191. package/dist/tools/execute/bash/index.d.ts +3 -0
  192. package/dist/tools/execute/bash/index.d.ts.map +1 -0
  193. package/dist/tools/execute/bash/index.js +117 -0
  194. package/dist/tools/execute/bash/index.js.map +1 -0
  195. package/dist/tools/execute/bash/platform.d.ts +6 -0
  196. package/dist/tools/execute/bash/platform.d.ts.map +1 -0
  197. package/dist/tools/execute/bash/platform.js +64 -0
  198. package/dist/tools/execute/bash/platform.js.map +1 -0
  199. package/dist/tools/execute/bash/security.d.ts +12 -0
  200. package/dist/tools/execute/bash/security.d.ts.map +1 -0
  201. package/dist/tools/execute/bash/security.js +26 -0
  202. package/dist/tools/execute/bash/security.js.map +1 -0
  203. package/dist/tools/execute/bash/validation.d.ts +16 -0
  204. package/dist/tools/execute/bash/validation.d.ts.map +1 -0
  205. package/dist/tools/execute/bash/validation.js +57 -0
  206. package/dist/tools/execute/bash/validation.js.map +1 -0
  207. package/dist/tools/execute/bash.d.ts +3 -0
  208. package/dist/tools/execute/bash.d.ts.map +1 -0
  209. package/dist/tools/execute/bash.js +728 -0
  210. package/dist/tools/execute/bash.js.map +1 -0
  211. package/dist/tools/execute/git.d.ts +3 -0
  212. package/dist/tools/execute/git.d.ts.map +1 -0
  213. package/dist/tools/execute/git.js +183 -0
  214. package/dist/tools/execute/git.js.map +1 -0
  215. package/dist/tools/execute/task_complete.d.ts +7 -0
  216. package/dist/tools/execute/task_complete.d.ts.map +1 -0
  217. package/dist/tools/execute/task_complete.js +38 -0
  218. package/dist/tools/execute/task_complete.js.map +1 -0
  219. package/dist/tools/fetch.d.ts.map +1 -1
  220. package/dist/tools/fetch.js +2 -0
  221. package/dist/tools/fetch.js.map +1 -1
  222. package/dist/tools/find.d.ts.map +1 -1
  223. package/dist/tools/find.js +2 -0
  224. package/dist/tools/find.js.map +1 -1
  225. package/dist/tools/git.d.ts.map +1 -1
  226. package/dist/tools/git.js +2 -0
  227. package/dist/tools/git.js.map +1 -1
  228. package/dist/tools/glob.d.ts.map +1 -1
  229. package/dist/tools/glob.js +2 -0
  230. package/dist/tools/glob.js.map +1 -1
  231. package/dist/tools/grep.d.ts.map +1 -1
  232. package/dist/tools/grep.js +2 -0
  233. package/dist/tools/grep.js.map +1 -1
  234. package/dist/tools/install/clawhub.d.ts +26 -0
  235. package/dist/tools/install/clawhub.d.ts.map +1 -0
  236. package/dist/tools/install/clawhub.js +182 -0
  237. package/dist/tools/install/clawhub.js.map +1 -0
  238. package/dist/tools/install/constants.d.ts +5 -0
  239. package/dist/tools/install/constants.d.ts.map +1 -0
  240. package/dist/tools/install/constants.js +5 -0
  241. package/dist/tools/install/constants.js.map +1 -0
  242. package/dist/tools/install/install_skill.d.ts +3 -0
  243. package/dist/tools/install/install_skill.d.ts.map +1 -0
  244. package/dist/tools/install/install_skill.js +132 -0
  245. package/dist/tools/install/install_skill.js.map +1 -0
  246. package/dist/tools/install/npm.d.ts +8 -0
  247. package/dist/tools/install/npm.d.ts.map +1 -0
  248. package/dist/tools/install/npm.js +52 -0
  249. package/dist/tools/install/npm.js.map +1 -0
  250. package/dist/tools/install/skill-content.d.ts +9 -0
  251. package/dist/tools/install/skill-content.d.ts.map +1 -0
  252. package/dist/tools/install/skill-content.js +89 -0
  253. package/dist/tools/install/skill-content.js.map +1 -0
  254. package/dist/tools/install_skill.d.ts.map +1 -1
  255. package/dist/tools/install_skill.js +2 -0
  256. package/dist/tools/install_skill.js.map +1 -1
  257. package/dist/tools/list_memory.d.ts +3 -0
  258. package/dist/tools/list_memory.d.ts.map +1 -0
  259. package/dist/tools/list_memory.js +55 -0
  260. package/dist/tools/list_memory.js.map +1 -0
  261. package/dist/tools/ls.d.ts.map +1 -1
  262. package/dist/tools/ls.js +2 -0
  263. package/dist/tools/ls.js.map +1 -1
  264. package/dist/tools/memory/delete_memory.d.ts +3 -0
  265. package/dist/tools/memory/delete_memory.d.ts.map +1 -0
  266. package/dist/tools/memory/delete_memory.js +46 -0
  267. package/dist/tools/memory/delete_memory.js.map +1 -0
  268. package/dist/tools/memory/list_memory.d.ts +3 -0
  269. package/dist/tools/memory/list_memory.d.ts.map +1 -0
  270. package/dist/tools/memory/list_memory.js +55 -0
  271. package/dist/tools/memory/list_memory.js.map +1 -0
  272. package/dist/tools/memory/save_memory.d.ts +3 -0
  273. package/dist/tools/memory/save_memory.d.ts.map +1 -0
  274. package/dist/tools/memory/save_memory.js +45 -0
  275. package/dist/tools/memory/save_memory.js.map +1 -0
  276. package/dist/tools/read.d.ts.map +1 -1
  277. package/dist/tools/read.js +2 -0
  278. package/dist/tools/read.js.map +1 -1
  279. package/dist/tools/registry.d.ts +7 -2
  280. package/dist/tools/registry.d.ts.map +1 -1
  281. package/dist/tools/registry.js +112 -17
  282. package/dist/tools/registry.js.map +1 -1
  283. package/dist/tools/save_memory.d.ts +3 -0
  284. package/dist/tools/save_memory.d.ts.map +1 -0
  285. package/dist/tools/save_memory.js +45 -0
  286. package/dist/tools/save_memory.js.map +1 -0
  287. package/dist/tools/search.d.ts.map +1 -1
  288. package/dist/tools/search.js +2 -0
  289. package/dist/tools/search.js.map +1 -1
  290. package/dist/tools/tree.d.ts +12 -0
  291. package/dist/tools/tree.d.ts.map +1 -0
  292. package/dist/tools/tree.js +87 -0
  293. package/dist/tools/tree.js.map +1 -0
  294. package/dist/tools/types.d.ts +12 -0
  295. package/dist/tools/types.d.ts.map +1 -1
  296. package/dist/tools/web/fetch.d.ts +3 -0
  297. package/dist/tools/web/fetch.d.ts.map +1 -0
  298. package/dist/tools/web/fetch.js +49 -0
  299. package/dist/tools/web/fetch.js.map +1 -0
  300. package/dist/tools/web/search.d.ts +3 -0
  301. package/dist/tools/web/search.d.ts.map +1 -0
  302. package/dist/tools/web/search.js +134 -0
  303. package/dist/tools/web/search.js.map +1 -0
  304. package/dist/tools/write_plan.d.ts.map +1 -1
  305. package/dist/tools/write_plan.js +2 -0
  306. package/dist/tools/write_plan.js.map +1 -1
  307. package/dist/tui/commands.d.ts +4 -0
  308. package/dist/tui/commands.d.ts.map +1 -0
  309. package/dist/tui/commands.js +118 -0
  310. package/dist/tui/commands.js.map +1 -0
  311. package/dist/tui/components/colors.d.ts +36 -0
  312. package/dist/tui/components/colors.d.ts.map +1 -0
  313. package/dist/tui/components/colors.js +48 -0
  314. package/dist/tui/components/colors.js.map +1 -0
  315. package/dist/tui/components/header-bar.d.ts +26 -0
  316. package/dist/tui/components/header-bar.d.ts.map +1 -0
  317. package/dist/tui/components/header-bar.js +78 -0
  318. package/dist/tui/components/header-bar.js.map +1 -0
  319. package/dist/tui/components/index.d.ts +9 -0
  320. package/dist/tui/components/index.d.ts.map +1 -0
  321. package/dist/tui/components/index.js +9 -0
  322. package/dist/tui/components/index.js.map +1 -0
  323. package/dist/tui/components/landing-screen.d.ts +12 -0
  324. package/dist/tui/components/landing-screen.d.ts.map +1 -0
  325. package/dist/tui/components/landing-screen.js +53 -0
  326. package/dist/tui/components/landing-screen.js.map +1 -0
  327. package/dist/tui/components/pixel-logo.d.ts +5 -0
  328. package/dist/tui/components/pixel-logo.d.ts.map +1 -0
  329. package/dist/tui/components/pixel-logo.js +27 -0
  330. package/dist/tui/components/pixel-logo.js.map +1 -0
  331. package/dist/tui/components/plan-utils.d.ts +11 -0
  332. package/dist/tui/components/plan-utils.d.ts.map +1 -0
  333. package/dist/tui/components/plan-utils.js +62 -0
  334. package/dist/tui/components/plan-utils.js.map +1 -0
  335. package/dist/tui/components/plans-list.d.ts +18 -0
  336. package/dist/tui/components/plans-list.d.ts.map +1 -0
  337. package/dist/tui/components/plans-list.js +46 -0
  338. package/dist/tui/components/plans-list.js.map +1 -0
  339. package/dist/tui/components/rich-footer.d.ts +29 -0
  340. package/dist/tui/components/rich-footer.d.ts.map +1 -0
  341. package/dist/tui/components/rich-footer.js +124 -0
  342. package/dist/tui/components/rich-footer.js.map +1 -0
  343. package/dist/tui/components/todo-panel.d.ts +14 -0
  344. package/dist/tui/components/todo-panel.d.ts.map +1 -0
  345. package/dist/tui/components/todo-panel.js +83 -0
  346. package/dist/tui/components/todo-panel.js.map +1 -0
  347. package/dist/tui/components.d.ts.map +1 -1
  348. package/dist/tui/components.js +14 -7
  349. package/dist/tui/components.js.map +1 -1
  350. package/dist/tui/index.d.ts +2 -1
  351. package/dist/tui/index.d.ts.map +1 -1
  352. package/dist/tui/index.js +99 -505
  353. package/dist/tui/index.js.map +1 -1
  354. package/dist/tui/overlays.d.ts +28 -0
  355. package/dist/tui/overlays.d.ts.map +1 -0
  356. package/dist/tui/overlays.js +154 -0
  357. package/dist/tui/overlays.js.map +1 -0
  358. package/dist/tui/themes.d.ts +5 -0
  359. package/dist/tui/themes.d.ts.map +1 -0
  360. package/dist/tui/themes.js +38 -0
  361. package/dist/tui/themes.js.map +1 -0
  362. package/dist/web/index.d.ts.map +1 -1
  363. package/dist/web/index.js +60 -122
  364. package/dist/web/index.js.map +1 -1
  365. package/dist/web/public/index.html +1 -1
  366. package/dist/web/public/messages.js +41 -25
  367. package/dist/web/public/state.js +72 -72
  368. package/dist/web/public/style.css +1126 -996
  369. package/dist/web/public/websocket.js +267 -267
  370. package/dist/web/session-bridge.d.ts.map +1 -1
  371. package/dist/web/session-bridge.js +82 -3
  372. package/dist/web/session-bridge.js.map +1 -1
  373. package/docs/ast-tools.md +117 -0
  374. package/package.json +60 -55
  375. package/scripts/check-line-count.mjs +53 -0
  376. package/scripts/copy-ast-wasm.mjs +32 -0
  377. package/src/agent/__tests__/session-restore.test.ts +284 -0
  378. package/src/agent/commands.ts +274 -0
  379. package/src/agent/compaction.ts +9 -2
  380. package/src/agent/index.ts +30 -25
  381. package/src/agent/memory.ts +60 -0
  382. package/src/agent/prompt-builder.ts +100 -26
  383. package/src/agent/providers/anthropic.ts +179 -179
  384. package/src/agent/providers/google.ts +210 -210
  385. package/src/agent/providers/ollama.ts +186 -186
  386. package/src/agent/providers/openai.ts +18 -3
  387. package/src/agent/session-chat.ts +480 -0
  388. package/src/agent/session-display.ts +195 -0
  389. package/src/agent/session-persistence.ts +146 -0
  390. package/src/agent/session-utils.ts +54 -0
  391. package/src/agent/session.ts +261 -718
  392. package/src/cli/index.ts +73 -4
  393. package/src/config/index.ts +342 -342
  394. package/src/index.ts +30 -3
  395. package/src/tools/__tests__/bash.test.ts +187 -1
  396. package/src/tools/__tests__/edit.test.ts +275 -11
  397. package/src/tools/__tests__/fetch.test.ts +1 -1
  398. package/src/tools/__tests__/glob.test.ts +1 -1
  399. package/src/tools/__tests__/grep.test.ts +1 -1
  400. package/src/tools/__tests__/ls.test.ts +1 -1
  401. package/src/tools/__tests__/read.test.ts +1 -1
  402. package/src/tools/__tests__/registry.test.ts +332 -0
  403. package/src/tools/__tests__/write_plan.test.ts +1 -1
  404. package/src/tools/codebase/ast/__tests__/integration.test.ts +452 -0
  405. package/src/tools/codebase/ast/__tests__/tree-sitter.test.ts +466 -0
  406. package/src/tools/codebase/ast/adapter.ts +44 -0
  407. package/src/tools/codebase/ast/tools.ts +322 -0
  408. package/src/tools/codebase/ast/tree-sitter-adapter.ts +468 -0
  409. package/src/tools/codebase/ast/tree-sitter-converters.ts +480 -0
  410. package/src/tools/codebase/ast/types.ts +102 -0
  411. package/src/tools/codebase/ast/wasm/tree-sitter-python.wasm +0 -0
  412. package/src/tools/codebase/ast/wasm/tree-sitter-tsx.wasm +0 -0
  413. package/src/tools/codebase/ast/wasm/tree-sitter-typescript.wasm +0 -0
  414. package/src/tools/codebase/ast/wasm/tree-sitter.wasm +0 -0
  415. package/src/tools/codebase/ast/wasm/web-tree-sitter.wasm +0 -0
  416. package/src/tools/{find.ts → codebase/find.ts} +4 -2
  417. package/src/tools/{glob.ts → codebase/glob.ts} +4 -2
  418. package/src/tools/{grep.ts → codebase/grep.ts} +4 -2
  419. package/src/tools/{ls.ts → codebase/ls.ts} +4 -2
  420. package/src/tools/{read.ts → codebase/read.ts} +5 -3
  421. package/src/tools/edit/diff-compute.ts +21 -0
  422. package/src/tools/edit/diff-render.ts +145 -0
  423. package/src/tools/edit/edit.ts +468 -0
  424. package/src/tools/edit/matcher.ts +70 -0
  425. package/src/tools/edit/parser.ts +131 -0
  426. package/src/tools/edit/types.ts +19 -0
  427. package/src/tools/{write_plan.ts → edit/write_plan.ts} +4 -2
  428. package/src/tools/execute/bash/constants.ts +93 -0
  429. package/src/tools/execute/bash/errors.ts +304 -0
  430. package/src/tools/execute/bash/execution.ts +211 -0
  431. package/src/tools/execute/bash/index.ts +131 -0
  432. package/src/tools/execute/bash/platform.ts +65 -0
  433. package/src/tools/execute/bash/security.ts +28 -0
  434. package/src/tools/execute/bash/validation.ts +78 -0
  435. package/src/tools/execute/git.ts +209 -0
  436. package/src/tools/execute/task_complete.ts +40 -0
  437. package/src/tools/install/clawhub.ts +222 -0
  438. package/src/tools/install/constants.ts +4 -0
  439. package/src/tools/install/install_skill.ts +150 -0
  440. package/src/tools/install/npm.ts +60 -0
  441. package/src/tools/install/skill-content.ts +108 -0
  442. package/src/tools/memory/delete_memory.ts +46 -0
  443. package/src/tools/memory/list_memory.ts +53 -0
  444. package/src/tools/memory/save_memory.ts +45 -0
  445. package/src/tools/registry.ts +119 -19
  446. package/src/tools/tree.ts +103 -0
  447. package/src/tools/types.ts +13 -0
  448. package/src/tools/{fetch.ts → web/fetch.ts} +4 -2
  449. package/src/tools/{search.ts → web/search.ts} +4 -2
  450. package/src/tui/commands.ts +135 -0
  451. package/src/tui/components/colors.ts +52 -0
  452. package/src/tui/components/header-bar.ts +88 -0
  453. package/src/tui/components/index.ts +8 -0
  454. package/src/tui/components/landing-screen.ts +67 -0
  455. package/src/tui/components/pixel-logo.ts +33 -0
  456. package/src/tui/components/plan-utils.ts +69 -0
  457. package/src/tui/components/plans-list.ts +55 -0
  458. package/src/tui/components/rich-footer.ts +146 -0
  459. package/src/tui/components/todo-panel.ts +95 -0
  460. package/src/tui/index.ts +335 -786
  461. package/src/tui/overlays.ts +203 -0
  462. package/src/tui/themes.ts +41 -0
  463. package/src/web/index.ts +64 -127
  464. package/src/web/public/index.html +1 -1
  465. package/src/web/public/messages.js +41 -25
  466. package/src/web/public/state.js +72 -72
  467. package/src/web/public/style.css +1126 -996
  468. package/src/web/public/websocket.js +267 -267
  469. package/src/web/session-bridge.ts +272 -193
  470. package/AGENT.md +0 -99
  471. package/src/tools/bash.ts +0 -125
  472. package/src/tools/edit.ts +0 -747
  473. package/src/tools/git.ts +0 -76
  474. package/src/tools/install_skill.ts +0 -539
  475. package/src/tui/components.ts +0 -635
package/src/tools/edit.ts DELETED
@@ -1,747 +0,0 @@
1
- import * as fs from 'node:fs'
2
- import * as path from 'node:path'
3
- import { DIFF_DELETE, DIFF_INSERT, diffLinesRaw } from 'jest-diff'
4
- import type { FileReadTracker } from '../diff/apply.js'
5
- import { fmtErr } from './errors.js'
6
- import type { Tool, ToolResult } from './types.js'
7
-
8
- // ── Diff types ────────────────────────────────────────────────────────────
9
- export type DiffLineType = 'delete' | 'insert' | 'equal'
10
-
11
- export interface DiffLine {
12
- type: DiffLineType
13
- content: string
14
- }
15
-
16
- // ── ANSI colors for terminal output ───────────────────────────────────────
17
- const DIFF_RED = '\x1b[38;2;255;80;80m'
18
- const DIFF_GREEN = '\x1b[38;2;0;200;100m'
19
- const DIFF_DIM = '\x1b[38;2;100;100;100m'
20
- const DIFF_RESET = '\x1b[0m'
21
-
22
- // ── Colors for HTML output ────────────────────────────────────────────────
23
- const HTML_RED = '#ff5050'
24
- const HTML_GREEN = '#00c864'
25
- const HTML_DIM = '#888888'
26
-
27
- /** Build diagnostic JSON for error messages */
28
- function buildDiag(edit: SingleEdit): string {
29
- return JSON.stringify({
30
- file_path: edit.file_path,
31
- old_string: edit.old_string,
32
- new_string: edit.new_string,
33
- })
34
- }
35
-
36
- /** Summarize raw input for error messages to avoid dumping huge strings into the LLM context. */
37
- function summarizeRawInput(rawInput: unknown): string {
38
- const s = JSON.stringify(rawInput)
39
- if (s.length <= 500) return s
40
- return `${s.slice(0, 500)}... [truncated, total ${s.length} chars]`
41
- }
42
-
43
- /** Compute diff lines using jest-diff for proper line-level diffs */
44
- export function computeDiff(oldStr: string, newStr: string): DiffLine[] {
45
- const oldLines = oldStr === '' ? [] : oldStr.split('\n')
46
- const newLines = newStr === '' ? [] : newStr.split('\n')
47
-
48
- if (oldLines.length === 0 && newLines.length === 0) return []
49
-
50
- const rawDiff = diffLinesRaw(oldLines, newLines)
51
- return rawDiff.map(d => ({
52
- type:
53
- d[0] === DIFF_DELETE
54
- ? ('delete' as const)
55
- : d[0] === DIFF_INSERT
56
- ? ('insert' as const)
57
- : ('equal' as const),
58
- content: d[1],
59
- }))
60
- }
61
-
62
- /** Terminal renderer — unified-diff format with color */
63
- export function renderDiffTerminal(lines: DiffLine[], startLineNum?: number): string {
64
- if (lines.length === 0) return ''
65
-
66
- const output: string[] = []
67
- let oldLn = startLineNum ?? 1
68
- let newLn = startLineNum ?? 1
69
- for (const line of lines) {
70
- if (line.type === 'delete') {
71
- output.push(` ${DIFF_RED}- ${oldLn} ${line.content}${DIFF_RESET}`)
72
- oldLn++
73
- } else if (line.type === 'insert') {
74
- output.push(` ${DIFF_GREEN}+ ${newLn} ${line.content}${DIFF_RESET}`)
75
- newLn++
76
- } else {
77
- output.push(` ${DIFF_DIM} ${oldLn} ${line.content}${DIFF_RESET}`)
78
- oldLn++
79
- newLn++
80
- }
81
- }
82
- return output.join('\n')
83
- }
84
-
85
- /** HTML renderer for web output */
86
- export function renderDiffHtml(lines: DiffLine[]): string {
87
- if (lines.length === 0) return ''
88
-
89
- const output: string[] = []
90
- for (const line of lines) {
91
- if (line.type === 'delete') {
92
- output.push(` <span style="color: ${HTML_RED};">- ${escapeHtml(line.content)}</span>`)
93
- } else if (line.type === 'insert') {
94
- output.push(` <span style="color: ${HTML_GREEN};">+ ${escapeHtml(line.content)}</span>`)
95
- } else {
96
- output.push(` <span style="color: ${HTML_DIM};"> ${escapeHtml(line.content)}</span>`)
97
- }
98
- }
99
- return output.join('\n')
100
- }
101
-
102
- /** Escape HTML special characters */
103
- export function escapeHtml(str: string): string {
104
- return str
105
- .replace(/&/g, '&amp;')
106
- .replace(/</g, '&lt;')
107
- .replace(/>/g, '&gt;')
108
- .replace(/"/g, '&quot;')
109
- .replace(/'/g, '&#39;')
110
- }
111
-
112
- /**
113
- * Generate diff output using jest-diff.
114
- * Returns terminal-colored unified-diff format with line numbers.
115
- */
116
- export function generateDiff(oldStr: string, newStr: string, startLineNum?: number): string {
117
- const lines = computeDiff(oldStr, newStr)
118
- return renderDiffTerminal(lines, startLineNum)
119
- }
120
-
121
- /**
122
- * Generate diff with surrounding context lines (1 before, 1 after)
123
- * and line numbers. Context lines are extracted from the full file content.
124
- */
125
- export function generateDiffWithContext(
126
- fullContent: string,
127
- oldStr: string,
128
- newStr: string,
129
- matchIndex: number,
130
- matchLength: number,
131
- ): string {
132
- // Calculate 1-based line number of the first matched line
133
- const firstLineNum = fullContent.slice(0, matchIndex).split('\n').length
134
-
135
- const contentLines = fullContent.split('\n')
136
-
137
- // Find 0-based line index where matchIndex falls
138
- let charPos = 0
139
- let matchStartLineIdx = 0
140
- for (let i = 0; i < contentLines.length; i++) {
141
- if (charPos <= matchIndex && matchIndex < charPos + contentLines[i].length + 1) {
142
- matchStartLineIdx = i
143
- break
144
- }
145
- charPos += contentLines[i].length + 1 // +1 for \n
146
- }
147
-
148
- // How many lines does oldStr span in the original content?
149
- const oldLines = oldStr === '' ? [] : oldStr.split('\n')
150
- const oldLineCount = oldLines.length
151
- const matchEndLineIdx = matchStartLineIdx + oldLineCount - 1
152
-
153
- // Context before (1 line) and after (1 line)
154
- const beforeLineIdx = matchStartLineIdx > 0 ? matchStartLineIdx - 1 : -1
155
- const afterLineIdx = matchEndLineIdx < contentLines.length - 1 ? matchEndLineIdx + 1 : -1
156
-
157
- // Compute diff between oldStr and newStr
158
- const diffLines = computeDiff(oldStr, newStr)
159
-
160
- const output: string[] = []
161
-
162
- // Context before
163
- if (beforeLineIdx !== -1) {
164
- const lineNum = beforeLineIdx + 1
165
- output.push(` ${DIFF_DIM} ${lineNum} ${contentLines[beforeLineIdx]}${DIFF_RESET}`)
166
- }
167
-
168
- // Diff lines with line numbers
169
- let oldLn = firstLineNum
170
- let newLn = firstLineNum
171
- for (const line of diffLines) {
172
- if (line.type === 'delete') {
173
- output.push(` ${DIFF_RED}- ${oldLn} ${line.content}${DIFF_RESET}`)
174
- oldLn++
175
- } else if (line.type === 'insert') {
176
- output.push(` ${DIFF_GREEN}+ ${newLn} ${line.content}${DIFF_RESET}`)
177
- newLn++
178
- } else {
179
- output.push(` ${DIFF_DIM} ${oldLn} ${line.content}${DIFF_RESET}`)
180
- oldLn++
181
- newLn++
182
- }
183
- }
184
-
185
- // Context after
186
- if (afterLineIdx !== -1) {
187
- const lineNum = afterLineIdx + 1
188
- output.push(` ${DIFF_DIM} ${lineNum} ${contentLines[afterLineIdx]}${DIFF_RESET}`)
189
- }
190
-
191
- return output.join('\n')
192
- }
193
-
194
- /**
195
- * Normalize a line for whitespace-tolerant comparison:
196
- * 1. Trim leading/trailing whitespace
197
- * 2. Collapse runs of 2+ spaces/tabs into a single space
198
- *
199
- * This handles whitespace differences ANYWHERE on the line:
200
- * - Leading/trailing spaces → `trim()` removes them
201
- * - Extra internal spaces → `"foo bar"` → `"foo bar"`
202
- * - Blank lines with spaces → `" "` → `""`
203
- */
204
- /** Export for testing */
205
- export function normalizeLine(s: string): string {
206
- return s.trim().replace(/[ \t]{2,}/g, ' ')
207
- }
208
-
209
- interface MatchPos {
210
- index: number
211
- length: number
212
- }
213
-
214
- /**
215
- * Sliding-window line search that ignores whitespace differences
216
- * (leading, trailing, and internal runs).
217
- *
218
- * Returns all match positions in the ORIGINAL (unnormalized) content,
219
- * so the caller can do content.slice(match.index, match.index + match.length)
220
- * to extract the actual matched text (with its original whitespace).
221
- */
222
- /** Export for testing */
223
- export function findAllLinesTolerant(content: string, oldString: string): MatchPos[] {
224
- if (oldString === '') return []
225
-
226
- const contentLines = content.split('\n')
227
- const oldLines = oldString.split('\n')
228
-
229
- if (oldLines.length > contentLines.length) return []
230
-
231
- // Pre-normalize for speed
232
- const normContent = contentLines.map(normalizeLine)
233
- const normOld = oldLines.map(normalizeLine)
234
-
235
- const matches: MatchPos[] = []
236
-
237
- for (let start = 0; start <= normContent.length - normOld.length; start++) {
238
- let match = true
239
- for (let j = 0; j < normOld.length; j++) {
240
- if (normContent[start + j] !== normOld[j]) {
241
- match = false
242
- break
243
- }
244
- }
245
- if (match) {
246
- // Compute byte position in ORIGINAL (unnormalized) content
247
- let charPos = 0
248
- for (let k = 0; k < start; k++) {
249
- charPos += contentLines[k]!.length + 1 // +1 for the \n
250
- }
251
- // Compute matched text length in ORIGINAL content
252
- let matchedLen = 0
253
- for (let j = 0; j < oldLines.length; j++) {
254
- matchedLen += contentLines[start + j]!.length
255
- if (j < oldLines.length - 1) matchedLen += 1 // +1 for the \n
256
- }
257
- matches.push({ index: charPos, length: matchedLen })
258
- }
259
- }
260
-
261
- return matches
262
- }
263
-
264
- interface SingleEdit {
265
- file_path: string
266
- old_string: string
267
- new_string: string
268
- }
269
-
270
- type Edit = SingleEdit
271
-
272
- /** Export for testing */
273
- export function parseMarkdownEdit(content: string): Edit[] {
274
- const edits: Edit[] = []
275
-
276
- function parseEditBlock(raw: string): Edit | null {
277
- // Remove stray ``` lines (model may close the block early before new:)
278
- const cleaned = raw.replace(/^```\s*$/gm, '')
279
-
280
- const fileMatch = cleaned.match(/^file:\s*(.+)$/m)
281
- if (!fileMatch) return null
282
- const filePath = fileMatch[1]!.trim()
283
-
284
- let oldString = ''
285
- let newString = ''
286
-
287
- const oldMatch = cleaned.match(/^old:(?:\s*\|\d*\s*\n)?([\s\S]*?)^new:/m)
288
- const newMatch = cleaned.match(/^new:(?:\s*\|\d*\s*\n)?([\s\S]*)$/m)
289
-
290
- if (oldMatch) {
291
- oldString = oldMatch[1]!.replace(/^\n+/, '').replace(/\n+$/, '')
292
- }
293
- if (newMatch) {
294
- newString = newMatch[1]!.replace(/^\n+/, '').replace(/\n+$/, '')
295
- }
296
-
297
- return { file_path: filePath, old_string: oldString || '', new_string: newString || '' }
298
- }
299
-
300
- // Strategy 1: Non-greedy block regex (handles multi-edit, correct formatting)
301
- const blockRegex = /```edit\s*([\s\S]*?)```/gi
302
- for (const regexMatch of content.matchAll(blockRegex)) {
303
- const edit = parseEditBlock(regexMatch[1]!)
304
- if (edit) edits.push(edit)
305
- }
306
-
307
- // Strategy 2: If no edit with new_string found (model likely closed ``` before new:),
308
- // retry with greedy matching to capture everything up to the last ```
309
- if (!edits.some(e => e.new_string)) {
310
- edits.length = 0
311
- const greedyRegex = /```edit\s*([\s\S]*)```/g
312
- for (const regexMatch of content.matchAll(greedyRegex)) {
313
- const edit = parseEditBlock(regexMatch[1]!)
314
- if (edit) edits.push(edit)
315
- }
316
- }
317
-
318
- // Strategy 3: No block markers at all — try parsing raw content directly
319
- if (edits.length === 0) {
320
- const edit = parseEditBlock(content)
321
- if (edit) edits.push(edit)
322
- }
323
-
324
- return edits
325
- }
326
-
327
- /** Extract edits from legacy JSON format (backward compatibility) */
328
- function extractEditsFromJSON(input: Record<string, unknown>): Edit[] {
329
- // Pattern 0: input is an array (edits passed directly instead of wrapped)
330
- if (Array.isArray(input)) {
331
- // Preserve array for validation
332
- return input as Edit[]
333
- }
334
-
335
- // Pattern 1: input has file_path, old_string, new_string at top level (missing edits array)
336
- if (!Array.isArray(input.edits)) {
337
- const keys = Object.keys(input)
338
-
339
- // Check if the keys look like a single edit object (file_path + old_string + new_string)
340
- const hasFilePath = typeof input.file_path === 'string'
341
- const hasOldString = typeof input.old_string === 'string'
342
- const hasNewString = typeof input.new_string === 'string'
343
-
344
- if (hasFilePath && hasOldString && hasNewString) {
345
- return [
346
- {
347
- file_path: input.file_path as string,
348
- old_string: input.old_string as string,
349
- new_string: input.new_string as string,
350
- },
351
- ]
352
- } else if (hasFilePath && hasOldString) {
353
- // Only file_path + old_string (missing new_string) — use a sentinel
354
- // value so validation catches this as an error instead of silently deleting content.
355
- return [
356
- {
357
- file_path: input.file_path as string,
358
- old_string: input.old_string as string,
359
- new_string: (input.new_string as string) || '__MISSING_NEW_STRING__',
360
- },
361
- ]
362
- } else if (keys.length === 2 && hasFilePath && typeof input.new_string === 'string') {
363
- // file_path + new_string but no old_string — treat as new file creation
364
- return [
365
- {
366
- file_path: input.file_path as string,
367
- old_string: '',
368
- new_string: input.new_string as string,
369
- },
370
- ]
371
- } else if (keys.length === 1 && hasFilePath) {
372
- // Only file_path — maybe they meant create file with empty content?
373
- return [{ file_path: input.file_path as string, old_string: '', new_string: '' }]
374
- }
375
- return []
376
- }
377
-
378
- // If edits is an array (even empty), preserve for validation
379
- if (Array.isArray(input.edits)) {
380
- return input.edits as Edit[]
381
- }
382
-
383
- return []
384
- }
385
-
386
- export function createEditTool(applier: FileReadTracker, cwd: string): Tool {
387
- return {
388
- definition: {
389
- name: 'edit',
390
- description: `Replace exact text in files using markdown code block format.
391
-
392
- HOW TO USE:
393
- 1. Read the file first with \`read\`
394
- 2. Copy the EXACT text to replace — include 2-3 lines of context before/after
395
- 3. Use markdown code block format below
396
-
397
- FORMAT:
398
- \`\`\`edit
399
- file: <file_path>
400
- old: |
401
- <exact text to find>
402
- new: |
403
- <replacement text>
404
- \`\`\`
405
-
406
- EXAMPLES:
407
- Single edit:
408
- \`\`\`edit
409
- file: src/config.ts
410
- old: |
411
- mode: 'code'
412
- new: |
413
- mode: 'plan'
414
- \`\`\`
415
-
416
- Create new file:
417
- \`\`\`edit
418
- file: src/new.ts
419
- old: |
420
- new: |
421
- const x = 1
422
- \`\`\`
423
-
424
- CRITICAL RULES:
425
- - old and new are separated by "old:" and "new:" labels
426
- - Use | after label for multi-line content
427
- - old_string must match EXACTLY (whitespace, indentation, line breaks)
428
- - Do NOT include the "<lineNumber>: " prefix from read output`,
429
- parameters: {
430
- content: {
431
- type: 'string',
432
- description: 'Markdown code block with edit instructions. See description for format.',
433
- required: true,
434
- },
435
- },
436
- },
437
- async execute(input): Promise<ToolResult> {
438
- // Debug: keep rawInput for error messages (capture early for all error paths)
439
- const rawInput = input
440
-
441
- // ── Parse markdown format ─────────────────────────────────────────
442
- let edits: Edit[] = []
443
-
444
- // If input has 'content' field (new markdown format)
445
- if (typeof input.content === 'string') {
446
- // Compat: handle double-JSON-wrapped content
447
- let contentStr = input.content
448
- try {
449
- const parsed = JSON.parse(contentStr)
450
- if (typeof parsed === 'string') {
451
- contentStr = parsed
452
- } else if (parsed && typeof parsed === 'object' && typeof parsed.content === 'string') {
453
- contentStr = parsed.content
454
- }
455
- } catch {
456
- // Not JSON — use as-is
457
- }
458
- edits = parseMarkdownEdit(contentStr)
459
- if (edits.length === 0) {
460
- return {
461
- success: false,
462
- output: '',
463
- error: `Failed to parse edit format. Raw input: ${summarizeRawInput(rawInput)}\nUse: \`\`\`edit\\nfile: path\\nold: |\\ntext\\nnew: |\\ntext\\n\`\`\``,
464
- }
465
- }
466
- } else {
467
- // Legacy JSON format (backward compatibility)
468
- edits = extractEditsFromJSON(input as Record<string, unknown>)
469
- }
470
-
471
- if (edits.length === 0) {
472
- // Check if input specifically had empty edits array (for better error message)
473
- const inputEdits = (input as Record<string, unknown>).edits
474
- if ('edits' in input && Array.isArray(inputEdits) && inputEdits.length === 0) {
475
- return {
476
- success: false,
477
- output: '',
478
- error: `edit FAILED — no edits to apply. The edits array exists but is empty. Raw input: ${summarizeRawInput(rawInput)}`,
479
- }
480
- }
481
- // Check if input has edits key but it's not an array
482
- if ('edits' in input && !Array.isArray((input as Record<string, unknown>).edits)) {
483
- return {
484
- success: false,
485
- output: '',
486
- error: `edit requires "edits" array. Use markdown format: \`\`\`edit\\nfile: path\\nold: |\\ntext\\nnew: |\\ntext\\n\`\`\`\nRaw input: ${summarizeRawInput(rawInput)}`,
487
- }
488
- }
489
- return {
490
- success: false,
491
- output: '',
492
- error: `No valid edits found (empty or invalid format). The edit array must contain objects with file_path, old_string, and new_string. Use markdown code block format: \`\`\`edit\\nfile: path\\nold: |\\ntext\\nnew: |\\ntext\\n\`\`\`\nRaw input: ${summarizeRawInput(rawInput)}`,
493
- }
494
- }
495
-
496
- // Validate each edit object
497
- const editErrors: string[] = []
498
- for (let i = 0; i < edits.length; i++) {
499
- const e = edits[i]
500
- const missing: string[] = []
501
- if (typeof e.file_path !== 'string' || !e.file_path) missing.push('file_path')
502
- if (typeof e.old_string !== 'string') missing.push('old_string')
503
- if (typeof e.new_string !== 'string') missing.push('new_string')
504
- if (e.new_string === '__MISSING_NEW_STRING__') {
505
- missing.push('new_string (required — LLM must provide a non-empty replacement)')
506
- }
507
- if (missing.length > 0) {
508
- const present = Object.keys(e)
509
- .filter(k => typeof e[k as keyof Edit] === 'string')
510
- .join(', ')
511
- editErrors.push(
512
- ` edit #${i + 1}: missing ${missing.join(', ')}${present ? ` (has: ${present})` : ''}`,
513
- )
514
- }
515
- }
516
- if (editErrors.length > 0) {
517
- return {
518
- success: false,
519
- output: '',
520
- error: `edit FAILED — ${editErrors.length} of ${edits.length} edit(s) have missing fields.\n${editErrors.join('\n')}\n\nReceived: ${summarizeRawInput(rawInput)}\n\nEach edit object must be a COMPLETE find-replace pair with BOTH old_string AND new_string.`,
521
- }
522
- }
523
-
524
- const fileGroups = new Map<string, { edits: SingleEdit[]; originalContent: string | null }>()
525
- for (const e of edits) {
526
- const resolved = path.resolve(cwd, e.file_path)
527
- if (!fileGroups.has(resolved)) {
528
- let originalContent: string | null = null
529
- try {
530
- originalContent = fs.readFileSync(resolved, 'utf-8')
531
- } catch {
532
- /* file doesn't exist yet */
533
- }
534
- fileGroups.set(resolved, { edits: [], originalContent })
535
- }
536
- fileGroups.get(resolved)!.edits.push(e)
537
- }
538
-
539
- const results: string[] = []
540
- let anyFailed = false
541
- let firstError = ''
542
- const modifiedFiles = new Map<string, string | null>()
543
-
544
- for (const [resolved, group] of fileGroups) {
545
- const relPath = path.relative(cwd, resolved).replace(/\\/g, '/')
546
- let content =
547
- group.originalContent !== null ? group.originalContent.replace(/\r\n/g, '\n') : null
548
-
549
- // Check if file was read (for stale-content diagnostics)
550
- const readWarning = content !== null ? applier.checkModified(resolved) : null
551
-
552
- for (let i = group.edits.length - 1; i >= 0; i--) {
553
- const e = group.edits[i]
554
- // Normalize CRLF → LF in AI-provided strings (critical on Windows)
555
- e.old_string = e.old_string.replace(/\r\n/g, '\n')
556
- e.new_string = e.new_string.replace(/\r\n/g, '\n')
557
-
558
- if (e.old_string === '') {
559
- if (content !== null) {
560
- // File already exists in-memory (from prior edit in this batch)
561
- // or on disk — treat as error to prevent silent overwrites.
562
- results.push(
563
- ` FAIL ${relPath}: Cannot create — file already exists (duplicate create in batch or file on disk)`,
564
- )
565
- if (!anyFailed) {
566
- anyFailed = true
567
- firstError = `Cannot create file "${relPath}": file already exists`
568
- }
569
- break
570
- }
571
- const newLines = e.new_string.split('\n').length
572
- content = e.new_string
573
- results.push(
574
- ` Created ${relPath} (${newLines} lines):\n${generateDiff('', e.new_string, 1)}`,
575
- )
576
- continue
577
- }
578
-
579
- if (content === null) {
580
- const diag = buildDiag(e)
581
- const suggestion = ` → Check the file path, or create it first with old_string: "" (empty string).`
582
- results.push(` FAIL ${relPath}: File not found
583
- Edit: ${diag}
584
- ${suggestion}`)
585
- if (!anyFailed) {
586
- anyFailed = true
587
- firstError = `File not found: ${relPath}`
588
- }
589
- break
590
- }
591
-
592
- // ── Tier 1: Exact match ──────────────────────────────────────
593
- const exactIdx = content.indexOf(e.old_string)
594
-
595
- let matchInfo: { index: number; length: number; strategy: 'exact' | 'tolerant' } | null =
596
- null
597
-
598
- if (exactIdx !== -1) {
599
- // Exact match found — check for duplicates
600
- const lastIdx = content.lastIndexOf(e.old_string)
601
- if (exactIdx !== lastIdx) {
602
- const diag = buildDiag(e)
603
- const suggestion = ` → Include more surrounding context lines (2-3 lines before and after) in old_string to make the match unique.`
604
- results.push(` FAIL ${relPath}: old_string appears MULTIPLE times
605
- Edit: ${diag}
606
- ${suggestion}`)
607
- if (!anyFailed) {
608
- anyFailed = true
609
- firstError = `old_string appears MULTIPLE times in ${relPath}`
610
- }
611
- break
612
- }
613
- matchInfo = { index: exactIdx, length: e.old_string.length, strategy: 'exact' }
614
- } else {
615
- // ── Tier 2: Whitespace-normalized fallback ─────────────────
616
- const tolerant = findAllLinesTolerant(content, e.old_string)
617
- if (tolerant.length === 0) {
618
- // Not found by any strategy — include proximity hint
619
- const contentLines = content.split('\n')
620
- let hint = ''
621
- const firstLine = (e.old_string.split('\n')[0] || '').trim()
622
- if (firstLine) {
623
- const normFirst = normalizeLine(firstLine)
624
- // Try to find a line that contains the first line text
625
- const similarIdx = contentLines.findIndex(
626
- l => l.length > 0 && normFirst.length > 0 && normalizeLine(l).includes(normFirst),
627
- )
628
- if (similarIdx !== -1) {
629
- const start = Math.max(0, similarIdx - 1)
630
- const end = Math.min(contentLines.length, similarIdx + 2)
631
- const snippet = contentLines.slice(start, end).join('\n')
632
- hint = `\n Near line ${similarIdx + 1}:\n """\n${snippet}\n """`
633
- }
634
- }
635
- // Always show top of file for context, unless already shown via match
636
- if (!hint && contentLines.length > 0) {
637
- const lines = contentLines.slice(0, Math.min(contentLines.length, 5))
638
- hint = `\n File content (first ${lines.length} lines):\n """\n${lines.join('\n')}\n """`
639
- }
640
- const diag = buildDiag(e)
641
- const readHint = readWarning ? `\n ${readWarning}` : ''
642
- const suggestion = ` → Read the file again with read({ paths: ["${e.file_path}"] }) to get current content, then retry with exact matching text. Include 2-3 lines of surrounding context for uniqueness.${readHint}`
643
- results.push(` FAIL ${relPath}: old_string not found${hint}
644
- Edit: ${diag}
645
- ${suggestion}`)
646
- if (!anyFailed) {
647
- anyFailed = true
648
- firstError = `old_string not found in ${relPath}${hint}`
649
- }
650
- break
651
- }
652
- if (tolerant.length > 1) {
653
- const diag = buildDiag(e)
654
- const suggestion = ` → Include more surrounding context lines (2-3 lines before and after) in old_string to make the match unique.`
655
- results.push(
656
- ` FAIL ${relPath}: old_string appears MULTIPLE times (whitespace-normalized)\n Edit: ${diag}\n${suggestion}`,
657
- )
658
- if (!anyFailed) {
659
- anyFailed = true
660
- firstError = `old_string appears MULTIPLE times in ${relPath} (whitespace-normalized)`
661
- }
662
- break
663
- }
664
- matchInfo = {
665
- index: tolerant[0]!.index,
666
- length: tolerant[0]!.length,
667
- strategy: 'tolerant',
668
- }
669
- }
670
-
671
- // For tolerant matching, use the actual file text at the match position
672
- // so the diff reflects what was really in the file (with original whitespace).
673
- const matchedOld =
674
- matchInfo.strategy === 'tolerant'
675
- ? content.slice(matchInfo.index, matchInfo.index + matchInfo.length)
676
- : e.old_string
677
- const diff = generateDiffWithContext(
678
- content,
679
- matchedOld,
680
- e.new_string,
681
- matchInfo.index,
682
- matchInfo.length,
683
- )
684
- results.push(` Edited ${relPath}:\n${diff}`)
685
- content =
686
- content.slice(0, matchInfo.index) +
687
- e.new_string +
688
- content.slice(matchInfo.index + matchInfo.length)
689
- }
690
-
691
- if (anyFailed) break
692
- modifiedFiles.set(resolved, content)
693
- }
694
-
695
- if (anyFailed) {
696
- for (const [filePath, group] of fileGroups) {
697
- const originalContent = group.originalContent
698
- if (originalContent === null) {
699
- try {
700
- fs.unlinkSync(filePath)
701
- } catch {
702
- /* ok */
703
- }
704
- } else {
705
- try {
706
- fs.mkdirSync(path.dirname(filePath), { recursive: true })
707
- fs.writeFileSync(filePath, originalContent, 'utf-8')
708
- } catch {
709
- /* ok */
710
- }
711
- }
712
- applier.markRead(filePath)
713
- }
714
- // Only show FAIL lines in the error (successful edits were rolled back)
715
- const failLines = results.filter(r => r.startsWith(' FAIL ')).join('\n')
716
- return {
717
- success: false,
718
- output: '',
719
- error: `Edit FAILED — all changes rolled back.\n${failLines}\n\n${firstError}`,
720
- }
721
- }
722
-
723
- for (const [resolved, content] of modifiedFiles) {
724
- try {
725
- if (content === null) {
726
- fs.unlinkSync(resolved)
727
- } else {
728
- fs.mkdirSync(path.dirname(resolved), { recursive: true })
729
- fs.writeFileSync(resolved, content, 'utf-8')
730
- }
731
- } catch (err) {
732
- return {
733
- success: false,
734
- output: '',
735
- error: `Failed to write ${path.relative(cwd, resolved).replace(/\\/g, '/')}: ${fmtErr(err)}. Input: ${summarizeRawInput(rawInput)}`,
736
- }
737
- }
738
- applier.markRead(resolved)
739
- }
740
-
741
- return {
742
- success: true,
743
- output: results.join('\n').replace(/^ {2}/gm, ''),
744
- }
745
- },
746
- }
747
- }