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
@@ -1,13 +1,16 @@
1
1
  import * as fs from 'node:fs';
2
2
  import * as path from 'node:path';
3
+ import { DIFF_DELETE, DIFF_INSERT, diffLinesRaw } from 'jest-diff';
3
4
  import { fmtErr } from './errors.js';
4
5
  // ── ANSI colors for terminal output ───────────────────────────────────────
5
6
  const DIFF_RED = '\x1b[38;2;255;80;80m';
6
7
  const DIFF_GREEN = '\x1b[38;2;0;200;100m';
8
+ const DIFF_DIM = '\x1b[38;2;100;100;100m';
7
9
  const DIFF_RESET = '\x1b[0m';
8
- // ── ANSI colors for HTML output ───────────────────────────────────────────
10
+ // ── Colors for HTML output ────────────────────────────────────────────────
9
11
  const HTML_RED = '#ff5050';
10
12
  const HTML_GREEN = '#00c864';
13
+ const HTML_DIM = '#888888';
11
14
  /** Build diagnostic JSON for error messages */
12
15
  function buildDiag(edit) {
13
16
  return JSON.stringify({
@@ -16,31 +19,50 @@ function buildDiag(edit) {
16
19
  new_string: edit.new_string,
17
20
  });
18
21
  }
19
- /** Compute diff lines (pure data, no rendering) */
20
- export function computeDiff(oldStr, newStr, startLine = 0) {
22
+ /** Summarize raw input for error messages to avoid dumping huge strings into the LLM context. */
23
+ function summarizeRawInput(rawInput) {
24
+ const s = JSON.stringify(rawInput);
25
+ if (s.length <= 500)
26
+ return s;
27
+ return `${s.slice(0, 500)}... [truncated, total ${s.length} chars]`;
28
+ }
29
+ /** Compute diff lines using jest-diff for proper line-level diffs */
30
+ export function computeDiff(oldStr, newStr) {
21
31
  const oldLines = oldStr === '' ? [] : oldStr.split('\n');
22
- const newLines = newStr.split('\n');
23
- const lines = [];
24
- for (let i = 0; i < oldLines.length; i++) {
25
- lines.push({ lineNum: startLine + i, type: 'old', content: oldLines[i] });
26
- }
27
- for (let i = 0; i < newLines.length; i++) {
28
- lines.push({ lineNum: startLine + i, type: 'new', content: newLines[i] });
29
- }
30
- return lines;
32
+ const newLines = newStr === '' ? [] : newStr.split('\n');
33
+ if (oldLines.length === 0 && newLines.length === 0)
34
+ return [];
35
+ const rawDiff = diffLinesRaw(oldLines, newLines);
36
+ return rawDiff.map(d => ({
37
+ type: d[0] === DIFF_DELETE
38
+ ? 'delete'
39
+ : d[0] === DIFF_INSERT
40
+ ? 'insert'
41
+ : 'equal',
42
+ content: d[1],
43
+ }));
31
44
  }
32
- /** Terminal renderer */
33
- export function renderDiffTerminal(lines) {
45
+ /** Terminal renderer — unified-diff format with color */
46
+ export function renderDiffTerminal(lines, startLineNum) {
34
47
  if (lines.length === 0)
35
48
  return '';
36
- const maxLineNum = Math.max(...lines.map(l => l.lineNum));
37
- const lineNumWidth = String(maxLineNum).length;
38
- const fmtLineNum = (n) => String(n).padStart(lineNumWidth, ' ');
39
49
  const output = [];
50
+ let oldLn = startLineNum ?? 1;
51
+ let newLn = startLineNum ?? 1;
40
52
  for (const line of lines) {
41
- const color = line.type === 'old' ? DIFF_RED : DIFF_GREEN;
42
- const reset = DIFF_RESET;
43
- output.push(` ${fmtLineNum(line.lineNum)} ${color}${line.content}${reset}`);
53
+ if (line.type === 'delete') {
54
+ output.push(` ${DIFF_RED}- ${oldLn} ${line.content}${DIFF_RESET}`);
55
+ oldLn++;
56
+ }
57
+ else if (line.type === 'insert') {
58
+ output.push(` ${DIFF_GREEN}+ ${newLn} ${line.content}${DIFF_RESET}`);
59
+ newLn++;
60
+ }
61
+ else {
62
+ output.push(` ${DIFF_DIM} ${oldLn} ${line.content}${DIFF_RESET}`);
63
+ oldLn++;
64
+ newLn++;
65
+ }
44
66
  }
45
67
  return output.join('\n');
46
68
  }
@@ -48,32 +70,94 @@ export function renderDiffTerminal(lines) {
48
70
  export function renderDiffHtml(lines) {
49
71
  if (lines.length === 0)
50
72
  return '';
51
- const maxLineNum = Math.max(...lines.map(l => l.lineNum));
52
- const lineNumWidth = String(maxLineNum).length;
53
- const fmtLineNum = (n) => String(n).padStart(lineNumWidth, ' ');
54
73
  const output = [];
55
74
  for (const line of lines) {
56
- const color = line.type === 'old' ? HTML_RED : HTML_GREEN;
57
- const style = `color: ${color};`;
58
- output.push(` <span style="${style}">${fmtLineNum(line.lineNum)} ${escapeHtml(line.content)}</span>`);
75
+ if (line.type === 'delete') {
76
+ output.push(` <span style="color: ${HTML_RED};">- ${escapeHtml(line.content)}</span>`);
77
+ }
78
+ else if (line.type === 'insert') {
79
+ output.push(` <span style="color: ${HTML_GREEN};">+ ${escapeHtml(line.content)}</span>`);
80
+ }
81
+ else {
82
+ output.push(` <span style="color: ${HTML_DIM};"> ${escapeHtml(line.content)}</span>`);
83
+ }
59
84
  }
60
85
  return output.join('\n');
61
86
  }
62
87
  /** Escape HTML special characters */
63
- function escapeHtml(str) {
88
+ export function escapeHtml(str) {
64
89
  return str
65
90
  .replace(/&/g, '&amp;')
66
91
  .replace(/</g, '&lt;')
67
92
  .replace(/>/g, '&gt;')
68
- .replace(/"/g, '&quot;');
93
+ .replace(/"/g, '&quot;')
94
+ .replace(/'/g, '&#39;');
69
95
  }
70
96
  /**
71
- * Generate diff output (backward compatible).
72
- * Delegates to terminal renderer by default.
97
+ * Generate diff output using jest-diff.
98
+ * Returns terminal-colored unified-diff format with line numbers.
73
99
  */
74
- export function generateDiff(oldStr, newStr, startLine = 0) {
75
- const lines = computeDiff(oldStr, newStr, startLine);
76
- return renderDiffTerminal(lines);
100
+ export function generateDiff(oldStr, newStr, startLineNum) {
101
+ const lines = computeDiff(oldStr, newStr);
102
+ return renderDiffTerminal(lines, startLineNum);
103
+ }
104
+ /**
105
+ * Generate diff with surrounding context lines (1 before, 1 after)
106
+ * and line numbers. Context lines are extracted from the full file content.
107
+ */
108
+ export function generateDiffWithContext(fullContent, oldStr, newStr, matchIndex, matchLength) {
109
+ // Calculate 1-based line number of the first matched line
110
+ const firstLineNum = fullContent.slice(0, matchIndex).split('\n').length;
111
+ const contentLines = fullContent.split('\n');
112
+ // Find 0-based line index where matchIndex falls
113
+ let charPos = 0;
114
+ let matchStartLineIdx = 0;
115
+ for (let i = 0; i < contentLines.length; i++) {
116
+ if (charPos <= matchIndex && matchIndex < charPos + contentLines[i].length + 1) {
117
+ matchStartLineIdx = i;
118
+ break;
119
+ }
120
+ charPos += contentLines[i].length + 1; // +1 for \n
121
+ }
122
+ // How many lines does oldStr span in the original content?
123
+ const oldLines = oldStr === '' ? [] : oldStr.split('\n');
124
+ const oldLineCount = oldLines.length;
125
+ const matchEndLineIdx = matchStartLineIdx + oldLineCount - 1;
126
+ // Context before (1 line) and after (1 line)
127
+ const beforeLineIdx = matchStartLineIdx > 0 ? matchStartLineIdx - 1 : -1;
128
+ const afterLineIdx = matchEndLineIdx < contentLines.length - 1 ? matchEndLineIdx + 1 : -1;
129
+ // Compute diff between oldStr and newStr
130
+ const diffLines = computeDiff(oldStr, newStr);
131
+ const output = [];
132
+ // Context before
133
+ if (beforeLineIdx !== -1) {
134
+ const lineNum = beforeLineIdx + 1;
135
+ output.push(` ${DIFF_DIM} ${lineNum} ${contentLines[beforeLineIdx]}${DIFF_RESET}`);
136
+ }
137
+ // Diff lines with line numbers
138
+ let oldLn = firstLineNum;
139
+ let newLn = firstLineNum;
140
+ for (const line of diffLines) {
141
+ if (line.type === 'delete') {
142
+ output.push(` ${DIFF_RED}- ${oldLn} ${line.content}${DIFF_RESET}`);
143
+ oldLn++;
144
+ }
145
+ else if (line.type === 'insert') {
146
+ output.push(` ${DIFF_GREEN}+ ${newLn} ${line.content}${DIFF_RESET}`);
147
+ newLn++;
148
+ }
149
+ else {
150
+ output.push(` ${DIFF_DIM} ${oldLn} ${line.content}${DIFF_RESET}`);
151
+ oldLn++;
152
+ newLn++;
153
+ }
154
+ }
155
+ // Context after
156
+ if (afterLineIdx !== -1) {
157
+ const lineNum = afterLineIdx + 1;
158
+ output.push(` ${DIFF_DIM} ${lineNum} ${contentLines[afterLineIdx]}${DIFF_RESET}`);
159
+ }
160
+ return output.join('\n');
77
161
  }
78
162
  /**
79
163
  * Normalize a line for whitespace-tolerant comparison:
@@ -85,7 +169,8 @@ export function generateDiff(oldStr, newStr, startLine = 0) {
85
169
  * - Extra internal spaces → `"foo bar"` → `"foo bar"`
86
170
  * - Blank lines with spaces → `" "` → `""`
87
171
  */
88
- function normalizeLine(s) {
172
+ /** Export for testing */
173
+ export function normalizeLine(s) {
89
174
  return s.trim().replace(/[ \t]{2,}/g, ' ');
90
175
  }
91
176
  /**
@@ -96,7 +181,8 @@ function normalizeLine(s) {
96
181
  * so the caller can do content.slice(match.index, match.index + match.length)
97
182
  * to extract the actual matched text (with its original whitespace).
98
183
  */
99
- function findAllLinesTolerant(content, oldString) {
184
+ /** Export for testing */
185
+ export function findAllLinesTolerant(content, oldString) {
100
186
  if (oldString === '')
101
187
  return [];
102
188
  const contentLines = content.split('\n');
@@ -124,7 +210,12 @@ function findAllLinesTolerant(content, oldString) {
124
210
  // Compute matched text length in ORIGINAL content
125
211
  let matchedLen = 0;
126
212
  for (let j = 0; j < oldLines.length; j++) {
127
- matchedLen += contentLines[start + j].length;
213
+ const line = contentLines[start + j];
214
+ let lineLen = line.length;
215
+ // CRLF: the last line's trailing \r is part of content but not in oldString
216
+ if (j === oldLines.length - 1 && line.endsWith('\r'))
217
+ lineLen -= 1;
218
+ matchedLen += lineLen;
128
219
  if (j < oldLines.length - 1)
129
220
  matchedLen += 1; // +1 for the \n
130
221
  }
@@ -133,142 +224,222 @@ function findAllLinesTolerant(content, oldString) {
133
224
  }
134
225
  return matches;
135
226
  }
227
+ /** Export for testing */
228
+ export function parseMarkdownEdit(content) {
229
+ const edits = [];
230
+ function parseEditBlock(raw) {
231
+ // Remove stray ``` lines (model may close the block early before new:)
232
+ const cleaned = raw.replace(/^```\s*$/gm, '');
233
+ const fileMatch = cleaned.match(/^file:\s*(.+)$/m);
234
+ if (!fileMatch)
235
+ return null;
236
+ const filePath = fileMatch[1].trim();
237
+ let oldString = '';
238
+ let newString = '';
239
+ const oldMatch = cleaned.match(/^old:(?:\s*\|\d*\s*\n)?([\s\S]*?)^new:/m);
240
+ const newMatch = cleaned.match(/^new:(?:\s*\|\d*\s*\n)?([\s\S]*)$/m);
241
+ if (oldMatch) {
242
+ oldString = oldMatch[1].replace(/^\n+/, '').replace(/\n+$/, '');
243
+ }
244
+ if (newMatch) {
245
+ newString = newMatch[1].replace(/^\n+/, '').replace(/\n+$/, '');
246
+ }
247
+ return { file_path: filePath, old_string: oldString || '', new_string: newString || '' };
248
+ }
249
+ // Strategy 1: Non-greedy block regex (handles multi-edit, correct formatting)
250
+ const blockRegex = /```edit\s*([\s\S]*?)```/gi;
251
+ for (const regexMatch of content.matchAll(blockRegex)) {
252
+ const edit = parseEditBlock(regexMatch[1]);
253
+ if (edit)
254
+ edits.push(edit);
255
+ }
256
+ // Strategy 2: If no edit with new_string found (model likely closed ``` before new:),
257
+ // retry with greedy matching to capture everything up to the last ```
258
+ if (!edits.some(e => e.new_string)) {
259
+ edits.length = 0;
260
+ const greedyRegex = /```edit\s*([\s\S]*)```/g;
261
+ for (const regexMatch of content.matchAll(greedyRegex)) {
262
+ const edit = parseEditBlock(regexMatch[1]);
263
+ if (edit)
264
+ edits.push(edit);
265
+ }
266
+ }
267
+ // Strategy 3: No block markers at all — try parsing raw content directly
268
+ if (edits.length === 0) {
269
+ const edit = parseEditBlock(content);
270
+ if (edit)
271
+ edits.push(edit);
272
+ }
273
+ return edits;
274
+ }
275
+ /** Extract edits from legacy JSON format (backward compatibility) */
276
+ function extractEditsFromJSON(input) {
277
+ // Pattern 0: input is an array (edits passed directly instead of wrapped)
278
+ if (Array.isArray(input)) {
279
+ // Preserve array for validation
280
+ return input;
281
+ }
282
+ // Pattern 1: input has file_path, old_string, new_string at top level (missing edits array)
283
+ if (!Array.isArray(input.edits)) {
284
+ const keys = Object.keys(input);
285
+ // Check if the keys look like a single edit object (file_path + old_string + new_string)
286
+ const hasFilePath = typeof input.file_path === 'string';
287
+ const hasOldString = typeof input.old_string === 'string';
288
+ const hasNewString = typeof input.new_string === 'string';
289
+ if (hasFilePath && hasOldString && hasNewString) {
290
+ return [
291
+ {
292
+ file_path: input.file_path,
293
+ old_string: input.old_string,
294
+ new_string: input.new_string,
295
+ },
296
+ ];
297
+ }
298
+ else if (hasFilePath && hasOldString) {
299
+ // Only file_path + old_string (missing new_string) — use a sentinel
300
+ // value so validation catches this as an error instead of silently deleting content.
301
+ return [
302
+ {
303
+ file_path: input.file_path,
304
+ old_string: input.old_string,
305
+ new_string: input.new_string || '__MISSING_NEW_STRING__',
306
+ },
307
+ ];
308
+ }
309
+ else if (keys.length === 2 && hasFilePath && typeof input.new_string === 'string') {
310
+ // file_path + new_string but no old_string — treat as new file creation
311
+ return [
312
+ {
313
+ file_path: input.file_path,
314
+ old_string: '',
315
+ new_string: input.new_string,
316
+ },
317
+ ];
318
+ }
319
+ else if (keys.length === 1 && hasFilePath) {
320
+ // Only file_path — maybe they meant create file with empty content?
321
+ return [{ file_path: input.file_path, old_string: '', new_string: '' }];
322
+ }
323
+ return [];
324
+ }
325
+ // If edits is an array (even empty), preserve for validation
326
+ if (Array.isArray(input.edits)) {
327
+ return input.edits;
328
+ }
329
+ return [];
330
+ }
136
331
  export function createEditTool(applier, cwd) {
137
332
  return {
138
333
  definition: {
139
334
  name: 'edit',
140
- description: `Replace exact text in files. Each edit object is a COMPLETE find-and-replace pair.
141
- Parameter: {"edits": [{"file_path": "...", "old_string": "...", "new_string": "..."}]}
335
+ category: 'Edit',
336
+ group: 'File',
337
+ description: `Replace exact text in files using markdown code block format.
142
338
 
143
339
  HOW TO USE:
144
340
  1. Read the file first with \`read\`
145
341
  2. Copy the EXACT text to replace — include 2-3 lines of context before/after
146
- 3. Call: edit({ edits: [{ file_path, old_string, new_string }] })
342
+ 3. Use markdown code block format below
147
343
 
148
- CRITICAL RULES:
149
- - Each edit MUST have BOTH old_string AND new_string (a complete find-replace pair)
150
- - old_string must match EXACTLY (whitespace, indentation, line breaks)
151
- - old_string must be UNIQUE — include enough surrounding context
152
- - Do NOT include the "<lineNumber>: " prefix from read output
153
- - \`edits\` is always an array. Never pass file_path/old_string/new_string as top-level keys.
154
- - To batch multiple independent edits, add multiple objects to the array
344
+ FORMAT:
345
+ \`\`\`edit
346
+ file: <file_path>
347
+ old: |
348
+ <exact text to find>
349
+ new: |
350
+ <replacement text>
351
+ \`\`\`
155
352
 
156
353
  EXAMPLES:
157
- Single edit: edit({ edits: [{ file_path: "src/config.ts", old_string: "mode: 'code'", new_string: "mode: 'plan'" }] })
354
+ Single edit:
355
+ \`\`\`edit
356
+ file: src/config.ts
357
+ old: |
358
+ mode: 'code'
359
+ new: |
360
+ mode: 'plan'
361
+ \`\`\`
158
362
 
159
- Batch edits: edit({ edits: [
160
- { file_path: "src/a.ts", old_string: "foo", new_string: "bar" },
161
- { file_path: "src/b.ts", old_string: "x", new_string: "y" }
162
- ] })
363
+ Create new file:
364
+ \`\`\`edit
365
+ file: src/new.ts
366
+ old: |
367
+ new: |
368
+ const x = 1
369
+ \`\`\`
163
370
 
164
- Create file: edit({ edits: [{ file_path: "src/new.ts", old_string: "", new_string: "const x = 1" }] })`,
371
+ CRITICAL RULES:
372
+ - old and new are separated by "old:" and "new:" labels
373
+ - Use | after label for multi-line content
374
+ - old_string must match EXACTLY (whitespace, indentation, line breaks)
375
+ - Do NOT include the "<lineNumber>: " prefix from read output`,
165
376
  parameters: {
166
- edits: {
167
- type: 'array',
168
- minItems: 1,
169
- description: 'REQUIRED. Array of complete find-replace pairs. Each edit MUST have BOTH old_string AND new_string. Example: [{ file_path: "src/file.ts", old_string: "old", new_string: "new" }]',
377
+ content: {
378
+ type: 'string',
379
+ description: 'Markdown code block with edit instructions. See description for format.',
170
380
  required: true,
171
- items: {
172
- type: 'object',
173
- properties: {
174
- file_path: { type: 'string', description: 'Path to the file' },
175
- old_string: {
176
- type: 'string',
177
- description: 'Text to FIND in the file. REQUIRED. Pair with new_string to form a complete find-replace. Pass "" to create a new file.',
178
- },
179
- new_string: {
180
- type: 'string',
181
- description: 'Replacement text. REQUIRED. Pair with old_string to form a complete find-replace. Pass "" to delete content.',
182
- },
183
- },
184
- },
185
381
  },
186
382
  },
187
383
  },
188
384
  async execute(input) {
189
- // ── Auto-correction: detect common misuse patterns ────────────────
190
- // ── Debug: log raw input for diagnosing failures ─────────────
191
- let rawInput = input;
192
- try {
193
- rawInput = structuredClone(input);
194
- }
195
- catch {
196
- console.error('[edit] Failed to clone input:', input);
197
- rawInput = input;
198
- }
199
- // Pattern 0: input is an array (edits passed directly instead of wrapped)
200
- if (Array.isArray(input)) {
201
- input = { edits: input };
202
- }
203
- // Pattern 1: input has file_path, old_string, new_string at top level (missing edits array)
204
- if (!Array.isArray(input.edits)) {
205
- const keys = Object.keys(input);
206
- // Check if the keys look like a single edit object (file_path + old_string + new_string)
207
- const hasFilePath = typeof input.file_path === 'string';
208
- const hasOldString = typeof input.old_string === 'string';
209
- const hasNewString = typeof input.new_string === 'string';
210
- if (hasFilePath && hasOldString && hasNewString) {
211
- // Auto-correct: wrap into edits array
212
- input = {
213
- edits: [
214
- {
215
- file_path: input.file_path,
216
- old_string: input.old_string,
217
- new_string: input.new_string,
218
- },
219
- ],
220
- };
385
+ // Debug: keep rawInput for error messages (capture early for all error paths)
386
+ const rawInput = input;
387
+ // ── Parse markdown format ─────────────────────────────────────────
388
+ let edits = [];
389
+ // If input has 'content' field (new markdown format)
390
+ if (typeof input.content === 'string') {
391
+ // Compat: handle double-JSON-wrapped content
392
+ let contentStr = input.content;
393
+ try {
394
+ const parsed = JSON.parse(contentStr);
395
+ if (typeof parsed === 'string') {
396
+ contentStr = parsed;
397
+ }
398
+ else if (parsed && typeof parsed === 'object' && typeof parsed.content === 'string') {
399
+ contentStr = parsed.content;
400
+ }
221
401
  }
222
- else if (hasFilePath && hasOldString) {
223
- // Only file_path + old_string (missing new_string) still try
224
- input = {
225
- edits: [
226
- {
227
- file_path: input.file_path,
228
- old_string: input.old_string,
229
- new_string: input.new_string || '',
230
- },
231
- ],
232
- };
402
+ catch {
403
+ // Not JSONuse as-is
233
404
  }
234
- else if (keys.length === 1 && hasFilePath) {
235
- // Only file_path maybe they meant create file with empty content?
236
- input = { edits: [{ file_path: input.file_path, old_string: '', new_string: '' }] };
405
+ edits = parseMarkdownEdit(contentStr);
406
+ if (edits.length === 0) {
407
+ return {
408
+ success: false,
409
+ output: '',
410
+ error: `Failed to parse edit format. Raw input: ${summarizeRawInput(rawInput)}\nUse: \`\`\`edit\\nfile: path\\nold: |\\ntext\\nnew: |\\ntext\\n\`\`\``,
411
+ };
237
412
  }
238
- else if (keys.length === 2 && hasFilePath && typeof input.new_string === 'string') {
239
- // file_path + new_string but no old_string — treat as new file creation
240
- input = {
241
- edits: [{ file_path: input.file_path, old_string: '', new_string: input.new_string }],
413
+ }
414
+ else {
415
+ // Legacy JSON format (backward compatibility)
416
+ edits = extractEditsFromJSON(input);
417
+ }
418
+ if (edits.length === 0) {
419
+ // Check if input specifically had empty edits array (for better error message)
420
+ const inputEdits = input.edits;
421
+ if ('edits' in input && Array.isArray(inputEdits) && inputEdits.length === 0) {
422
+ return {
423
+ success: false,
424
+ output: '',
425
+ error: `edit FAILED — no edits to apply. The edits array exists but is empty. Raw input: ${summarizeRawInput(rawInput)}`,
242
426
  };
243
427
  }
244
- else {
245
- // Can't auto-correct give helpful error with examples
246
- const example = hasFilePath
247
- ? `edit({ edits: [{ file_path: "${input.file_path}", old_string: "...", new_string: "..." }] })`
248
- : `edit({ edits: [{ file_path: "src/file.ts", old_string: "old", new_string: "new" }] })`;
428
+ // Check if input has edits key but it's not an array
429
+ if ('edits' in input && !Array.isArray(input.edits)) {
249
430
  return {
250
431
  success: false,
251
432
  output: '',
252
- error: `edit requires "edits" array. Received: ${JSON.stringify(rawInput)}. Usage: ${example}`,
433
+ error: `edit requires "edits" array. Use markdown format: \`\`\`edit\\nfile: path\\nold: |\\ntext\\nnew: |\\ntext\\n\`\`\`\nRaw input: ${summarizeRawInput(rawInput)}`,
253
434
  };
254
435
  }
255
- }
256
- const edits = input.edits;
257
- if (edits.length === 0) {
258
- // Build a diagnostic: show what the AI received vs expected
259
- const receivedKeys = Object.keys(rawInput);
260
- const hint = receivedKeys.length === 1 && receivedKeys[0] === 'edits'
261
- ? 'The edits array exists but is empty — include at least one edit object with file_path, old_string, and new_string.'
262
- : receivedKeys.includes('file_path')
263
- ? 'Top-level file_path/old_string/new_string detected — wrap them in an edits array: { edits: [{ file_path, old_string, new_string }] }'
264
- : 'No usable edit data found in the input. Provide an edits array with at least one edit.';
265
436
  return {
266
437
  success: false,
267
438
  output: '',
268
- error: `edit FAILED no edits to apply. Raw input: ${JSON.stringify(rawInput)}. ${hint}`,
439
+ 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)}`,
269
440
  };
270
441
  }
271
- // Validate each edit object — catch missing old_string/new_string early
442
+ // Validate each edit object
272
443
  const editErrors = [];
273
444
  for (let i = 0; i < edits.length; i++) {
274
445
  const e = edits[i];
@@ -279,18 +450,81 @@ EXAMPLES:
279
450
  missing.push('old_string');
280
451
  if (typeof e.new_string !== 'string')
281
452
  missing.push('new_string');
453
+ if (e.new_string === '__MISSING_NEW_STRING__') {
454
+ missing.push('new_string (required — LLM must provide a non-empty replacement)');
455
+ }
282
456
  if (missing.length > 0) {
283
- editErrors.push(` edit #${i + 1}: missing ${missing.join(', ')} (has: ${Object.keys(e).join(', ')})`);
457
+ const present = Object.keys(e)
458
+ .filter(k => typeof e[k] === 'string')
459
+ .join(', ');
460
+ editErrors.push(` edit #${i + 1}: missing ${missing.join(', ')}${present ? ` (has: ${present})` : ''}`);
284
461
  }
285
462
  }
286
463
  if (editErrors.length > 0) {
287
464
  return {
288
465
  success: false,
289
466
  output: '',
290
- error: `edit FAILED — ${editErrors.length} of ${edits.length} edit(s) have missing fields.\n${editErrors.join('\n')}\n\nReceived: ${JSON.stringify(rawInput)}\n\nEach edit object must be a COMPLETE find-replace pair with BOTH old_string AND new_string.
291
- Do NOT split old_string (text to find) and new_string (replacement) across different edit objects.`,
467
+ 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.`,
292
468
  };
293
469
  }
470
+ // ── Path traversal security check (symlink-aware) ─────────────────
471
+ const resolvedCwd = path.resolve(cwd);
472
+ let realCwd;
473
+ try {
474
+ realCwd = fs.realpathSync(resolvedCwd);
475
+ }
476
+ catch {
477
+ realCwd = resolvedCwd;
478
+ }
479
+ for (let i = 0; i < edits.length; i++) {
480
+ const e = edits[i];
481
+ const resolved = path.resolve(cwd, e.file_path);
482
+ const relative = path.relative(resolvedCwd, resolved);
483
+ if (relative.startsWith('..') || path.isAbsolute(relative)) {
484
+ return {
485
+ success: false,
486
+ output: '',
487
+ error: 'edit FAILED — Path traversal detected: "' +
488
+ e.file_path +
489
+ '" resolves outside the working directory "' +
490
+ resolvedCwd +
491
+ '". All file paths must be within the project directory.',
492
+ };
493
+ }
494
+ // Symlink-aware check: resolve real path to detect traversal via symlink
495
+ let realPath;
496
+ try {
497
+ realPath = fs.realpathSync(resolved);
498
+ }
499
+ catch {
500
+ // File doesn't exist yet – resolve nearest existing ancestor through symlinks
501
+ let dir = path.dirname(resolved);
502
+ while (!fs.existsSync(dir)) {
503
+ const parent = path.dirname(dir);
504
+ if (parent === dir)
505
+ break; // root
506
+ dir = parent;
507
+ }
508
+ try {
509
+ realPath = path.join(fs.realpathSync(dir), path.relative(dir, resolved));
510
+ }
511
+ catch {
512
+ realPath = resolved;
513
+ }
514
+ }
515
+ const realRelative = path.relative(realCwd, realPath);
516
+ if (realRelative.startsWith('..') || path.isAbsolute(realRelative)) {
517
+ return {
518
+ success: false,
519
+ output: '',
520
+ error: 'edit FAILED — Path traversal detected via symlink: "' +
521
+ e.file_path +
522
+ '" resolves outside "' +
523
+ realCwd +
524
+ '".',
525
+ };
526
+ }
527
+ }
294
528
  const fileGroups = new Map();
295
529
  for (const e of edits) {
296
530
  const resolved = path.resolve(cwd, e.file_path);
@@ -322,26 +556,24 @@ Do NOT split old_string (text to find) and new_string (replacement) across diffe
322
556
  e.new_string = e.new_string.replace(/\r\n/g, '\n');
323
557
  if (e.old_string === '') {
324
558
  if (content !== null) {
325
- const diag = buildDiag({ ...e, old_string: '' });
326
- const suggestion = ` → Use edit with old_string to replace existing content, or choose a different path.`;
327
- results.push(` FAIL ${relPath}: File already exists Raw input: ${JSON.stringify(rawInput)}
328
- Edit: ${diag}
329
- ${suggestion}`);
559
+ // File already exists in-memory (from prior edit in this batch)
560
+ // or on disk treat as error to prevent silent overwrites.
561
+ results.push(` FAIL ${relPath}: Cannot create — file already exists (duplicate create in batch or file on disk)`);
330
562
  if (!anyFailed) {
331
563
  anyFailed = true;
332
- firstError = `File already exists: ${relPath}`;
564
+ firstError = `Cannot create file "${relPath}": file already exists`;
333
565
  }
334
566
  break;
335
567
  }
336
568
  const newLines = e.new_string.split('\n').length;
337
569
  content = e.new_string;
338
- results.push(` Created ${relPath} (${newLines} lines):\n${generateDiff('', e.new_string)}`);
570
+ results.push(` Created ${relPath} (${newLines} lines):\n${generateDiff('', e.new_string, 1)}`);
339
571
  continue;
340
572
  }
341
573
  if (content === null) {
342
574
  const diag = buildDiag(e);
343
575
  const suggestion = ` → Check the file path, or create it first with old_string: "" (empty string).`;
344
- results.push(` FAIL ${relPath}: File not found — Raw input: ${JSON.stringify(rawInput)}
576
+ results.push(` FAIL ${relPath}: File not found
345
577
  Edit: ${diag}
346
578
  ${suggestion}`);
347
579
  if (!anyFailed) {
@@ -359,7 +591,7 @@ ${suggestion}`);
359
591
  if (exactIdx !== lastIdx) {
360
592
  const diag = buildDiag(e);
361
593
  const suggestion = ` → Include more surrounding context lines (2-3 lines before and after) in old_string to make the match unique.`;
362
- results.push(` FAIL ${relPath}: old_string appears MULTIPLE times — Raw input: ${JSON.stringify(rawInput)}
594
+ results.push(` FAIL ${relPath}: old_string appears MULTIPLE times
363
595
  Edit: ${diag}
364
596
  ${suggestion}`);
365
597
  if (!anyFailed) {
@@ -397,8 +629,8 @@ ${suggestion}`);
397
629
  const diag = buildDiag(e);
398
630
  const readHint = readWarning ? `\n ${readWarning}` : '';
399
631
  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}`;
400
- results.push(` FAIL ${relPath}: old_string not found — Raw input: ${JSON.stringify(rawInput)}
401
- Edit: ${diag}${hint}
632
+ results.push(` FAIL ${relPath}: old_string not found${hint}
633
+ Edit: ${diag}
402
634
  ${suggestion}`);
403
635
  if (!anyFailed) {
404
636
  anyFailed = true;
@@ -409,7 +641,7 @@ ${suggestion}`);
409
641
  if (tolerant.length > 1) {
410
642
  const diag = buildDiag(e);
411
643
  const suggestion = ` → Include more surrounding context lines (2-3 lines before and after) in old_string to make the match unique.`;
412
- results.push(` FAIL ${relPath}: old_string appears MULTIPLE times (whitespace-normalized) — Raw input: ${JSON.stringify(rawInput)}\n Edit: ${diag}\n${suggestion}`);
644
+ results.push(` FAIL ${relPath}: old_string appears MULTIPLE times (whitespace-normalized)\n Edit: ${diag}\n${suggestion}`);
413
645
  if (!anyFailed) {
414
646
  anyFailed = true;
415
647
  firstError = `old_string appears MULTIPLE times in ${relPath} (whitespace-normalized)`;
@@ -422,10 +654,13 @@ ${suggestion}`);
422
654
  strategy: 'tolerant',
423
655
  };
424
656
  }
425
- const strategyLabel = matchInfo.strategy === 'tolerant' ? ' (whitespace-normalized)' : '';
426
- const matchLineNum = content.substring(0, matchInfo.index).split('\n').length;
427
- const diff = generateDiff(e.old_string, e.new_string, matchLineNum - 1);
428
- results.push(` Edited ${relPath}${strategyLabel}:\n${diff}`);
657
+ // For tolerant matching, use the actual file text at the match position
658
+ // so the diff reflects what was really in the file (with original whitespace).
659
+ const matchedOld = matchInfo.strategy === 'tolerant'
660
+ ? content.slice(matchInfo.index, matchInfo.index + matchInfo.length)
661
+ : e.old_string;
662
+ const diff = generateDiffWithContext(content, matchedOld, e.new_string, matchInfo.index, matchInfo.length);
663
+ results.push(` Edited ${relPath}:\n${diff}`);
429
664
  content =
430
665
  content.slice(0, matchInfo.index) +
431
666
  e.new_string +
@@ -462,7 +697,7 @@ ${suggestion}`);
462
697
  return {
463
698
  success: false,
464
699
  output: '',
465
- error: `Edit FAILED — all changes rolled back. Raw input: ${JSON.stringify(rawInput)}\n${failLines}\n\n${firstError}`,
700
+ error: `Edit FAILED — all changes rolled back.\n${failLines}\n\n${firstError}`,
466
701
  };
467
702
  }
468
703
  for (const [resolved, content] of modifiedFiles) {
@@ -479,7 +714,7 @@ ${suggestion}`);
479
714
  return {
480
715
  success: false,
481
716
  output: '',
482
- error: `Failed to write ${path.relative(cwd, resolved).replace(/\\/g, '/')}: ${fmtErr(err)}. Input: ${JSON.stringify(rawInput)}`,
717
+ error: `Failed to write ${path.relative(cwd, resolved).replace(/\\/g, '/')}: ${fmtErr(err)}. Input: ${summarizeRawInput(rawInput)}`,
483
718
  };
484
719
  }
485
720
  applier.markRead(resolved);