lonny-agent 0.2.4 → 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 (509) hide show
  1. package/.github/workflows/ci.yml +21 -0
  2. package/AGENTS.md +106 -0
  3. package/CHANGELOG.md +24 -0
  4. package/README.md +326 -326
  5. package/dist/agent/__tests__/session-restore.test.d.ts +2 -0
  6. package/dist/agent/__tests__/session-restore.test.d.ts.map +1 -0
  7. package/dist/agent/__tests__/session-restore.test.js +260 -0
  8. package/dist/agent/__tests__/session-restore.test.js.map +1 -0
  9. package/dist/agent/commands.d.ts +26 -0
  10. package/dist/agent/commands.d.ts.map +1 -0
  11. package/dist/agent/commands.js +253 -0
  12. package/dist/agent/commands.js.map +1 -0
  13. package/dist/agent/compaction.js +8 -1
  14. package/dist/agent/compaction.js.map +1 -1
  15. package/dist/agent/index.d.ts +2 -1
  16. package/dist/agent/index.d.ts.map +1 -1
  17. package/dist/agent/index.js +2 -2
  18. package/dist/agent/index.js.map +1 -1
  19. package/dist/agent/llm.d.ts +4 -0
  20. package/dist/agent/llm.d.ts.map +1 -1
  21. package/dist/agent/memory.d.ts +12 -0
  22. package/dist/agent/memory.d.ts.map +1 -0
  23. package/dist/agent/memory.js +51 -0
  24. package/dist/agent/memory.js.map +1 -0
  25. package/dist/agent/prompt-builder.d.ts +6 -1
  26. package/dist/agent/prompt-builder.d.ts.map +1 -1
  27. package/dist/agent/prompt-builder.js +100 -33
  28. package/dist/agent/prompt-builder.js.map +1 -1
  29. package/dist/agent/providers/google.d.ts.map +1 -1
  30. package/dist/agent/providers/google.js.map +1 -1
  31. package/dist/agent/providers/openai.d.ts +1 -0
  32. package/dist/agent/providers/openai.d.ts.map +1 -1
  33. package/dist/agent/providers/openai.js +59 -6
  34. package/dist/agent/providers/openai.js.map +1 -1
  35. package/dist/agent/session-chat.d.ts +3 -0
  36. package/dist/agent/session-chat.d.ts.map +1 -0
  37. package/dist/agent/session-chat.js +384 -0
  38. package/dist/agent/session-chat.js.map +1 -0
  39. package/dist/agent/session-display.d.ts +29 -0
  40. package/dist/agent/session-display.d.ts.map +1 -0
  41. package/dist/agent/session-display.js +179 -0
  42. package/dist/agent/session-display.js.map +1 -0
  43. package/dist/agent/session-persistence.d.ts +55 -0
  44. package/dist/agent/session-persistence.d.ts.map +1 -0
  45. package/dist/agent/session-persistence.js +101 -0
  46. package/dist/agent/session-persistence.js.map +1 -0
  47. package/dist/agent/session-utils.d.ts +5 -0
  48. package/dist/agent/session-utils.d.ts.map +1 -0
  49. package/dist/agent/session-utils.js +51 -0
  50. package/dist/agent/session-utils.js.map +1 -0
  51. package/dist/agent/session.d.ts +19 -17
  52. package/dist/agent/session.d.ts.map +1 -1
  53. package/dist/agent/session.js +234 -578
  54. package/dist/agent/session.js.map +1 -1
  55. package/dist/cli/index.d.ts +4 -0
  56. package/dist/cli/index.d.ts.map +1 -1
  57. package/dist/cli/index.js +67 -2
  58. package/dist/cli/index.js.map +1 -1
  59. package/dist/config/index.d.ts +2 -2
  60. package/dist/config/index.d.ts.map +1 -1
  61. package/dist/config/index.js +7 -1
  62. package/dist/config/index.js.map +1 -1
  63. package/dist/index.js +24 -3
  64. package/dist/index.js.map +1 -1
  65. package/dist/tools/__tests__/bash.test.js +169 -2
  66. package/dist/tools/__tests__/bash.test.js.map +1 -1
  67. package/dist/tools/__tests__/edit.test.js +1091 -340
  68. package/dist/tools/__tests__/edit.test.js.map +1 -1
  69. package/dist/tools/__tests__/fetch.test.js +1 -1
  70. package/dist/tools/__tests__/fetch.test.js.map +1 -1
  71. package/dist/tools/__tests__/glob.test.js +1 -1
  72. package/dist/tools/__tests__/glob.test.js.map +1 -1
  73. package/dist/tools/__tests__/grep.test.js +33 -1
  74. package/dist/tools/__tests__/grep.test.js.map +1 -1
  75. package/dist/tools/__tests__/ls.test.js +1 -1
  76. package/dist/tools/__tests__/ls.test.js.map +1 -1
  77. package/dist/tools/__tests__/read.test.js +1 -1
  78. package/dist/tools/__tests__/read.test.js.map +1 -1
  79. package/dist/tools/__tests__/registry.test.js +313 -0
  80. package/dist/tools/__tests__/registry.test.js.map +1 -1
  81. package/dist/tools/__tests__/sed.test.d.ts +2 -0
  82. package/dist/tools/__tests__/sed.test.d.ts.map +1 -0
  83. package/dist/tools/__tests__/sed.test.js +228 -0
  84. package/dist/tools/__tests__/sed.test.js.map +1 -0
  85. package/dist/tools/__tests__/write_plan.test.js +1 -1
  86. package/dist/tools/__tests__/write_plan.test.js.map +1 -1
  87. package/dist/tools/bash.d.ts.map +1 -1
  88. package/dist/tools/bash.js +711 -18
  89. package/dist/tools/bash.js.map +1 -1
  90. package/dist/tools/codebase/ast/__tests__/ast-edit-demo.d.ts +2 -0
  91. package/dist/tools/codebase/ast/__tests__/ast-edit-demo.d.ts.map +1 -0
  92. package/dist/tools/codebase/ast/__tests__/ast-edit-demo.js +7 -0
  93. package/dist/tools/codebase/ast/__tests__/ast-edit-demo.js.map +1 -0
  94. package/dist/tools/codebase/ast/__tests__/ast-edit-final.d.ts +6 -0
  95. package/dist/tools/codebase/ast/__tests__/ast-edit-final.d.ts.map +1 -0
  96. package/dist/tools/codebase/ast/__tests__/ast-edit-final.js +11 -0
  97. package/dist/tools/codebase/ast/__tests__/ast-edit-final.js.map +1 -0
  98. package/dist/tools/codebase/ast/__tests__/ast-edit-test.d.ts +2 -0
  99. package/dist/tools/codebase/ast/__tests__/ast-edit-test.d.ts.map +1 -0
  100. package/dist/tools/codebase/ast/__tests__/ast-edit-test.js +9 -0
  101. package/dist/tools/codebase/ast/__tests__/ast-edit-test.js.map +1 -0
  102. package/dist/tools/codebase/ast/__tests__/integration.test.d.ts +2 -0
  103. package/dist/tools/codebase/ast/__tests__/integration.test.d.ts.map +1 -0
  104. package/dist/tools/codebase/ast/__tests__/integration.test.js +415 -0
  105. package/dist/tools/codebase/ast/__tests__/integration.test.js.map +1 -0
  106. package/dist/tools/codebase/ast/__tests__/tree-sitter.test.d.ts +2 -0
  107. package/dist/tools/codebase/ast/__tests__/tree-sitter.test.d.ts.map +1 -0
  108. package/dist/tools/codebase/ast/__tests__/tree-sitter.test.js +414 -0
  109. package/dist/tools/codebase/ast/__tests__/tree-sitter.test.js.map +1 -0
  110. package/dist/tools/codebase/ast/adapter.d.ts +23 -0
  111. package/dist/tools/codebase/ast/adapter.d.ts.map +1 -0
  112. package/dist/tools/codebase/ast/adapter.js +11 -0
  113. package/dist/tools/codebase/ast/adapter.js.map +1 -0
  114. package/dist/tools/codebase/ast/tools.d.ts +3 -0
  115. package/dist/tools/codebase/ast/tools.d.ts.map +1 -0
  116. package/dist/tools/codebase/ast/tools.js +286 -0
  117. package/dist/tools/codebase/ast/tools.js.map +1 -0
  118. package/dist/tools/codebase/ast/tree-sitter-adapter.d.ts +3 -0
  119. package/dist/tools/codebase/ast/tree-sitter-adapter.d.ts.map +1 -0
  120. package/dist/tools/codebase/ast/tree-sitter-adapter.js +412 -0
  121. package/dist/tools/codebase/ast/tree-sitter-adapter.js.map +1 -0
  122. package/dist/tools/codebase/ast/tree-sitter-converters.d.ts +4 -0
  123. package/dist/tools/codebase/ast/tree-sitter-converters.d.ts.map +1 -0
  124. package/dist/tools/codebase/ast/tree-sitter-converters.js +447 -0
  125. package/dist/tools/codebase/ast/tree-sitter-converters.js.map +1 -0
  126. package/dist/tools/codebase/ast/types.d.ts +103 -0
  127. package/dist/tools/codebase/ast/types.d.ts.map +1 -0
  128. package/dist/tools/codebase/ast/types.js +2 -0
  129. package/dist/tools/codebase/ast/types.js.map +1 -0
  130. package/dist/tools/codebase/ast/wasm/tree-sitter-python.wasm +0 -0
  131. package/dist/tools/codebase/ast/wasm/tree-sitter-tsx.wasm +0 -0
  132. package/dist/tools/codebase/ast/wasm/tree-sitter-typescript.wasm +0 -0
  133. package/dist/tools/codebase/ast/wasm/tree-sitter.wasm +0 -0
  134. package/dist/tools/codebase/ast/wasm/web-tree-sitter.wasm +0 -0
  135. package/dist/tools/codebase/find.d.ts +3 -0
  136. package/dist/tools/codebase/find.d.ts.map +1 -0
  137. package/dist/tools/codebase/find.js +61 -0
  138. package/dist/tools/codebase/find.js.map +1 -0
  139. package/dist/tools/codebase/glob.d.ts +3 -0
  140. package/dist/tools/codebase/glob.d.ts.map +1 -0
  141. package/dist/tools/codebase/glob.js +37 -0
  142. package/dist/tools/codebase/glob.js.map +1 -0
  143. package/dist/tools/codebase/grep.d.ts +3 -0
  144. package/dist/tools/codebase/grep.d.ts.map +1 -0
  145. package/dist/tools/codebase/grep.js +169 -0
  146. package/dist/tools/codebase/grep.js.map +1 -0
  147. package/dist/tools/codebase/ls.d.ts +3 -0
  148. package/dist/tools/codebase/ls.d.ts.map +1 -0
  149. package/dist/tools/codebase/ls.js +39 -0
  150. package/dist/tools/codebase/ls.js.map +1 -0
  151. package/dist/tools/codebase/read.d.ts +10 -0
  152. package/dist/tools/codebase/read.d.ts.map +1 -0
  153. package/dist/tools/codebase/read.js +81 -0
  154. package/dist/tools/codebase/read.js.map +1 -0
  155. package/dist/tools/delete_memory.d.ts +3 -0
  156. package/dist/tools/delete_memory.d.ts.map +1 -0
  157. package/dist/tools/delete_memory.js +46 -0
  158. package/dist/tools/delete_memory.js.map +1 -0
  159. package/dist/tools/edit/diff-compute.d.ts +4 -0
  160. package/dist/tools/edit/diff-compute.d.ts.map +1 -0
  161. package/dist/tools/edit/diff-compute.js +18 -0
  162. package/dist/tools/edit/diff-compute.js.map +1 -0
  163. package/dist/tools/edit/diff-render.d.ts +18 -0
  164. package/dist/tools/edit/diff-render.d.ts.map +1 -0
  165. package/dist/tools/edit/diff-render.js +128 -0
  166. package/dist/tools/edit/diff-render.js.map +1 -0
  167. package/dist/tools/edit/edit.d.ts +4 -0
  168. package/dist/tools/edit/edit.d.ts.map +1 -0
  169. package/dist/tools/edit/edit.js +440 -0
  170. package/dist/tools/edit/edit.js.map +1 -0
  171. package/dist/tools/edit/matcher.d.ts +24 -0
  172. package/dist/tools/edit/matcher.d.ts.map +1 -0
  173. package/dist/tools/edit/matcher.js +66 -0
  174. package/dist/tools/edit/matcher.js.map +1 -0
  175. package/dist/tools/edit/parser.d.ts +10 -0
  176. package/dist/tools/edit/parser.d.ts.map +1 -0
  177. package/dist/tools/edit/parser.js +120 -0
  178. package/dist/tools/edit/parser.js.map +1 -0
  179. package/dist/tools/edit/types.d.ts +16 -0
  180. package/dist/tools/edit/types.d.ts.map +1 -0
  181. package/dist/tools/edit/types.js +2 -0
  182. package/dist/tools/edit/types.js.map +1 -0
  183. package/dist/tools/edit/write_plan.d.ts +5 -0
  184. package/dist/tools/edit/write_plan.d.ts.map +1 -0
  185. package/dist/tools/edit/write_plan.js +98 -0
  186. package/dist/tools/edit/write_plan.js.map +1 -0
  187. package/dist/tools/edit.d.ts +50 -9
  188. package/dist/tools/edit.d.ts.map +1 -1
  189. package/dist/tools/edit.js +394 -159
  190. package/dist/tools/edit.js.map +1 -1
  191. package/dist/tools/execute/bash/constants.d.ts +24 -0
  192. package/dist/tools/execute/bash/constants.d.ts.map +1 -0
  193. package/dist/tools/execute/bash/constants.js +88 -0
  194. package/dist/tools/execute/bash/constants.js.map +1 -0
  195. package/dist/tools/execute/bash/errors.d.ts +39 -0
  196. package/dist/tools/execute/bash/errors.d.ts.map +1 -0
  197. package/dist/tools/execute/bash/errors.js +249 -0
  198. package/dist/tools/execute/bash/errors.js.map +1 -0
  199. package/dist/tools/execute/bash/execution.d.ts +18 -0
  200. package/dist/tools/execute/bash/execution.d.ts.map +1 -0
  201. package/dist/tools/execute/bash/execution.js +207 -0
  202. package/dist/tools/execute/bash/execution.js.map +1 -0
  203. package/dist/tools/execute/bash/index.d.ts +3 -0
  204. package/dist/tools/execute/bash/index.d.ts.map +1 -0
  205. package/dist/tools/execute/bash/index.js +117 -0
  206. package/dist/tools/execute/bash/index.js.map +1 -0
  207. package/dist/tools/execute/bash/platform.d.ts +6 -0
  208. package/dist/tools/execute/bash/platform.d.ts.map +1 -0
  209. package/dist/tools/execute/bash/platform.js +64 -0
  210. package/dist/tools/execute/bash/platform.js.map +1 -0
  211. package/dist/tools/execute/bash/security.d.ts +12 -0
  212. package/dist/tools/execute/bash/security.d.ts.map +1 -0
  213. package/dist/tools/execute/bash/security.js +26 -0
  214. package/dist/tools/execute/bash/security.js.map +1 -0
  215. package/dist/tools/execute/bash/validation.d.ts +16 -0
  216. package/dist/tools/execute/bash/validation.d.ts.map +1 -0
  217. package/dist/tools/execute/bash/validation.js +57 -0
  218. package/dist/tools/execute/bash/validation.js.map +1 -0
  219. package/dist/tools/execute/bash.d.ts +3 -0
  220. package/dist/tools/execute/bash.d.ts.map +1 -0
  221. package/dist/tools/execute/bash.js +728 -0
  222. package/dist/tools/execute/bash.js.map +1 -0
  223. package/dist/tools/execute/git.d.ts +3 -0
  224. package/dist/tools/execute/git.d.ts.map +1 -0
  225. package/dist/tools/execute/git.js +183 -0
  226. package/dist/tools/execute/git.js.map +1 -0
  227. package/dist/tools/execute/task_complete.d.ts +7 -0
  228. package/dist/tools/execute/task_complete.d.ts.map +1 -0
  229. package/dist/tools/execute/task_complete.js +38 -0
  230. package/dist/tools/execute/task_complete.js.map +1 -0
  231. package/dist/tools/fetch.d.ts.map +1 -1
  232. package/dist/tools/fetch.js +2 -0
  233. package/dist/tools/fetch.js.map +1 -1
  234. package/dist/tools/find.d.ts.map +1 -1
  235. package/dist/tools/find.js +2 -0
  236. package/dist/tools/find.js.map +1 -1
  237. package/dist/tools/git.d.ts.map +1 -1
  238. package/dist/tools/git.js +2 -0
  239. package/dist/tools/git.js.map +1 -1
  240. package/dist/tools/glob.d.ts.map +1 -1
  241. package/dist/tools/glob.js +2 -0
  242. package/dist/tools/glob.js.map +1 -1
  243. package/dist/tools/grep.d.ts.map +1 -1
  244. package/dist/tools/grep.js +56 -30
  245. package/dist/tools/grep.js.map +1 -1
  246. package/dist/tools/install/clawhub.d.ts +26 -0
  247. package/dist/tools/install/clawhub.d.ts.map +1 -0
  248. package/dist/tools/install/clawhub.js +182 -0
  249. package/dist/tools/install/clawhub.js.map +1 -0
  250. package/dist/tools/install/constants.d.ts +5 -0
  251. package/dist/tools/install/constants.d.ts.map +1 -0
  252. package/dist/tools/install/constants.js +5 -0
  253. package/dist/tools/install/constants.js.map +1 -0
  254. package/dist/tools/install/install_skill.d.ts +3 -0
  255. package/dist/tools/install/install_skill.d.ts.map +1 -0
  256. package/dist/tools/install/install_skill.js +132 -0
  257. package/dist/tools/install/install_skill.js.map +1 -0
  258. package/dist/tools/install/npm.d.ts +8 -0
  259. package/dist/tools/install/npm.d.ts.map +1 -0
  260. package/dist/tools/install/npm.js +52 -0
  261. package/dist/tools/install/npm.js.map +1 -0
  262. package/dist/tools/install/skill-content.d.ts +9 -0
  263. package/dist/tools/install/skill-content.d.ts.map +1 -0
  264. package/dist/tools/install/skill-content.js +89 -0
  265. package/dist/tools/install/skill-content.js.map +1 -0
  266. package/dist/tools/install_skill.d.ts.map +1 -1
  267. package/dist/tools/install_skill.js +2 -0
  268. package/dist/tools/install_skill.js.map +1 -1
  269. package/dist/tools/list_memory.d.ts +3 -0
  270. package/dist/tools/list_memory.d.ts.map +1 -0
  271. package/dist/tools/list_memory.js +55 -0
  272. package/dist/tools/list_memory.js.map +1 -0
  273. package/dist/tools/ls.d.ts.map +1 -1
  274. package/dist/tools/ls.js +2 -0
  275. package/dist/tools/ls.js.map +1 -1
  276. package/dist/tools/memory/delete_memory.d.ts +3 -0
  277. package/dist/tools/memory/delete_memory.d.ts.map +1 -0
  278. package/dist/tools/memory/delete_memory.js +46 -0
  279. package/dist/tools/memory/delete_memory.js.map +1 -0
  280. package/dist/tools/memory/list_memory.d.ts +3 -0
  281. package/dist/tools/memory/list_memory.d.ts.map +1 -0
  282. package/dist/tools/memory/list_memory.js +55 -0
  283. package/dist/tools/memory/list_memory.js.map +1 -0
  284. package/dist/tools/memory/save_memory.d.ts +3 -0
  285. package/dist/tools/memory/save_memory.d.ts.map +1 -0
  286. package/dist/tools/memory/save_memory.js +45 -0
  287. package/dist/tools/memory/save_memory.js.map +1 -0
  288. package/dist/tools/read.d.ts.map +1 -1
  289. package/dist/tools/read.js +2 -0
  290. package/dist/tools/read.js.map +1 -1
  291. package/dist/tools/registry.d.ts +7 -8
  292. package/dist/tools/registry.d.ts.map +1 -1
  293. package/dist/tools/registry.js +114 -46
  294. package/dist/tools/registry.js.map +1 -1
  295. package/dist/tools/save_memory.d.ts +3 -0
  296. package/dist/tools/save_memory.d.ts.map +1 -0
  297. package/dist/tools/save_memory.js +45 -0
  298. package/dist/tools/save_memory.js.map +1 -0
  299. package/dist/tools/search.d.ts.map +1 -1
  300. package/dist/tools/search.js +2 -0
  301. package/dist/tools/search.js.map +1 -1
  302. package/dist/tools/sed.d.ts +4 -0
  303. package/dist/tools/sed.d.ts.map +1 -0
  304. package/dist/tools/sed.js +121 -0
  305. package/dist/tools/sed.js.map +1 -0
  306. package/dist/tools/tree.d.ts +12 -0
  307. package/dist/tools/tree.d.ts.map +1 -0
  308. package/dist/tools/tree.js +87 -0
  309. package/dist/tools/tree.js.map +1 -0
  310. package/dist/tools/types.d.ts +12 -0
  311. package/dist/tools/types.d.ts.map +1 -1
  312. package/dist/tools/web/fetch.d.ts +3 -0
  313. package/dist/tools/web/fetch.d.ts.map +1 -0
  314. package/dist/tools/web/fetch.js +49 -0
  315. package/dist/tools/web/fetch.js.map +1 -0
  316. package/dist/tools/web/search.d.ts +3 -0
  317. package/dist/tools/web/search.d.ts.map +1 -0
  318. package/dist/tools/web/search.js +134 -0
  319. package/dist/tools/web/search.js.map +1 -0
  320. package/dist/tools/write_plan.d.ts.map +1 -1
  321. package/dist/tools/write_plan.js +2 -0
  322. package/dist/tools/write_plan.js.map +1 -1
  323. package/dist/tui/commands.d.ts +4 -0
  324. package/dist/tui/commands.d.ts.map +1 -0
  325. package/dist/tui/commands.js +118 -0
  326. package/dist/tui/commands.js.map +1 -0
  327. package/dist/tui/components/colors.d.ts +36 -0
  328. package/dist/tui/components/colors.d.ts.map +1 -0
  329. package/dist/tui/components/colors.js +48 -0
  330. package/dist/tui/components/colors.js.map +1 -0
  331. package/dist/tui/components/header-bar.d.ts +26 -0
  332. package/dist/tui/components/header-bar.d.ts.map +1 -0
  333. package/dist/tui/components/header-bar.js +78 -0
  334. package/dist/tui/components/header-bar.js.map +1 -0
  335. package/dist/tui/components/index.d.ts +9 -0
  336. package/dist/tui/components/index.d.ts.map +1 -0
  337. package/dist/tui/components/index.js +9 -0
  338. package/dist/tui/components/index.js.map +1 -0
  339. package/dist/tui/components/landing-screen.d.ts +12 -0
  340. package/dist/tui/components/landing-screen.d.ts.map +1 -0
  341. package/dist/tui/components/landing-screen.js +53 -0
  342. package/dist/tui/components/landing-screen.js.map +1 -0
  343. package/dist/tui/components/pixel-logo.d.ts +5 -0
  344. package/dist/tui/components/pixel-logo.d.ts.map +1 -0
  345. package/dist/tui/components/pixel-logo.js +27 -0
  346. package/dist/tui/components/pixel-logo.js.map +1 -0
  347. package/dist/tui/components/plan-utils.d.ts +11 -0
  348. package/dist/tui/components/plan-utils.d.ts.map +1 -0
  349. package/dist/tui/components/plan-utils.js +62 -0
  350. package/dist/tui/components/plan-utils.js.map +1 -0
  351. package/dist/tui/components/plans-list.d.ts +18 -0
  352. package/dist/tui/components/plans-list.d.ts.map +1 -0
  353. package/dist/tui/components/plans-list.js +46 -0
  354. package/dist/tui/components/plans-list.js.map +1 -0
  355. package/dist/tui/components/rich-footer.d.ts +29 -0
  356. package/dist/tui/components/rich-footer.d.ts.map +1 -0
  357. package/dist/tui/components/rich-footer.js +124 -0
  358. package/dist/tui/components/rich-footer.js.map +1 -0
  359. package/dist/tui/components/todo-panel.d.ts +14 -0
  360. package/dist/tui/components/todo-panel.d.ts.map +1 -0
  361. package/dist/tui/components/todo-panel.js +83 -0
  362. package/dist/tui/components/todo-panel.js.map +1 -0
  363. package/dist/tui/components.d.ts.map +1 -1
  364. package/dist/tui/components.js +14 -7
  365. package/dist/tui/components.js.map +1 -1
  366. package/dist/tui/index.d.ts +2 -1
  367. package/dist/tui/index.d.ts.map +1 -1
  368. package/dist/tui/index.js +99 -505
  369. package/dist/tui/index.js.map +1 -1
  370. package/dist/tui/overlays.d.ts +28 -0
  371. package/dist/tui/overlays.d.ts.map +1 -0
  372. package/dist/tui/overlays.js +154 -0
  373. package/dist/tui/overlays.js.map +1 -0
  374. package/dist/tui/themes.d.ts +5 -0
  375. package/dist/tui/themes.d.ts.map +1 -0
  376. package/dist/tui/themes.js +38 -0
  377. package/dist/tui/themes.js.map +1 -0
  378. package/dist/web/index.d.ts.map +1 -1
  379. package/dist/web/index.js +62 -101
  380. package/dist/web/index.js.map +1 -1
  381. package/dist/web/public/app.js +5 -855
  382. package/dist/web/public/confirm.js +70 -0
  383. package/dist/web/public/index.html +121 -120
  384. package/dist/web/public/input.js +107 -0
  385. package/dist/web/public/messages.js +411 -0
  386. package/dist/web/public/sidebar.js +82 -0
  387. package/dist/web/public/state.js +72 -0
  388. package/dist/web/public/style.css +247 -70
  389. package/dist/web/public/utils.js +37 -0
  390. package/dist/web/public/websocket.js +267 -0
  391. package/dist/web/public/ws.js +19 -0
  392. package/dist/web/session-bridge.d.ts.map +1 -1
  393. package/dist/web/session-bridge.js +85 -10
  394. package/dist/web/session-bridge.js.map +1 -1
  395. package/docs/ast-tools.md +117 -0
  396. package/package.json +11 -5
  397. package/scripts/check-line-count.mjs +53 -0
  398. package/scripts/copy-ast-wasm.mjs +32 -0
  399. package/scripts/copy-native.mjs +24 -0
  400. package/scripts/copy-web.mjs +15 -0
  401. package/src/agent/__tests__/session-restore.test.ts +284 -0
  402. package/src/agent/commands.ts +274 -0
  403. package/src/agent/compaction.ts +9 -2
  404. package/src/agent/index.ts +7 -2
  405. package/src/agent/llm.ts +4 -0
  406. package/src/agent/memory.ts +60 -0
  407. package/src/agent/prompt-builder.ts +111 -33
  408. package/src/agent/providers/google.ts +0 -1
  409. package/src/agent/providers/openai.ts +79 -8
  410. package/src/agent/session-chat.ts +480 -0
  411. package/src/agent/session-display.ts +195 -0
  412. package/src/agent/session-persistence.ts +146 -0
  413. package/src/agent/session-utils.ts +54 -0
  414. package/src/agent/session.ts +269 -670
  415. package/src/cli/index.ts +73 -4
  416. package/src/config/index.ts +14 -7
  417. package/src/index.ts +30 -3
  418. package/src/tools/__tests__/bash.test.ts +188 -2
  419. package/src/tools/__tests__/edit.test.ts +1216 -356
  420. package/src/tools/__tests__/fetch.test.ts +1 -1
  421. package/src/tools/__tests__/glob.test.ts +1 -1
  422. package/src/tools/__tests__/grep.test.ts +36 -1
  423. package/src/tools/__tests__/ls.test.ts +1 -1
  424. package/src/tools/__tests__/read.test.ts +1 -1
  425. package/src/tools/__tests__/registry.test.ts +332 -0
  426. package/src/tools/__tests__/write_plan.test.ts +1 -1
  427. package/src/tools/codebase/ast/__tests__/integration.test.ts +452 -0
  428. package/src/tools/codebase/ast/__tests__/tree-sitter.test.ts +466 -0
  429. package/src/tools/codebase/ast/adapter.ts +44 -0
  430. package/src/tools/codebase/ast/tools.ts +322 -0
  431. package/src/tools/codebase/ast/tree-sitter-adapter.ts +468 -0
  432. package/src/tools/codebase/ast/tree-sitter-converters.ts +480 -0
  433. package/src/tools/codebase/ast/types.ts +102 -0
  434. package/src/tools/codebase/ast/wasm/tree-sitter-python.wasm +0 -0
  435. package/src/tools/codebase/ast/wasm/tree-sitter-tsx.wasm +0 -0
  436. package/src/tools/codebase/ast/wasm/tree-sitter-typescript.wasm +0 -0
  437. package/src/tools/codebase/ast/wasm/tree-sitter.wasm +0 -0
  438. package/src/tools/codebase/ast/wasm/web-tree-sitter.wasm +0 -0
  439. package/src/tools/{find.ts → codebase/find.ts} +4 -2
  440. package/src/tools/{glob.ts → codebase/glob.ts} +4 -2
  441. package/src/tools/{grep.ts → codebase/grep.ts} +61 -32
  442. package/src/tools/{ls.ts → codebase/ls.ts} +4 -2
  443. package/src/tools/{read.ts → codebase/read.ts} +5 -3
  444. package/src/tools/edit/diff-compute.ts +21 -0
  445. package/src/tools/edit/diff-render.ts +145 -0
  446. package/src/tools/edit/edit.ts +468 -0
  447. package/src/tools/edit/matcher.ts +70 -0
  448. package/src/tools/edit/parser.ts +131 -0
  449. package/src/tools/edit/types.ts +19 -0
  450. package/src/tools/{write_plan.ts → edit/write_plan.ts} +4 -2
  451. package/src/tools/execute/bash/constants.ts +93 -0
  452. package/src/tools/execute/bash/errors.ts +304 -0
  453. package/src/tools/execute/bash/execution.ts +211 -0
  454. package/src/tools/execute/bash/index.ts +131 -0
  455. package/src/tools/execute/bash/platform.ts +65 -0
  456. package/src/tools/execute/bash/security.ts +28 -0
  457. package/src/tools/execute/bash/validation.ts +78 -0
  458. package/src/tools/execute/git.ts +209 -0
  459. package/src/tools/execute/task_complete.ts +40 -0
  460. package/src/tools/install/clawhub.ts +222 -0
  461. package/src/tools/install/constants.ts +4 -0
  462. package/src/tools/install/install_skill.ts +150 -0
  463. package/src/tools/install/npm.ts +60 -0
  464. package/src/tools/install/skill-content.ts +108 -0
  465. package/src/tools/memory/delete_memory.ts +46 -0
  466. package/src/tools/memory/list_memory.ts +53 -0
  467. package/src/tools/memory/save_memory.ts +45 -0
  468. package/src/tools/registry.ts +121 -50
  469. package/src/tools/tree.ts +103 -0
  470. package/src/tools/types.ts +13 -0
  471. package/src/tools/{fetch.ts → web/fetch.ts} +4 -2
  472. package/src/tools/{search.ts → web/search.ts} +4 -2
  473. package/src/tui/commands.ts +135 -0
  474. package/src/tui/components/colors.ts +52 -0
  475. package/src/tui/components/header-bar.ts +88 -0
  476. package/src/tui/components/index.ts +8 -0
  477. package/src/tui/components/landing-screen.ts +67 -0
  478. package/src/tui/components/pixel-logo.ts +33 -0
  479. package/src/tui/components/plan-utils.ts +69 -0
  480. package/src/tui/components/plans-list.ts +55 -0
  481. package/src/tui/components/rich-footer.ts +146 -0
  482. package/src/tui/components/todo-panel.ts +95 -0
  483. package/src/tui/index.ts +110 -561
  484. package/src/tui/overlays.ts +203 -0
  485. package/src/tui/themes.ts +41 -0
  486. package/src/web/index.ts +66 -107
  487. package/src/web/public/app.js +5 -855
  488. package/src/web/public/confirm.js +70 -0
  489. package/src/web/public/index.html +121 -120
  490. package/src/web/public/input.js +107 -0
  491. package/src/web/public/messages.js +411 -0
  492. package/src/web/public/sidebar.js +82 -0
  493. package/src/web/public/state.js +72 -0
  494. package/src/web/public/style.css +247 -70
  495. package/src/web/public/utils.js +37 -0
  496. package/src/web/public/websocket.js +267 -0
  497. package/src/web/public/ws.js +19 -0
  498. package/src/web/session-bridge.ts +90 -12
  499. package/.claude/settings.local.json +0 -14
  500. package/.kilo/plans/1780064105789-mighty-pixel.md +0 -171
  501. package/.kilo/plans/1780293725888-quick-wizard.md +0 -62
  502. package/.lonny/plan-web-cwd-status.md +0 -38
  503. package/AGENT.md +0 -99
  504. package/src/tools/bash.ts +0 -39
  505. package/src/tools/edit.ts +0 -554
  506. package/src/tools/exec.ts +0 -348
  507. package/src/tools/git.ts +0 -76
  508. package/src/tools/install_skill.ts +0 -539
  509. package/src/tui/components.ts +0 -635
@@ -0,0 +1,468 @@
1
+ import * as fs from 'node:fs'
2
+ import * as path from 'node:path'
3
+ import type { FileReadTracker } from '../../diff/apply.js'
4
+ import { fmtErr } from '../errors.js'
5
+ import type { Tool, ToolResult } from '../types.js'
6
+ import { generateDiff, generateDiffWithContext } from './diff-render.js'
7
+ import { findAllLinesTolerant, normalizeLine } from './matcher.js'
8
+ import { buildDiag, extractEditsFromJSON, parseMarkdownEdit, summarizeRawInput } from './parser.js'
9
+ import type { Edit, SingleEdit } from './types.js'
10
+
11
+ export function createEditTool(applier: FileReadTracker, cwd: string): Tool {
12
+ return {
13
+ definition: {
14
+ name: 'edit',
15
+ category: 'Edit',
16
+ group: 'File',
17
+ description: `Replace exact text in files using markdown code block format.
18
+ IMPORTANT: There is no "write" tool — always use this tool ("edit") to modify files.
19
+
20
+ HOW TO USE:
21
+ 1. Read the file first with \`read\`
22
+ 2. Copy the EXACT text to replace — include 2-3 lines of context before/after
23
+ 3. Use markdown code block format below
24
+
25
+ FORMAT:
26
+ \`\`\`edit
27
+ file: <file_path>
28
+ old: |
29
+ <exact text to find>
30
+ new: |
31
+ <replacement text>
32
+ \`\`\`
33
+
34
+ EXAMPLES:
35
+ Single edit:
36
+ \`\`\`edit
37
+ file: src/config.ts
38
+ old: |
39
+ mode: 'code'
40
+ new: |
41
+ mode: 'plan'
42
+ \`\`\`
43
+
44
+ Create new file:
45
+ \`\`\`edit
46
+ file: src/new.ts
47
+ old: |
48
+ new: |
49
+ const x = 1
50
+ \`\`\`
51
+
52
+ Multiple files (use separate code blocks):
53
+ \`\`\`edit
54
+ file: src/config.ts
55
+ old: |
56
+ mode: 'code'
57
+ new: |
58
+ mode: 'plan'
59
+ \`\`\`
60
+ \`\`\`edit
61
+ file: src/constants.ts
62
+ old: |
63
+ MAX_RETRIES = 3
64
+ new: |
65
+ MAX_RETRIES = 5
66
+ \`\`\`
67
+
68
+ CRITICAL RULES:
69
+ - old and new are separated by "old:" and "new:" labels
70
+ - Use | after label for multi-line content
71
+ - old_string must match EXACTLY (whitespace, indentation, line breaks)
72
+ - Do NOT include the "<lineNumber>: " prefix from read output
73
+ - old_string must be CONTIGUOUS text from the file — you CANNOT skip lines between old_string and new_string boundaries. If you need to edit non-adjacent sections, use separate \`\`\`edit blocks.`,
74
+ parameters: {
75
+ content: {
76
+ type: 'string',
77
+ description: 'Markdown code block with edit instructions. See description for format.',
78
+ required: true,
79
+ },
80
+ },
81
+ },
82
+ async execute(input): Promise<ToolResult> {
83
+ // Debug: keep rawInput for error messages (capture early for all error paths)
84
+ const rawInput = input
85
+
86
+ // ── Parse markdown format ─────────────────────────────────────────
87
+ let edits: Edit[] = []
88
+
89
+ // If input has 'content' field (new markdown format)
90
+ if (typeof input.content === 'string') {
91
+ // Compat: handle double-JSON-wrapped content
92
+ let contentStr = input.content
93
+ try {
94
+ const parsed = JSON.parse(contentStr)
95
+ if (typeof parsed === 'string') {
96
+ contentStr = parsed
97
+ } else if (parsed && typeof parsed === 'object' && typeof parsed.content === 'string') {
98
+ contentStr = parsed.content
99
+ }
100
+ } catch {
101
+ // Not JSON — use as-is
102
+ }
103
+ edits = parseMarkdownEdit(contentStr)
104
+ if (edits.length === 0) {
105
+ return {
106
+ success: false,
107
+ output: '',
108
+ error: `Failed to parse edit format. Raw input: ${summarizeRawInput(rawInput)}\nUse: \`\`\`edit\\nfile: path\\nold: |\\ntext\\nnew: |\\ntext\\n\`\`\``,
109
+ }
110
+ }
111
+ } else {
112
+ // Legacy JSON format (backward compatibility)
113
+ edits = extractEditsFromJSON(input as Record<string, unknown>)
114
+ }
115
+
116
+ if (edits.length === 0) {
117
+ // Check if input specifically had empty edits array (for better error message)
118
+ const inputEdits = (input as Record<string, unknown>).edits
119
+ if ('edits' in input && Array.isArray(inputEdits) && inputEdits.length === 0) {
120
+ return {
121
+ success: false,
122
+ output: '',
123
+ error: `edit FAILED — no edits to apply. The edits array exists but is empty. Raw input: ${summarizeRawInput(rawInput)}`,
124
+ }
125
+ }
126
+ // Check if input has edits key but it's not an array
127
+ if ('edits' in input && !Array.isArray((input as Record<string, unknown>).edits)) {
128
+ return {
129
+ success: false,
130
+ output: '',
131
+ error: `edit requires "edits" array. Use markdown format: \`\`\`edit\\nfile: path\\nold: |\\ntext\\nnew: |\\ntext\\n\`\`\`\nRaw input: ${summarizeRawInput(rawInput)}`,
132
+ }
133
+ }
134
+ return {
135
+ success: false,
136
+ output: '',
137
+ 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)}`,
138
+ }
139
+ }
140
+
141
+ // Validate each edit object
142
+ const editErrors: string[] = []
143
+ for (let i = 0; i < edits.length; i++) {
144
+ const e = edits[i]
145
+ const missing: string[] = []
146
+ if (typeof e.file_path !== 'string' || !e.file_path) missing.push('file_path')
147
+ if (typeof e.old_string !== 'string') missing.push('old_string')
148
+ if (typeof e.new_string !== 'string') missing.push('new_string')
149
+ if (e.new_string === '__MISSING_NEW_STRING__') {
150
+ missing.push('new_string (required — LLM must provide a non-empty replacement)')
151
+ }
152
+ if (missing.length > 0) {
153
+ const present = Object.keys(e)
154
+ .filter(k => typeof e[k as keyof Edit] === 'string')
155
+ .join(', ')
156
+ editErrors.push(
157
+ ` edit #${i + 1}: missing ${missing.join(', ')}${present ? ` (has: ${present})` : ''}`,
158
+ )
159
+ }
160
+ }
161
+ if (editErrors.length > 0) {
162
+ return {
163
+ success: false,
164
+ output: '',
165
+ 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.`,
166
+ }
167
+ }
168
+
169
+ // ── Path traversal security check (symlink-aware) ─────────────────
170
+ const resolvedCwd = path.resolve(cwd)
171
+ let realCwd: string
172
+ try {
173
+ realCwd = fs.realpathSync(resolvedCwd)
174
+ } catch {
175
+ realCwd = resolvedCwd
176
+ }
177
+ for (let i = 0; i < edits.length; i++) {
178
+ const e = edits[i]
179
+ const resolved = path.resolve(cwd, e.file_path)
180
+ const relative = path.relative(resolvedCwd, resolved)
181
+ if (relative.startsWith('..') || path.isAbsolute(relative)) {
182
+ return {
183
+ success: false,
184
+ output: '',
185
+ error:
186
+ 'edit FAILED — Path traversal detected: "' +
187
+ e.file_path +
188
+ '" resolves outside the working directory "' +
189
+ resolvedCwd +
190
+ '". All file paths must be within the project directory.',
191
+ }
192
+ }
193
+ // Symlink-aware check: resolve real path to detect traversal via symlink
194
+ let realPath: string
195
+ try {
196
+ realPath = fs.realpathSync(resolved)
197
+ } catch {
198
+ // File doesn't exist yet – resolve nearest existing ancestor through symlinks
199
+ let dir = path.dirname(resolved)
200
+ while (!fs.existsSync(dir)) {
201
+ const parent = path.dirname(dir)
202
+ if (parent === dir) break // root
203
+ dir = parent
204
+ }
205
+ try {
206
+ realPath = path.join(fs.realpathSync(dir), path.relative(dir, resolved))
207
+ } catch {
208
+ realPath = resolved
209
+ }
210
+ }
211
+ const realRelative = path.relative(realCwd, realPath)
212
+ if (realRelative.startsWith('..') || path.isAbsolute(realRelative)) {
213
+ return {
214
+ success: false,
215
+ output: '',
216
+ error:
217
+ 'edit FAILED — Path traversal detected via symlink: "' +
218
+ e.file_path +
219
+ '" resolves outside "' +
220
+ realCwd +
221
+ '".',
222
+ }
223
+ }
224
+ }
225
+
226
+ const fileGroups = new Map<string, { edits: SingleEdit[]; originalContent: string | null }>()
227
+ for (const e of edits) {
228
+ const resolved = path.resolve(cwd, e.file_path)
229
+ if (!fileGroups.has(resolved)) {
230
+ let originalContent: string | null = null
231
+ try {
232
+ originalContent = fs.readFileSync(resolved, 'utf-8')
233
+ } catch {
234
+ /* file doesn't exist yet */
235
+ }
236
+ fileGroups.set(resolved, { edits: [], originalContent })
237
+ }
238
+ fileGroups.get(resolved)!.edits.push(e)
239
+ }
240
+
241
+ const results: string[] = []
242
+ let anyFailed = false
243
+ let firstError = ''
244
+ const modifiedFiles = new Map<string, string | null>()
245
+
246
+ for (const [resolved, group] of fileGroups) {
247
+ const relPath = path.relative(cwd, resolved).replace(/\\/g, '/')
248
+ let content =
249
+ group.originalContent !== null ? group.originalContent.replace(/\r\n/g, '\n') : null
250
+
251
+ // Check if file was read (for stale-content diagnostics)
252
+ const readWarning = content !== null ? applier.checkModified(resolved) : null
253
+
254
+ for (let i = group.edits.length - 1; i >= 0; i--) {
255
+ const e = group.edits[i]
256
+ // Normalize CRLF → LF in AI-provided strings (critical on Windows)
257
+ e.old_string = e.old_string.replace(/\r\n/g, '\n')
258
+ e.new_string = e.new_string.replace(/\r\n/g, '\n')
259
+
260
+ if (e.old_string === '') {
261
+ if (content !== null) {
262
+ // File already exists in-memory (from prior edit in this batch)
263
+ // or on disk — treat as error to prevent silent overwrites.
264
+ results.push(
265
+ ` FAIL ${relPath}: Cannot create — file already exists (duplicate create in batch or file on disk)`,
266
+ )
267
+ if (!anyFailed) {
268
+ anyFailed = true
269
+ firstError = `Cannot create file "${relPath}": file already exists`
270
+ }
271
+ break
272
+ }
273
+ const newLines = e.new_string.split('\n').length
274
+ content = e.new_string
275
+ results.push(
276
+ ` Created ${relPath} (${newLines} lines):\n${generateDiff('', e.new_string, 1)}`,
277
+ )
278
+ continue
279
+ }
280
+
281
+ if (content === null) {
282
+ const diag = buildDiag(e)
283
+ const suggestion = ` → Check the file path, or create it first with old_string: "" (empty string).`
284
+ results.push(` FAIL ${relPath}: File not found
285
+ Edit: ${diag}
286
+ ${suggestion}`)
287
+ if (!anyFailed) {
288
+ anyFailed = true
289
+ firstError = `File not found: ${relPath}`
290
+ }
291
+ break
292
+ }
293
+
294
+ // ── Tier 1: Exact match ──────────────────────────────────────
295
+ const exactIdx = content.indexOf(e.old_string)
296
+
297
+ let matchInfo: { index: number; length: number; strategy: 'exact' | 'tolerant' } | null =
298
+ null
299
+
300
+ if (exactIdx !== -1) {
301
+ // Exact match found — check for duplicates
302
+ const lastIdx = content.lastIndexOf(e.old_string)
303
+ if (exactIdx !== lastIdx) {
304
+ const diag = buildDiag(e)
305
+ const suggestion = ` → Include more surrounding context lines (2-3 lines before and after) in old_string to make the match unique.`
306
+ results.push(` FAIL ${relPath}: old_string appears MULTIPLE times
307
+ Edit: ${diag}
308
+ ${suggestion}`)
309
+ if (!anyFailed) {
310
+ anyFailed = true
311
+ firstError = `old_string appears MULTIPLE times in ${relPath}`
312
+ }
313
+ break
314
+ }
315
+ matchInfo = { index: exactIdx, length: e.old_string.length, strategy: 'exact' }
316
+ } else {
317
+ // ── Tier 2: Whitespace-normalized fallback ─────────────────
318
+ const tolerant = findAllLinesTolerant(content, e.old_string)
319
+ if (tolerant.length === 0) {
320
+ // Not found by any strategy — check if old_string lines are scattered (non-contiguous)
321
+ const contentLines = content.split('\n')
322
+ const oldLines = e.old_string.split('\n').filter(l => l.trim())
323
+ const foundIndices: number[] = []
324
+ for (const ol of oldLines) {
325
+ const normOl = normalizeLine(ol.trim())
326
+ if (!normOl) continue
327
+ const idx = contentLines.findIndex(
328
+ (cl, ci) => !foundIndices.includes(ci) && normalizeLine(cl).includes(normOl),
329
+ )
330
+ if (idx !== -1) foundIndices.push(idx)
331
+ }
332
+ let hint = ''
333
+ const isScattered =
334
+ foundIndices.length >= 2 &&
335
+ foundIndices[foundIndices.length - 1]! - foundIndices[0]! + 1 > foundIndices.length
336
+
337
+ if (isScattered) {
338
+ hint = `\n ⚠️ ${foundIndices.length} lines of old_string were found in the file, but they are NOT contiguous — you skipped lines between them.
339
+ old_string must be a CONTIGUOUS chunk of the file. Use separate \`\`\`edit blocks for each section (see "Multiple files" example above).
340
+ Matched at lines: ${foundIndices.map(i => i + 1).join(', ')}`
341
+ } else {
342
+ const firstLine = (e.old_string.split('\n')[0] || '').trim()
343
+ if (firstLine) {
344
+ const normFirst = normalizeLine(firstLine)
345
+ const similarIdx = contentLines.findIndex(
346
+ l =>
347
+ l.length > 0 && normFirst.length > 0 && normalizeLine(l).includes(normFirst),
348
+ )
349
+ if (similarIdx !== -1) {
350
+ const start = Math.max(0, similarIdx - 1)
351
+ const end = Math.min(contentLines.length, similarIdx + 2)
352
+ const snippet = contentLines.slice(start, end).join('\n')
353
+ hint = `\n Near line ${similarIdx + 1}:\n """\n${snippet}\n """`
354
+ }
355
+ }
356
+ if (!hint && contentLines.length > 0) {
357
+ const lines = contentLines.slice(0, Math.min(contentLines.length, 5))
358
+ hint = `\n File content (first ${lines.length} lines):\n """\n${lines.join('\n')}\n """`
359
+ }
360
+ }
361
+ const diag = buildDiag(e)
362
+ const readHint = readWarning ? `\n ${readWarning}` : ''
363
+ 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}`
364
+ results.push(` FAIL ${relPath}: old_string not found${hint}
365
+ Edit: ${diag}
366
+ ${suggestion}`)
367
+ if (!anyFailed) {
368
+ anyFailed = true
369
+ firstError = `old_string not found in ${relPath}${hint}`
370
+ }
371
+ break
372
+ }
373
+ if (tolerant.length > 1) {
374
+ const diag = buildDiag(e)
375
+ const suggestion = ` → Include more surrounding context lines (2-3 lines before and after) in old_string to make the match unique.`
376
+ results.push(
377
+ ` FAIL ${relPath}: old_string appears MULTIPLE times (whitespace-normalized)\n Edit: ${diag}\n${suggestion}`,
378
+ )
379
+ if (!anyFailed) {
380
+ anyFailed = true
381
+ firstError = `old_string appears MULTIPLE times in ${relPath} (whitespace-normalized)`
382
+ }
383
+ break
384
+ }
385
+ matchInfo = {
386
+ index: tolerant[0]!.index,
387
+ length: tolerant[0]!.length,
388
+ strategy: 'tolerant',
389
+ }
390
+ }
391
+
392
+ // For tolerant matching, use the actual file text at the match position
393
+ // so the diff reflects what was really in the file (with original whitespace).
394
+ const matchedOld =
395
+ matchInfo.strategy === 'tolerant'
396
+ ? content.slice(matchInfo.index, matchInfo.index + matchInfo.length)
397
+ : e.old_string
398
+ const diff = generateDiffWithContext(
399
+ content,
400
+ matchedOld,
401
+ e.new_string,
402
+ matchInfo.index,
403
+ matchInfo.length,
404
+ )
405
+ results.push(` Edited ${relPath}:\n${diff}`)
406
+ content =
407
+ content.slice(0, matchInfo.index) +
408
+ e.new_string +
409
+ content.slice(matchInfo.index + matchInfo.length)
410
+ }
411
+
412
+ if (anyFailed) break
413
+ modifiedFiles.set(resolved, content)
414
+ }
415
+
416
+ if (anyFailed) {
417
+ for (const [filePath, group] of fileGroups) {
418
+ const originalContent = group.originalContent
419
+ if (originalContent === null) {
420
+ try {
421
+ fs.unlinkSync(filePath)
422
+ } catch {
423
+ /* ok */
424
+ }
425
+ } else {
426
+ try {
427
+ fs.mkdirSync(path.dirname(filePath), { recursive: true })
428
+ fs.writeFileSync(filePath, originalContent, 'utf-8')
429
+ } catch {
430
+ /* ok */
431
+ }
432
+ }
433
+ applier.markRead(filePath)
434
+ }
435
+ // Only show FAIL lines in the error (successful edits were rolled back)
436
+ const failLines = results.filter(r => r.startsWith(' FAIL ')).join('\n')
437
+ return {
438
+ success: false,
439
+ output: '',
440
+ error: `Edit FAILED — all changes rolled back.\n${failLines}\n\n${firstError}`,
441
+ }
442
+ }
443
+
444
+ for (const [resolved, content] of modifiedFiles) {
445
+ try {
446
+ if (content === null) {
447
+ fs.unlinkSync(resolved)
448
+ } else {
449
+ fs.mkdirSync(path.dirname(resolved), { recursive: true })
450
+ fs.writeFileSync(resolved, content, 'utf-8')
451
+ }
452
+ } catch (err) {
453
+ return {
454
+ success: false,
455
+ output: '',
456
+ error: `Failed to write ${path.relative(cwd, resolved).replace(/\\/g, '/')}: ${fmtErr(err)}. Input: ${summarizeRawInput(rawInput)}`,
457
+ }
458
+ }
459
+ applier.markRead(resolved)
460
+ }
461
+
462
+ return {
463
+ success: true,
464
+ output: results.join('\n').replace(/^ {2}/gm, ''),
465
+ }
466
+ },
467
+ }
468
+ }
@@ -0,0 +1,70 @@
1
+ import type { MatchPos } from './types.js'
2
+
3
+ /**
4
+ * Normalize a line for whitespace-tolerant comparison:
5
+ * 1. Trim leading/trailing whitespace
6
+ * 2. Collapse runs of 2+ spaces/tabs into a single space
7
+ *
8
+ * This handles whitespace differences ANYWHERE on the line:
9
+ * - Leading/trailing spaces → `trim()` removes them
10
+ * - Extra internal spaces → `"foo bar"` → `"foo bar"`
11
+ * - Blank lines with spaces → `" "` → `""`
12
+ */
13
+ /** Export for testing */
14
+ export function normalizeLine(s: string): string {
15
+ return s.trim().replace(/[ \t]{2,}/g, ' ')
16
+ }
17
+
18
+ /**
19
+ * Sliding-window line search that ignores whitespace differences
20
+ * (leading, trailing, and internal runs).
21
+ *
22
+ * Returns all match positions in the ORIGINAL (unnormalized) content,
23
+ * so the caller can do content.slice(match.index, match.index + match.length)
24
+ * to extract the actual matched text (with its original whitespace).
25
+ */
26
+ /** Export for testing */
27
+ export function findAllLinesTolerant(content: string, oldString: string): MatchPos[] {
28
+ if (oldString === '') return []
29
+
30
+ const contentLines = content.split('\n')
31
+ const oldLines = oldString.split('\n')
32
+
33
+ if (oldLines.length > contentLines.length) return []
34
+
35
+ // Pre-normalize for speed
36
+ const normContent = contentLines.map(normalizeLine)
37
+ const normOld = oldLines.map(normalizeLine)
38
+
39
+ const matches: MatchPos[] = []
40
+
41
+ for (let start = 0; start <= normContent.length - normOld.length; start++) {
42
+ let match = true
43
+ for (let j = 0; j < normOld.length; j++) {
44
+ if (normContent[start + j] !== normOld[j]) {
45
+ match = false
46
+ break
47
+ }
48
+ }
49
+ if (match) {
50
+ // Compute byte position in ORIGINAL (unnormalized) content
51
+ let charPos = 0
52
+ for (let k = 0; k < start; k++) {
53
+ charPos += contentLines[k]!.length + 1 // +1 for the \n
54
+ }
55
+ // Compute matched text length in ORIGINAL content
56
+ let matchedLen = 0
57
+ for (let j = 0; j < oldLines.length; j++) {
58
+ const line = contentLines[start + j]!
59
+ let lineLen = line.length
60
+ // CRLF: the last line's trailing \r is part of content but not in oldString
61
+ if (j === oldLines.length - 1 && line.endsWith('\r')) lineLen -= 1
62
+ matchedLen += lineLen
63
+ if (j < oldLines.length - 1) matchedLen += 1 // +1 for the \n
64
+ }
65
+ matches.push({ index: charPos, length: matchedLen })
66
+ }
67
+ }
68
+
69
+ return matches
70
+ }
@@ -0,0 +1,131 @@
1
+ import type { Edit, SingleEdit } from './types.js'
2
+
3
+ /** Build diagnostic JSON for error messages */
4
+ export function buildDiag(edit: SingleEdit): string {
5
+ return JSON.stringify({
6
+ file_path: edit.file_path,
7
+ old_string: edit.old_string,
8
+ new_string: edit.new_string,
9
+ })
10
+ }
11
+
12
+ /** Summarize raw input for error messages to avoid dumping huge strings into the LLM context. */
13
+ export function summarizeRawInput(rawInput: unknown): string {
14
+ const s = JSON.stringify(rawInput)
15
+ if (s.length <= 500) return s
16
+ return `${s.slice(0, 500)}... [truncated, total ${s.length} chars]`
17
+ }
18
+
19
+ /** Export for testing */
20
+ export function parseMarkdownEdit(content: string): Edit[] {
21
+ const edits: Edit[] = []
22
+
23
+ function parseEditBlock(raw: string): Edit | null {
24
+ // Remove stray ``` lines (model may close the block early before new:)
25
+ const cleaned = raw.replace(/^```\s*$/gm, '')
26
+
27
+ const fileMatch = cleaned.match(/^file:\s*(.+)$/m)
28
+ if (!fileMatch) return null
29
+ const filePath = fileMatch[1]!.trim()
30
+
31
+ let oldString = ''
32
+ let newString = ''
33
+
34
+ const oldMatch = cleaned.match(/^old:(?:\s*\|\s*[\d\-+]*\r?\n)?([\s\S]*?)^new:/m)
35
+ const newMatch = cleaned.match(/^new:(?:\s*\|\s*[\d\-+]*\r?\n)?([\s\S]*)$/m)
36
+
37
+ if (oldMatch) {
38
+ oldString = oldMatch[1]!.replace(/^\n+/, '').replace(/\n+$/, '')
39
+ }
40
+ if (newMatch) {
41
+ newString = newMatch[1]!.replace(/^\n+/, '').replace(/\n+$/, '')
42
+ }
43
+
44
+ return { file_path: filePath, old_string: oldString || '', new_string: newString || '' }
45
+ }
46
+
47
+ // Strategy 1: Non-greedy block regex (handles multi-edit, correct formatting)
48
+ const blockRegex = /```edit\s*([\s\S]*?)```/gi
49
+ for (const regexMatch of content.matchAll(blockRegex)) {
50
+ const edit = parseEditBlock(regexMatch[1]!)
51
+ if (edit) edits.push(edit)
52
+ }
53
+
54
+ // Strategy 2: If no edit with new_string found (model likely closed ``` before new:),
55
+ // retry with greedy matching to capture everything up to the last ```
56
+ if (!edits.some(e => e.new_string)) {
57
+ edits.length = 0
58
+ const greedyRegex = /```edit\s*([\s\S]*)```/g
59
+ for (const regexMatch of content.matchAll(greedyRegex)) {
60
+ const edit = parseEditBlock(regexMatch[1]!)
61
+ if (edit) edits.push(edit)
62
+ }
63
+ }
64
+
65
+ // Strategy 3: No block markers at all — try parsing raw content directly
66
+ if (edits.length === 0) {
67
+ const edit = parseEditBlock(content)
68
+ if (edit) edits.push(edit)
69
+ }
70
+
71
+ return edits
72
+ }
73
+
74
+ /** Extract edits from legacy JSON format (backward compatibility) */
75
+ export function extractEditsFromJSON(input: Record<string, unknown>): Edit[] {
76
+ // Pattern 0: input is an array (edits passed directly instead of wrapped)
77
+ if (Array.isArray(input)) {
78
+ // Preserve array for validation
79
+ return input as Edit[]
80
+ }
81
+
82
+ // Pattern 1: input has file_path, old_string, new_string at top level (missing edits array)
83
+ if (!Array.isArray(input.edits)) {
84
+ const keys = Object.keys(input)
85
+
86
+ // Check if the keys look like a single edit object (file_path + old_string + new_string)
87
+ const hasFilePath = typeof input.file_path === 'string'
88
+ const hasOldString = typeof input.old_string === 'string'
89
+ const hasNewString = typeof input.new_string === 'string'
90
+
91
+ if (hasFilePath && hasOldString && hasNewString) {
92
+ return [
93
+ {
94
+ file_path: input.file_path as string,
95
+ old_string: input.old_string as string,
96
+ new_string: input.new_string as string,
97
+ },
98
+ ]
99
+ } else if (hasFilePath && hasOldString) {
100
+ // Only file_path + old_string (missing new_string) — use a sentinel
101
+ // value so validation catches this as an error instead of silently deleting content.
102
+ return [
103
+ {
104
+ file_path: input.file_path as string,
105
+ old_string: input.old_string as string,
106
+ new_string: (input.new_string as string) || '__MISSING_NEW_STRING__',
107
+ },
108
+ ]
109
+ } else if (keys.length === 2 && hasFilePath && typeof input.new_string === 'string') {
110
+ // file_path + new_string but no old_string — treat as new file creation
111
+ return [
112
+ {
113
+ file_path: input.file_path as string,
114
+ old_string: '',
115
+ new_string: input.new_string as string,
116
+ },
117
+ ]
118
+ } else if (keys.length === 1 && hasFilePath) {
119
+ // Only file_path — maybe they meant create file with empty content?
120
+ return [{ file_path: input.file_path as string, old_string: '', new_string: '' }]
121
+ }
122
+ return []
123
+ }
124
+
125
+ // If edits is an array (even empty), preserve for validation
126
+ if (Array.isArray(input.edits)) {
127
+ return input.edits as Edit[]
128
+ }
129
+
130
+ return []
131
+ }
@@ -0,0 +1,19 @@
1
+ export type DiffLineType = 'delete' | 'insert' | 'equal'
2
+
3
+ export interface DiffLine {
4
+ type: DiffLineType
5
+ content: string
6
+ }
7
+
8
+ export interface MatchPos {
9
+ index: number
10
+ length: number
11
+ }
12
+
13
+ export interface SingleEdit {
14
+ file_path: string
15
+ old_string: string
16
+ new_string: string
17
+ }
18
+
19
+ export type Edit = SingleEdit