lonny-agent 0.2.6 → 0.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (475) hide show
  1. package/.github/workflows/ci.yml +21 -0
  2. package/AGENTS.md +106 -0
  3. package/CHANGELOG.md +24 -0
  4. package/dist/agent/__tests__/session-restore.test.d.ts +2 -0
  5. package/dist/agent/__tests__/session-restore.test.d.ts.map +1 -0
  6. package/dist/agent/__tests__/session-restore.test.js +260 -0
  7. package/dist/agent/__tests__/session-restore.test.js.map +1 -0
  8. package/dist/agent/commands.d.ts +26 -0
  9. package/dist/agent/commands.d.ts.map +1 -0
  10. package/dist/agent/commands.js +253 -0
  11. package/dist/agent/commands.js.map +1 -0
  12. package/dist/agent/compaction.js +8 -1
  13. package/dist/agent/compaction.js.map +1 -1
  14. package/dist/agent/index.d.ts +2 -1
  15. package/dist/agent/index.d.ts.map +1 -1
  16. package/dist/agent/index.js +2 -2
  17. package/dist/agent/index.js.map +1 -1
  18. package/dist/agent/memory.d.ts +12 -0
  19. package/dist/agent/memory.d.ts.map +1 -0
  20. package/dist/agent/memory.js +51 -0
  21. package/dist/agent/memory.js.map +1 -0
  22. package/dist/agent/prompt-builder.d.ts +6 -1
  23. package/dist/agent/prompt-builder.d.ts.map +1 -1
  24. package/dist/agent/prompt-builder.js +89 -26
  25. package/dist/agent/prompt-builder.js.map +1 -1
  26. package/dist/agent/providers/openai.d.ts +1 -0
  27. package/dist/agent/providers/openai.d.ts.map +1 -1
  28. package/dist/agent/providers/openai.js +16 -3
  29. package/dist/agent/providers/openai.js.map +1 -1
  30. package/dist/agent/session-chat.d.ts +3 -0
  31. package/dist/agent/session-chat.d.ts.map +1 -0
  32. package/dist/agent/session-chat.js +384 -0
  33. package/dist/agent/session-chat.js.map +1 -0
  34. package/dist/agent/session-display.d.ts +29 -0
  35. package/dist/agent/session-display.d.ts.map +1 -0
  36. package/dist/agent/session-display.js +179 -0
  37. package/dist/agent/session-display.js.map +1 -0
  38. package/dist/agent/session-persistence.d.ts +55 -0
  39. package/dist/agent/session-persistence.d.ts.map +1 -0
  40. package/dist/agent/session-persistence.js +101 -0
  41. package/dist/agent/session-persistence.js.map +1 -0
  42. package/dist/agent/session-utils.d.ts +5 -0
  43. package/dist/agent/session-utils.d.ts.map +1 -0
  44. package/dist/agent/session-utils.js +51 -0
  45. package/dist/agent/session-utils.js.map +1 -0
  46. package/dist/agent/session.d.ts +15 -17
  47. package/dist/agent/session.d.ts.map +1 -1
  48. package/dist/agent/session.js +226 -608
  49. package/dist/agent/session.js.map +1 -1
  50. package/dist/cli/index.d.ts +4 -0
  51. package/dist/cli/index.d.ts.map +1 -1
  52. package/dist/cli/index.js +67 -2
  53. package/dist/cli/index.js.map +1 -1
  54. package/dist/config/index.d.ts +2 -2
  55. package/dist/config/index.d.ts.map +1 -1
  56. package/dist/index.js +24 -3
  57. package/dist/index.js.map +1 -1
  58. package/dist/tools/__tests__/bash.test.js +168 -1
  59. package/dist/tools/__tests__/bash.test.js.map +1 -1
  60. package/dist/tools/__tests__/edit.test.js +253 -6
  61. package/dist/tools/__tests__/edit.test.js.map +1 -1
  62. package/dist/tools/__tests__/fetch.test.js +1 -1
  63. package/dist/tools/__tests__/fetch.test.js.map +1 -1
  64. package/dist/tools/__tests__/glob.test.js +1 -1
  65. package/dist/tools/__tests__/glob.test.js.map +1 -1
  66. package/dist/tools/__tests__/grep.test.js +1 -1
  67. package/dist/tools/__tests__/grep.test.js.map +1 -1
  68. package/dist/tools/__tests__/ls.test.js +1 -1
  69. package/dist/tools/__tests__/ls.test.js.map +1 -1
  70. package/dist/tools/__tests__/read.test.js +1 -1
  71. package/dist/tools/__tests__/read.test.js.map +1 -1
  72. package/dist/tools/__tests__/registry.test.js +313 -0
  73. package/dist/tools/__tests__/registry.test.js.map +1 -1
  74. package/dist/tools/__tests__/write_plan.test.js +1 -1
  75. package/dist/tools/__tests__/write_plan.test.js.map +1 -1
  76. package/dist/tools/bash.d.ts.map +1 -1
  77. package/dist/tools/bash.js +659 -39
  78. package/dist/tools/bash.js.map +1 -1
  79. package/dist/tools/codebase/ast/__tests__/ast-edit-demo.d.ts +2 -0
  80. package/dist/tools/codebase/ast/__tests__/ast-edit-demo.d.ts.map +1 -0
  81. package/dist/tools/codebase/ast/__tests__/ast-edit-demo.js +7 -0
  82. package/dist/tools/codebase/ast/__tests__/ast-edit-demo.js.map +1 -0
  83. package/dist/tools/codebase/ast/__tests__/ast-edit-final.d.ts +6 -0
  84. package/dist/tools/codebase/ast/__tests__/ast-edit-final.d.ts.map +1 -0
  85. package/dist/tools/codebase/ast/__tests__/ast-edit-final.js +11 -0
  86. package/dist/tools/codebase/ast/__tests__/ast-edit-final.js.map +1 -0
  87. package/dist/tools/codebase/ast/__tests__/ast-edit-test.d.ts +2 -0
  88. package/dist/tools/codebase/ast/__tests__/ast-edit-test.d.ts.map +1 -0
  89. package/dist/tools/codebase/ast/__tests__/ast-edit-test.js +9 -0
  90. package/dist/tools/codebase/ast/__tests__/ast-edit-test.js.map +1 -0
  91. package/dist/tools/codebase/ast/__tests__/integration.test.d.ts +2 -0
  92. package/dist/tools/codebase/ast/__tests__/integration.test.d.ts.map +1 -0
  93. package/dist/tools/codebase/ast/__tests__/integration.test.js +415 -0
  94. package/dist/tools/codebase/ast/__tests__/integration.test.js.map +1 -0
  95. package/dist/tools/codebase/ast/__tests__/tree-sitter.test.d.ts +2 -0
  96. package/dist/tools/codebase/ast/__tests__/tree-sitter.test.d.ts.map +1 -0
  97. package/dist/tools/codebase/ast/__tests__/tree-sitter.test.js +414 -0
  98. package/dist/tools/codebase/ast/__tests__/tree-sitter.test.js.map +1 -0
  99. package/dist/tools/codebase/ast/adapter.d.ts +23 -0
  100. package/dist/tools/codebase/ast/adapter.d.ts.map +1 -0
  101. package/dist/tools/codebase/ast/adapter.js +11 -0
  102. package/dist/tools/codebase/ast/adapter.js.map +1 -0
  103. package/dist/tools/codebase/ast/tools.d.ts +3 -0
  104. package/dist/tools/codebase/ast/tools.d.ts.map +1 -0
  105. package/dist/tools/codebase/ast/tools.js +286 -0
  106. package/dist/tools/codebase/ast/tools.js.map +1 -0
  107. package/dist/tools/codebase/ast/tree-sitter-adapter.d.ts +3 -0
  108. package/dist/tools/codebase/ast/tree-sitter-adapter.d.ts.map +1 -0
  109. package/dist/tools/codebase/ast/tree-sitter-adapter.js +412 -0
  110. package/dist/tools/codebase/ast/tree-sitter-adapter.js.map +1 -0
  111. package/dist/tools/codebase/ast/tree-sitter-converters.d.ts +4 -0
  112. package/dist/tools/codebase/ast/tree-sitter-converters.d.ts.map +1 -0
  113. package/dist/tools/codebase/ast/tree-sitter-converters.js +447 -0
  114. package/dist/tools/codebase/ast/tree-sitter-converters.js.map +1 -0
  115. package/dist/tools/codebase/ast/types.d.ts +103 -0
  116. package/dist/tools/codebase/ast/types.d.ts.map +1 -0
  117. package/dist/tools/codebase/ast/types.js +2 -0
  118. package/dist/tools/codebase/ast/types.js.map +1 -0
  119. package/dist/tools/codebase/ast/wasm/tree-sitter-python.wasm +0 -0
  120. package/dist/tools/codebase/ast/wasm/tree-sitter-tsx.wasm +0 -0
  121. package/dist/tools/codebase/ast/wasm/tree-sitter-typescript.wasm +0 -0
  122. package/dist/tools/codebase/ast/wasm/tree-sitter.wasm +0 -0
  123. package/dist/tools/codebase/ast/wasm/web-tree-sitter.wasm +0 -0
  124. package/dist/tools/codebase/find.d.ts +3 -0
  125. package/dist/tools/codebase/find.d.ts.map +1 -0
  126. package/dist/tools/codebase/find.js +61 -0
  127. package/dist/tools/codebase/find.js.map +1 -0
  128. package/dist/tools/codebase/glob.d.ts +3 -0
  129. package/dist/tools/codebase/glob.d.ts.map +1 -0
  130. package/dist/tools/codebase/glob.js +37 -0
  131. package/dist/tools/codebase/glob.js.map +1 -0
  132. package/dist/tools/codebase/grep.d.ts +3 -0
  133. package/dist/tools/codebase/grep.d.ts.map +1 -0
  134. package/dist/tools/codebase/grep.js +169 -0
  135. package/dist/tools/codebase/grep.js.map +1 -0
  136. package/dist/tools/codebase/ls.d.ts +3 -0
  137. package/dist/tools/codebase/ls.d.ts.map +1 -0
  138. package/dist/tools/codebase/ls.js +39 -0
  139. package/dist/tools/codebase/ls.js.map +1 -0
  140. package/dist/tools/codebase/read.d.ts +10 -0
  141. package/dist/tools/codebase/read.d.ts.map +1 -0
  142. package/dist/tools/codebase/read.js +81 -0
  143. package/dist/tools/codebase/read.js.map +1 -0
  144. package/dist/tools/delete_memory.d.ts +3 -0
  145. package/dist/tools/delete_memory.d.ts.map +1 -0
  146. package/dist/tools/delete_memory.js +46 -0
  147. package/dist/tools/delete_memory.js.map +1 -0
  148. package/dist/tools/edit/diff-compute.d.ts +4 -0
  149. package/dist/tools/edit/diff-compute.d.ts.map +1 -0
  150. package/dist/tools/edit/diff-compute.js +18 -0
  151. package/dist/tools/edit/diff-compute.js.map +1 -0
  152. package/dist/tools/edit/diff-render.d.ts +18 -0
  153. package/dist/tools/edit/diff-render.d.ts.map +1 -0
  154. package/dist/tools/edit/diff-render.js +128 -0
  155. package/dist/tools/edit/diff-render.js.map +1 -0
  156. package/dist/tools/edit/edit.d.ts +4 -0
  157. package/dist/tools/edit/edit.d.ts.map +1 -0
  158. package/dist/tools/edit/edit.js +440 -0
  159. package/dist/tools/edit/edit.js.map +1 -0
  160. package/dist/tools/edit/matcher.d.ts +24 -0
  161. package/dist/tools/edit/matcher.d.ts.map +1 -0
  162. package/dist/tools/edit/matcher.js +66 -0
  163. package/dist/tools/edit/matcher.js.map +1 -0
  164. package/dist/tools/edit/parser.d.ts +10 -0
  165. package/dist/tools/edit/parser.d.ts.map +1 -0
  166. package/dist/tools/edit/parser.js +120 -0
  167. package/dist/tools/edit/parser.js.map +1 -0
  168. package/dist/tools/edit/types.d.ts +16 -0
  169. package/dist/tools/edit/types.d.ts.map +1 -0
  170. package/dist/tools/edit/types.js +2 -0
  171. package/dist/tools/edit/types.js.map +1 -0
  172. package/dist/tools/edit/write_plan.d.ts +5 -0
  173. package/dist/tools/edit/write_plan.d.ts.map +1 -0
  174. package/dist/tools/edit/write_plan.js +98 -0
  175. package/dist/tools/edit/write_plan.js.map +1 -0
  176. package/dist/tools/edit.d.ts.map +1 -1
  177. package/dist/tools/edit.js +66 -1
  178. package/dist/tools/edit.js.map +1 -1
  179. package/dist/tools/execute/bash/constants.d.ts +24 -0
  180. package/dist/tools/execute/bash/constants.d.ts.map +1 -0
  181. package/dist/tools/execute/bash/constants.js +88 -0
  182. package/dist/tools/execute/bash/constants.js.map +1 -0
  183. package/dist/tools/execute/bash/errors.d.ts +39 -0
  184. package/dist/tools/execute/bash/errors.d.ts.map +1 -0
  185. package/dist/tools/execute/bash/errors.js +249 -0
  186. package/dist/tools/execute/bash/errors.js.map +1 -0
  187. package/dist/tools/execute/bash/execution.d.ts +18 -0
  188. package/dist/tools/execute/bash/execution.d.ts.map +1 -0
  189. package/dist/tools/execute/bash/execution.js +207 -0
  190. package/dist/tools/execute/bash/execution.js.map +1 -0
  191. package/dist/tools/execute/bash/index.d.ts +3 -0
  192. package/dist/tools/execute/bash/index.d.ts.map +1 -0
  193. package/dist/tools/execute/bash/index.js +117 -0
  194. package/dist/tools/execute/bash/index.js.map +1 -0
  195. package/dist/tools/execute/bash/platform.d.ts +6 -0
  196. package/dist/tools/execute/bash/platform.d.ts.map +1 -0
  197. package/dist/tools/execute/bash/platform.js +64 -0
  198. package/dist/tools/execute/bash/platform.js.map +1 -0
  199. package/dist/tools/execute/bash/security.d.ts +12 -0
  200. package/dist/tools/execute/bash/security.d.ts.map +1 -0
  201. package/dist/tools/execute/bash/security.js +26 -0
  202. package/dist/tools/execute/bash/security.js.map +1 -0
  203. package/dist/tools/execute/bash/validation.d.ts +16 -0
  204. package/dist/tools/execute/bash/validation.d.ts.map +1 -0
  205. package/dist/tools/execute/bash/validation.js +57 -0
  206. package/dist/tools/execute/bash/validation.js.map +1 -0
  207. package/dist/tools/execute/bash.d.ts +3 -0
  208. package/dist/tools/execute/bash.d.ts.map +1 -0
  209. package/dist/tools/execute/bash.js +728 -0
  210. package/dist/tools/execute/bash.js.map +1 -0
  211. package/dist/tools/execute/git.d.ts +3 -0
  212. package/dist/tools/execute/git.d.ts.map +1 -0
  213. package/dist/tools/execute/git.js +183 -0
  214. package/dist/tools/execute/git.js.map +1 -0
  215. package/dist/tools/execute/task_complete.d.ts +7 -0
  216. package/dist/tools/execute/task_complete.d.ts.map +1 -0
  217. package/dist/tools/execute/task_complete.js +38 -0
  218. package/dist/tools/execute/task_complete.js.map +1 -0
  219. package/dist/tools/fetch.d.ts.map +1 -1
  220. package/dist/tools/fetch.js +2 -0
  221. package/dist/tools/fetch.js.map +1 -1
  222. package/dist/tools/find.d.ts.map +1 -1
  223. package/dist/tools/find.js +2 -0
  224. package/dist/tools/find.js.map +1 -1
  225. package/dist/tools/git.d.ts.map +1 -1
  226. package/dist/tools/git.js +2 -0
  227. package/dist/tools/git.js.map +1 -1
  228. package/dist/tools/glob.d.ts.map +1 -1
  229. package/dist/tools/glob.js +2 -0
  230. package/dist/tools/glob.js.map +1 -1
  231. package/dist/tools/grep.d.ts.map +1 -1
  232. package/dist/tools/grep.js +2 -0
  233. package/dist/tools/grep.js.map +1 -1
  234. package/dist/tools/install/clawhub.d.ts +26 -0
  235. package/dist/tools/install/clawhub.d.ts.map +1 -0
  236. package/dist/tools/install/clawhub.js +182 -0
  237. package/dist/tools/install/clawhub.js.map +1 -0
  238. package/dist/tools/install/constants.d.ts +5 -0
  239. package/dist/tools/install/constants.d.ts.map +1 -0
  240. package/dist/tools/install/constants.js +5 -0
  241. package/dist/tools/install/constants.js.map +1 -0
  242. package/dist/tools/install/install_skill.d.ts +3 -0
  243. package/dist/tools/install/install_skill.d.ts.map +1 -0
  244. package/dist/tools/install/install_skill.js +132 -0
  245. package/dist/tools/install/install_skill.js.map +1 -0
  246. package/dist/tools/install/npm.d.ts +8 -0
  247. package/dist/tools/install/npm.d.ts.map +1 -0
  248. package/dist/tools/install/npm.js +52 -0
  249. package/dist/tools/install/npm.js.map +1 -0
  250. package/dist/tools/install/skill-content.d.ts +9 -0
  251. package/dist/tools/install/skill-content.d.ts.map +1 -0
  252. package/dist/tools/install/skill-content.js +89 -0
  253. package/dist/tools/install/skill-content.js.map +1 -0
  254. package/dist/tools/install_skill.d.ts.map +1 -1
  255. package/dist/tools/install_skill.js +2 -0
  256. package/dist/tools/install_skill.js.map +1 -1
  257. package/dist/tools/list_memory.d.ts +3 -0
  258. package/dist/tools/list_memory.d.ts.map +1 -0
  259. package/dist/tools/list_memory.js +55 -0
  260. package/dist/tools/list_memory.js.map +1 -0
  261. package/dist/tools/ls.d.ts.map +1 -1
  262. package/dist/tools/ls.js +2 -0
  263. package/dist/tools/ls.js.map +1 -1
  264. package/dist/tools/memory/delete_memory.d.ts +3 -0
  265. package/dist/tools/memory/delete_memory.d.ts.map +1 -0
  266. package/dist/tools/memory/delete_memory.js +46 -0
  267. package/dist/tools/memory/delete_memory.js.map +1 -0
  268. package/dist/tools/memory/list_memory.d.ts +3 -0
  269. package/dist/tools/memory/list_memory.d.ts.map +1 -0
  270. package/dist/tools/memory/list_memory.js +55 -0
  271. package/dist/tools/memory/list_memory.js.map +1 -0
  272. package/dist/tools/memory/save_memory.d.ts +3 -0
  273. package/dist/tools/memory/save_memory.d.ts.map +1 -0
  274. package/dist/tools/memory/save_memory.js +45 -0
  275. package/dist/tools/memory/save_memory.js.map +1 -0
  276. package/dist/tools/read.d.ts.map +1 -1
  277. package/dist/tools/read.js +2 -0
  278. package/dist/tools/read.js.map +1 -1
  279. package/dist/tools/registry.d.ts +7 -2
  280. package/dist/tools/registry.d.ts.map +1 -1
  281. package/dist/tools/registry.js +112 -17
  282. package/dist/tools/registry.js.map +1 -1
  283. package/dist/tools/save_memory.d.ts +3 -0
  284. package/dist/tools/save_memory.d.ts.map +1 -0
  285. package/dist/tools/save_memory.js +45 -0
  286. package/dist/tools/save_memory.js.map +1 -0
  287. package/dist/tools/search.d.ts.map +1 -1
  288. package/dist/tools/search.js +2 -0
  289. package/dist/tools/search.js.map +1 -1
  290. package/dist/tools/tree.d.ts +12 -0
  291. package/dist/tools/tree.d.ts.map +1 -0
  292. package/dist/tools/tree.js +87 -0
  293. package/dist/tools/tree.js.map +1 -0
  294. package/dist/tools/types.d.ts +12 -0
  295. package/dist/tools/types.d.ts.map +1 -1
  296. package/dist/tools/web/fetch.d.ts +3 -0
  297. package/dist/tools/web/fetch.d.ts.map +1 -0
  298. package/dist/tools/web/fetch.js +49 -0
  299. package/dist/tools/web/fetch.js.map +1 -0
  300. package/dist/tools/web/search.d.ts +3 -0
  301. package/dist/tools/web/search.d.ts.map +1 -0
  302. package/dist/tools/web/search.js +134 -0
  303. package/dist/tools/web/search.js.map +1 -0
  304. package/dist/tools/write_plan.d.ts.map +1 -1
  305. package/dist/tools/write_plan.js +2 -0
  306. package/dist/tools/write_plan.js.map +1 -1
  307. package/dist/tui/commands.d.ts +4 -0
  308. package/dist/tui/commands.d.ts.map +1 -0
  309. package/dist/tui/commands.js +118 -0
  310. package/dist/tui/commands.js.map +1 -0
  311. package/dist/tui/components/colors.d.ts +36 -0
  312. package/dist/tui/components/colors.d.ts.map +1 -0
  313. package/dist/tui/components/colors.js +48 -0
  314. package/dist/tui/components/colors.js.map +1 -0
  315. package/dist/tui/components/header-bar.d.ts +26 -0
  316. package/dist/tui/components/header-bar.d.ts.map +1 -0
  317. package/dist/tui/components/header-bar.js +78 -0
  318. package/dist/tui/components/header-bar.js.map +1 -0
  319. package/dist/tui/components/index.d.ts +9 -0
  320. package/dist/tui/components/index.d.ts.map +1 -0
  321. package/dist/tui/components/index.js +9 -0
  322. package/dist/tui/components/index.js.map +1 -0
  323. package/dist/tui/components/landing-screen.d.ts +12 -0
  324. package/dist/tui/components/landing-screen.d.ts.map +1 -0
  325. package/dist/tui/components/landing-screen.js +53 -0
  326. package/dist/tui/components/landing-screen.js.map +1 -0
  327. package/dist/tui/components/pixel-logo.d.ts +5 -0
  328. package/dist/tui/components/pixel-logo.d.ts.map +1 -0
  329. package/dist/tui/components/pixel-logo.js +27 -0
  330. package/dist/tui/components/pixel-logo.js.map +1 -0
  331. package/dist/tui/components/plan-utils.d.ts +11 -0
  332. package/dist/tui/components/plan-utils.d.ts.map +1 -0
  333. package/dist/tui/components/plan-utils.js +62 -0
  334. package/dist/tui/components/plan-utils.js.map +1 -0
  335. package/dist/tui/components/plans-list.d.ts +18 -0
  336. package/dist/tui/components/plans-list.d.ts.map +1 -0
  337. package/dist/tui/components/plans-list.js +46 -0
  338. package/dist/tui/components/plans-list.js.map +1 -0
  339. package/dist/tui/components/rich-footer.d.ts +29 -0
  340. package/dist/tui/components/rich-footer.d.ts.map +1 -0
  341. package/dist/tui/components/rich-footer.js +124 -0
  342. package/dist/tui/components/rich-footer.js.map +1 -0
  343. package/dist/tui/components/todo-panel.d.ts +14 -0
  344. package/dist/tui/components/todo-panel.d.ts.map +1 -0
  345. package/dist/tui/components/todo-panel.js +83 -0
  346. package/dist/tui/components/todo-panel.js.map +1 -0
  347. package/dist/tui/components.d.ts.map +1 -1
  348. package/dist/tui/components.js +14 -7
  349. package/dist/tui/components.js.map +1 -1
  350. package/dist/tui/index.d.ts +2 -1
  351. package/dist/tui/index.d.ts.map +1 -1
  352. package/dist/tui/index.js +99 -505
  353. package/dist/tui/index.js.map +1 -1
  354. package/dist/tui/overlays.d.ts +28 -0
  355. package/dist/tui/overlays.d.ts.map +1 -0
  356. package/dist/tui/overlays.js +154 -0
  357. package/dist/tui/overlays.js.map +1 -0
  358. package/dist/tui/themes.d.ts +5 -0
  359. package/dist/tui/themes.d.ts.map +1 -0
  360. package/dist/tui/themes.js +38 -0
  361. package/dist/tui/themes.js.map +1 -0
  362. package/dist/web/index.d.ts.map +1 -1
  363. package/dist/web/index.js +60 -122
  364. package/dist/web/index.js.map +1 -1
  365. package/dist/web/public/index.html +1 -1
  366. package/dist/web/public/messages.js +41 -25
  367. package/dist/web/public/state.js +72 -72
  368. package/dist/web/public/style.css +1126 -996
  369. package/dist/web/public/websocket.js +267 -267
  370. package/dist/web/session-bridge.d.ts.map +1 -1
  371. package/dist/web/session-bridge.js +82 -3
  372. package/dist/web/session-bridge.js.map +1 -1
  373. package/docs/ast-tools.md +117 -0
  374. package/package.json +60 -55
  375. package/scripts/check-line-count.mjs +53 -0
  376. package/scripts/copy-ast-wasm.mjs +32 -0
  377. package/src/agent/__tests__/session-restore.test.ts +284 -0
  378. package/src/agent/commands.ts +274 -0
  379. package/src/agent/compaction.ts +9 -2
  380. package/src/agent/index.ts +30 -25
  381. package/src/agent/memory.ts +60 -0
  382. package/src/agent/prompt-builder.ts +100 -26
  383. package/src/agent/providers/anthropic.ts +179 -179
  384. package/src/agent/providers/google.ts +210 -210
  385. package/src/agent/providers/ollama.ts +186 -186
  386. package/src/agent/providers/openai.ts +18 -3
  387. package/src/agent/session-chat.ts +480 -0
  388. package/src/agent/session-display.ts +195 -0
  389. package/src/agent/session-persistence.ts +146 -0
  390. package/src/agent/session-utils.ts +54 -0
  391. package/src/agent/session.ts +261 -718
  392. package/src/cli/index.ts +73 -4
  393. package/src/config/index.ts +342 -342
  394. package/src/index.ts +30 -3
  395. package/src/tools/__tests__/bash.test.ts +187 -1
  396. package/src/tools/__tests__/edit.test.ts +275 -11
  397. package/src/tools/__tests__/fetch.test.ts +1 -1
  398. package/src/tools/__tests__/glob.test.ts +1 -1
  399. package/src/tools/__tests__/grep.test.ts +1 -1
  400. package/src/tools/__tests__/ls.test.ts +1 -1
  401. package/src/tools/__tests__/read.test.ts +1 -1
  402. package/src/tools/__tests__/registry.test.ts +332 -0
  403. package/src/tools/__tests__/write_plan.test.ts +1 -1
  404. package/src/tools/codebase/ast/__tests__/integration.test.ts +452 -0
  405. package/src/tools/codebase/ast/__tests__/tree-sitter.test.ts +466 -0
  406. package/src/tools/codebase/ast/adapter.ts +44 -0
  407. package/src/tools/codebase/ast/tools.ts +322 -0
  408. package/src/tools/codebase/ast/tree-sitter-adapter.ts +468 -0
  409. package/src/tools/codebase/ast/tree-sitter-converters.ts +480 -0
  410. package/src/tools/codebase/ast/types.ts +102 -0
  411. package/src/tools/codebase/ast/wasm/tree-sitter-python.wasm +0 -0
  412. package/src/tools/codebase/ast/wasm/tree-sitter-tsx.wasm +0 -0
  413. package/src/tools/codebase/ast/wasm/tree-sitter-typescript.wasm +0 -0
  414. package/src/tools/codebase/ast/wasm/tree-sitter.wasm +0 -0
  415. package/src/tools/codebase/ast/wasm/web-tree-sitter.wasm +0 -0
  416. package/src/tools/{find.ts → codebase/find.ts} +4 -2
  417. package/src/tools/{glob.ts → codebase/glob.ts} +4 -2
  418. package/src/tools/{grep.ts → codebase/grep.ts} +4 -2
  419. package/src/tools/{ls.ts → codebase/ls.ts} +4 -2
  420. package/src/tools/{read.ts → codebase/read.ts} +5 -3
  421. package/src/tools/edit/diff-compute.ts +21 -0
  422. package/src/tools/edit/diff-render.ts +145 -0
  423. package/src/tools/edit/edit.ts +468 -0
  424. package/src/tools/edit/matcher.ts +70 -0
  425. package/src/tools/edit/parser.ts +131 -0
  426. package/src/tools/edit/types.ts +19 -0
  427. package/src/tools/{write_plan.ts → edit/write_plan.ts} +4 -2
  428. package/src/tools/execute/bash/constants.ts +93 -0
  429. package/src/tools/execute/bash/errors.ts +304 -0
  430. package/src/tools/execute/bash/execution.ts +211 -0
  431. package/src/tools/execute/bash/index.ts +131 -0
  432. package/src/tools/execute/bash/platform.ts +65 -0
  433. package/src/tools/execute/bash/security.ts +28 -0
  434. package/src/tools/execute/bash/validation.ts +78 -0
  435. package/src/tools/execute/git.ts +209 -0
  436. package/src/tools/execute/task_complete.ts +40 -0
  437. package/src/tools/install/clawhub.ts +222 -0
  438. package/src/tools/install/constants.ts +4 -0
  439. package/src/tools/install/install_skill.ts +150 -0
  440. package/src/tools/install/npm.ts +60 -0
  441. package/src/tools/install/skill-content.ts +108 -0
  442. package/src/tools/memory/delete_memory.ts +46 -0
  443. package/src/tools/memory/list_memory.ts +53 -0
  444. package/src/tools/memory/save_memory.ts +45 -0
  445. package/src/tools/registry.ts +119 -19
  446. package/src/tools/tree.ts +103 -0
  447. package/src/tools/types.ts +13 -0
  448. package/src/tools/{fetch.ts → web/fetch.ts} +4 -2
  449. package/src/tools/{search.ts → web/search.ts} +4 -2
  450. package/src/tui/commands.ts +135 -0
  451. package/src/tui/components/colors.ts +52 -0
  452. package/src/tui/components/header-bar.ts +88 -0
  453. package/src/tui/components/index.ts +8 -0
  454. package/src/tui/components/landing-screen.ts +67 -0
  455. package/src/tui/components/pixel-logo.ts +33 -0
  456. package/src/tui/components/plan-utils.ts +69 -0
  457. package/src/tui/components/plans-list.ts +55 -0
  458. package/src/tui/components/rich-footer.ts +146 -0
  459. package/src/tui/components/todo-panel.ts +95 -0
  460. package/src/tui/index.ts +335 -786
  461. package/src/tui/overlays.ts +203 -0
  462. package/src/tui/themes.ts +41 -0
  463. package/src/web/index.ts +64 -127
  464. package/src/web/public/index.html +1 -1
  465. package/src/web/public/messages.js +41 -25
  466. package/src/web/public/state.js +72 -72
  467. package/src/web/public/style.css +1126 -996
  468. package/src/web/public/websocket.js +267 -267
  469. package/src/web/session-bridge.ts +272 -193
  470. package/AGENT.md +0 -99
  471. package/src/tools/bash.ts +0 -125
  472. package/src/tools/edit.ts +0 -747
  473. package/src/tools/git.ts +0 -76
  474. package/src/tools/install_skill.ts +0 -539
  475. package/src/tui/components.ts +0 -635
package/src/index.ts CHANGED
@@ -2,6 +2,7 @@
2
2
  import { execSync } from 'node:child_process'
3
3
  import * as os from 'node:os'
4
4
  import { runAgent } from './agent/index.js'
5
+ import { Session } from './agent/session.js'
5
6
  import { parseArgs } from './cli/index.js'
6
7
  import { fmtErr } from './tools/errors.js'
7
8
  import { startTui } from './tui/index.js'
@@ -27,7 +28,9 @@ function tryEnableUtf8(): void {
27
28
 
28
29
  async function main() {
29
30
  tryEnableUtf8()
30
- const { config, prompt, web, port, init } = await parseArgs(process.argv)
31
+ const { config, prompt, web, port, init, continueSession, sessionId } = await parseArgs(
32
+ process.argv,
33
+ )
31
34
 
32
35
  // init command exits after completion
33
36
  if (init) {
@@ -41,12 +44,36 @@ async function main() {
41
44
  process.exit(1)
42
45
  }
43
46
 
47
+ let loadedSession: Session | null = null
48
+
49
+ // Handle --continue and --session flags
50
+ if (continueSession || sessionId) {
51
+ if (sessionId) {
52
+ loadedSession = await Session.loadById(sessionId, config)
53
+ if (loadedSession) {
54
+ console.log(
55
+ `${BLD}Resuming session:${RS} ${loadedSession.sessionTitle || loadedSession.sessionId}`,
56
+ )
57
+ } else {
58
+ console.error(`${RE}Error:${RS} Session not found: ${sessionId}`)
59
+ process.exit(1)
60
+ }
61
+ } else {
62
+ loadedSession = await Session.load(config)
63
+ if (loadedSession) {
64
+ console.log(
65
+ `${BLD}Continuing last session:${RS} ${loadedSession.sessionTitle || loadedSession.sessionId}`,
66
+ )
67
+ }
68
+ }
69
+ }
70
+
44
71
  if (web) {
45
72
  await startWebUi(config, port || 15090)
46
73
  } else if (prompt) {
47
- await runAgent(prompt, config)
74
+ await runAgent(prompt, config, loadedSession ?? undefined)
48
75
  } else {
49
- await startTui(config)
76
+ await startTui(config, loadedSession ?? undefined)
50
77
  }
51
78
  }
52
79
 
@@ -1,5 +1,12 @@
1
+ import * as fs from 'node:fs'
2
+ import * as os from 'node:os'
3
+ import * as path from 'node:path'
1
4
  import { describe, expect, it } from 'vitest'
2
- import { bashTool } from '../bash.js'
5
+ import { bashTool } from '../execute/bash/index.js'
6
+
7
+ function makeTempDir(): string {
8
+ return fs.mkdtempSync(path.join(os.tmpdir(), 'lonny-test-'))
9
+ }
3
10
 
4
11
  describe('bash tool', () => {
5
12
  it('executes a command successfully', async () => {
@@ -25,4 +32,183 @@ describe('bash tool', () => {
25
32
  expect(result.success).toBe(true)
26
33
  expect(result.output).toContain('timed')
27
34
  })
35
+
36
+ it('includes description in output when provided', async () => {
37
+ const result = await bashTool.execute({
38
+ command: 'echo hello',
39
+ description: 'testing description',
40
+ })
41
+ expect(result.success).toBe(true)
42
+ expect(result.output).toContain('[bash]')
43
+ expect(result.output).toContain('testing description')
44
+ expect(result.output).toContain('hello')
45
+ })
46
+
47
+ it('runs command in specified cwd', async () => {
48
+ const tmpDir = makeTempDir()
49
+ try {
50
+ // Create a marker file so we can verify cwd
51
+ const result = await bashTool.execute({ command: 'pwd', cwd: tmpDir })
52
+ expect(result.success).toBe(true)
53
+ // On Windows, PowerShell's Get-Location returns the path
54
+ // We just need to check the command executed without error
55
+ if (os.platform() === 'win32') {
56
+ // PowerShell: use Get-Location instead of pwd
57
+ const result2 = await bashTool.execute({
58
+ command: `Get-Location | Select-Object -ExpandProperty Path`,
59
+ cwd: tmpDir,
60
+ })
61
+ expect(result2.success).toBe(true)
62
+ const normalizedOutput = result2.output.replace(/\\/g, '/')
63
+ const normalizedTmp = tmpDir.replace(/\\/g, '/')
64
+ expect(normalizedOutput).toContain(normalizedTmp)
65
+ } else {
66
+ expect(result.output).toContain(tmpDir)
67
+ }
68
+ } finally {
69
+ fs.rmSync(tmpDir, { recursive: true, force: true })
70
+ }
71
+ })
72
+
73
+ it('handles stderr output separately', async () => {
74
+ // PowerShell: write to stderr using Write-Error
75
+ const result = await bashTool.execute({
76
+ command: 'Write-Error "test error" 2>&1 | Out-Null; Write-Output "stdout ok"',
77
+ })
78
+ // 2>&1 redirects stderr to stdout in PowerShell, so it should succeed
79
+ expect(result.success).toBe(true)
80
+ })
81
+
82
+ it('shows (no output) for empty output', async () => {
83
+ // A command that produces no output
84
+ const result = await bashTool.execute({ command: 'Write-Output ""' })
85
+ expect(result.success).toBe(true)
86
+ expect(result.output).toContain('(no output)')
87
+ })
88
+
89
+ it('truncates very large output', async () => {
90
+ // Generate a very large output to trigger truncation
91
+ const result = await bashTool.execute({ command: '1..5000 | ForEach-Object { "Line $_" }' })
92
+ expect(result.success).toBe(true)
93
+ // Check if truncation warning is present (output > 10k chars)
94
+ if (result.output.length > 10000) {
95
+ expect(result.output).toContain('truncated at')
96
+ }
97
+ })
98
+
99
+ it('rejects whitespace-only command', async () => {
100
+ const result = await bashTool.execute({ command: ' ' })
101
+ expect(result.success).toBe(false)
102
+ expect(result.error).toContain('whitespace')
103
+ })
104
+
105
+ it('rejects non-string command type', async () => {
106
+ const result = await bashTool.execute({ command: 123 })
107
+ expect(result.success).toBe(false)
108
+ expect(result.error).toContain('must be a string')
109
+ })
110
+
111
+ it('rejects non-existent cwd', async () => {
112
+ const result = await bashTool.execute({
113
+ command: 'echo test',
114
+ cwd: 'C:\\NonExistent_Directory_XYZ_123',
115
+ })
116
+ expect(result.success).toBe(false)
117
+ expect(result.error).toContain('does not exist')
118
+ })
119
+
120
+ it('blocks destructive rm -rf / command', async () => {
121
+ const result = await bashTool.execute({ command: 'rm -rf /' })
122
+ expect(result.success).toBe(false)
123
+ expect(result.error).toContain('Destructive')
124
+ expect(result.error).toContain('destroy')
125
+ })
126
+
127
+ it('blocks destructive format command', async () => {
128
+ const result = await bashTool.execute({ command: 'format D: /Q' })
129
+ expect(result.success).toBe(false)
130
+ expect(result.error).toContain('Destructive')
131
+ expect(result.error).toContain('destroy')
132
+ })
133
+
134
+ it('clamps timeout to valid range', async () => {
135
+ // Very large timeout should be clamped, not cause error
136
+ const result = await bashTool.execute({ command: 'echo ok', timeout: 999999999 })
137
+ expect(result.success).toBe(true)
138
+ })
139
+
140
+ it('rejects filename-like command (no space before flag)', async () => {
141
+ // This test verifies that the tool handles edge case input gracefully
142
+ const result = await bashTool.execute({ command: '--version' })
143
+ // Should fail with exit code 1, not crash
144
+ expect(result.success).toBe(false)
145
+ })
146
+
147
+ it('blocks Remove-Item -Recurse -Force command', async () => {
148
+ const result = await bashTool.execute({ command: 'Remove-Item C:\\Windows -Recurse -Force' })
149
+ expect(result.success).toBe(false)
150
+ expect(result.error).toContain('Destructive')
151
+ })
152
+
153
+ it('blocks diskpart clean-all command', async () => {
154
+ const result = await bashTool.execute({ command: 'diskpart clean-all' })
155
+ expect(result.success).toBe(false)
156
+ expect(result.error).toContain('Destructive')
157
+ })
158
+
159
+ it('blocks reg delete command', async () => {
160
+ const result = await bashTool.execute({ command: 'reg delete HKLM\\Software' })
161
+ expect(result.success).toBe(false)
162
+ expect(result.error).toContain('Destructive')
163
+ })
164
+
165
+ it('rejects cwd with path traversal', async () => {
166
+ const result = await bashTool.execute({
167
+ command: 'echo test',
168
+ cwd: 'C:\\Users\\..\\Windows',
169
+ })
170
+ expect(result.success).toBe(false)
171
+ expect(result.error).toContain('path traversal')
172
+ expect(result.error).toContain('..')
173
+ })
174
+
175
+ it('rejects cwd with path traversal using forward slashes', async () => {
176
+ const result = await bashTool.execute({
177
+ command: 'echo test',
178
+ cwd: '/Users/../etc',
179
+ })
180
+ expect(result.success).toBe(false)
181
+ expect(result.error).toContain('path traversal')
182
+ })
183
+
184
+ it('redacts sensitive data from output', async () => {
185
+ // Test redaction of API key pattern in output
186
+ const result = await bashTool.execute({
187
+ command: "Write-Output 'api_key=sk-abc123def456ghi789jkl012mno345pqr678stu'",
188
+ })
189
+ expect(result.success).toBe(true)
190
+ // The API key should be redacted
191
+ expect(result.output).not.toContain('sk-abc123def456ghi789jkl012mno345pqr678stu')
192
+ expect(result.output).toContain('[REDACTED]')
193
+ })
194
+
195
+ it('redacts sensitive data from stderr', async () => {
196
+ const result = await bashTool.execute({
197
+ command: "Write-Error 'The token ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx is invalid'",
198
+ })
199
+ // Should not crash; the redacted marker should appear in the output/error
200
+ if (!result.success && result.error) {
201
+ // The token may be split across lines in PowerShell error formatting,
202
+ // but [REDACTED] should appear at least once
203
+ expect(result.error).toContain('[REDACTED]')
204
+ }
205
+ })
206
+
207
+ it('shows platform-specific tip for grep on Windows', async () => {
208
+ if (os.platform() === 'win32') {
209
+ const result = await bashTool.execute({ command: 'grep "pattern" file.txt' })
210
+ expect(result.success).toBe(false)
211
+ expect(result.error).toContain('findstr')
212
+ }
213
+ })
28
214
  })
@@ -3,16 +3,16 @@ import * as os from 'node:os'
3
3
  import * as path from 'node:path'
4
4
  import { afterAll, beforeAll, describe, expect, it } from 'vitest'
5
5
  import { FileReadTracker } from '../../diff/apply.js'
6
+ import { computeDiff } from '../edit/diff-compute.js'
6
7
  import {
7
- computeDiff,
8
- createEditTool,
9
8
  escapeHtml,
10
- findAllLinesTolerant,
11
9
  generateDiff,
12
- normalizeLine,
13
- parseMarkdownEdit,
10
+ generateDiffWithContext,
14
11
  renderDiffTerminal,
15
- } from '../edit.js'
12
+ } from '../edit/diff-render.js'
13
+ import { createEditTool } from '../edit/edit.js'
14
+ import { findAllLinesTolerant, normalizeLine } from '../edit/matcher.js'
15
+ import { parseMarkdownEdit } from '../edit/parser.js'
16
16
  import { makeTempDir } from './helpers.js'
17
17
 
18
18
  // ── Minimal mock for FileReadTracker ─────────────────────────────────────
@@ -119,6 +119,34 @@ describe('findAllLinesTolerant', () => {
119
119
  const match = contentSlice('hello \nworld\n', result[0]!)
120
120
  expect(match).toBe('hello \nworld')
121
121
  })
122
+
123
+ it('handles CRLF content - correct position computation', () => {
124
+ const result = findAllLinesTolerant('hello\r\nworld\r\nfoo\r\n', 'hello\nworld')
125
+ expect(result).toHaveLength(1)
126
+ expect(result[0]!.index).toBe(0)
127
+ // 12 = 'hello\r' (6) + '\n' (1) + 'world' (5, trailing \r excluded)
128
+ expect(result[0]!.length).toBe(12)
129
+ const match = contentSlice('hello\r\nworld\r\nfoo\r\n', result[0]!)
130
+ expect(match).toBe('hello\r\nworld')
131
+ })
132
+
133
+ it('handles mixed CRLF/LF content', () => {
134
+ const result = findAllLinesTolerant('hello\r\nworld\nfoo\r\n', 'hello\nworld')
135
+ expect(result).toHaveLength(1)
136
+ expect(result[0]!.index).toBe(0)
137
+ expect(result[0]!.length).toBe(12)
138
+ const match = contentSlice('hello\r\nworld\nfoo\r\n', result[0]!)
139
+ expect(match).toBe('hello\r\nworld')
140
+ })
141
+
142
+ it('handles CRLF with whitespace normalization', () => {
143
+ const result = findAllLinesTolerant('hello \r\nworld\r\n', 'hello\nworld')
144
+ expect(result).toHaveLength(1)
145
+ // 13 = 'hello \r' (7) + '\n' (1) + 'world' (5, trailing \r excluded)
146
+ expect(result[0]!.length).toBe(13)
147
+ const match = contentSlice('hello \r\nworld\r\n', result[0]!)
148
+ expect(match).toBe('hello \r\nworld')
149
+ })
122
150
  })
123
151
 
124
152
  function contentSlice(s: string, m: { index: number; length: number }): string {
@@ -207,6 +235,59 @@ describe('generateDiff', () => {
207
235
  })
208
236
  })
209
237
 
238
+ describe('generateDiffWithContext', () => {
239
+ it('returns context line before match', () => {
240
+ const output = generateDiffWithContext('keep\nreplace\nkeep', 'replace', 'REPLACE', 5, 7)
241
+ expect(output).toContain('keep')
242
+ expect(output).toContain('REPLACE')
243
+ })
244
+
245
+ it('returns no context before when match starts at first line', () => {
246
+ const output = generateDiffWithContext('line1\nline2', 'line1', 'LINE1', 0, 5)
247
+ expect(output).toContain('LINE1')
248
+ const dimCount = (output.match(/\x1b\[38;2;100;100;100m/g) || []).length
249
+ expect(dimCount).toBe(1)
250
+ })
251
+
252
+ it('returns no context after when match ends at last line', () => {
253
+ const output = generateDiffWithContext('line1\nline2', 'line2', 'LINE2', 6, 5)
254
+ expect(output).toContain('LINE2')
255
+ const dimCount = (output.match(/\x1b\[38;2;100;100;100m/g) || []).length
256
+ expect(dimCount).toBe(1)
257
+ })
258
+
259
+ it('handles matchIndex at exact end of content (append behavior)', () => {
260
+ const output = generateDiffWithContext('line1\nline2', '', 'appended', 11, 0)
261
+ expect(output).toContain('appended')
262
+ // Should show line 2 context (the last line)
263
+ expect(output).toContain('line2')
264
+ })
265
+
266
+ it('handles match spanning only the last line with no trailing newline', () => {
267
+ const output = generateDiffWithContext('first\nlast', 'last', 'LAST', 6, 4)
268
+ expect(output).toContain('LAST')
269
+ expect(output).toContain('first')
270
+ // No context after since match is on the last line
271
+ const dimCount = (output.match(/\x1b\[38;2;100;100;100m/g) || []).length
272
+ expect(dimCount).toBe(1)
273
+ })
274
+
275
+ it('handles fullContent with no trailing newline', () => {
276
+ const output = generateDiffWithContext('line1\nline2\nline3', 'line2', 'LINE2', 6, 5)
277
+ expect(output).toContain('LINE2')
278
+ expect(output).toContain('line1')
279
+ expect(output).toContain('line3')
280
+ })
281
+
282
+ it('handles match on single-line content with no trailing newline', () => {
283
+ const output = generateDiffWithContext('onlyline', 'onlyline', 'REPLACED', 0, 8)
284
+ expect(output).toContain('REPLACED')
285
+ // No context dim lines, only the diff
286
+ const dimCount = (output.match(/\x1b\[38;2;100;100;100m/g) || []).length
287
+ expect(dimCount).toBe(0)
288
+ })
289
+ })
290
+
210
291
  describe('escapeHtml', () => {
211
292
  it('escapes ampersands', () => {
212
293
  expect(escapeHtml('a&b')).toBe('a&b')
@@ -312,7 +393,7 @@ describe('parseMarkdownEdit', () => {
312
393
  const input = '```edit\nfile: a.ts\nold: hello\nnew: world\n```'
313
394
  const edits = parseMarkdownEdit(input)
314
395
  expect(edits).toHaveLength(1)
315
- // Non-pipe format: "old: hello" content after "old: " includes the leading space
396
+ // Non-pipe format: "old: hello" �?content after "old: " includes the leading space
316
397
  expect(edits[0]!.old_string).toBe(' hello')
317
398
  expect(edits[0]!.new_string).toBe(' world')
318
399
  })
@@ -357,7 +438,7 @@ describe('parseMarkdownEdit', () => {
357
438
  })
358
439
 
359
440
  it('handles early-``` closing (new: outside the block)', () => {
360
- // Model may close ``` before new: common mistake
441
+ // Model may close ``` before new: �?common mistake
361
442
  const input = [
362
443
  '```edit',
363
444
  'file: a.ts',
@@ -393,10 +474,51 @@ describe('parseMarkdownEdit', () => {
393
474
  expect(edits[0]!.old_string).toBe(' hello')
394
475
  expect(edits[0]!.new_string).toBe(' world')
395
476
  })
477
+
478
+ it('handles old:| then new: (no pipe on new)', () => {
479
+ const input = '```edit\nfile: a.ts\nold: |\n hello\nnew:\n world\n```'
480
+ const edits = parseMarkdownEdit(input)
481
+ expect(edits).toHaveLength(1)
482
+ expect(edits[0]!.file_path).toBe('a.ts')
483
+ expect(edits[0]!.new_string).toBe(' world')
484
+ })
485
+
486
+ it('handles |2 pipe-with-digit syntax', () => {
487
+ const input = '```edit\nfile: a.ts\nold: |2\n hello\n world\nnew: |1\n hi\n```'
488
+ const edits = parseMarkdownEdit(input)
489
+ expect(edits).toHaveLength(1)
490
+ expect(edits[0]!.file_path).toBe('a.ts')
491
+ expect(edits[0]!.old_string).toBe(' hello\n world')
492
+ expect(edits[0]!.new_string).toBe(' hi')
493
+ })
494
+
495
+ it('handles backticks inside edit content', () => {
496
+ const input = '```edit\nfile: a.ts\nold: |\n some `code` here\nnew: |\n replaced `code`\n```'
497
+ const edits = parseMarkdownEdit(input)
498
+ expect(edits).toHaveLength(1)
499
+ expect(edits[0]!.file_path).toBe('a.ts')
500
+ expect(edits[0]!.old_string).toBe(' some `code` here')
501
+ expect(edits[0]!.new_string).toBe(' replaced `code`')
502
+ })
503
+
504
+ it('handles old_string with trailing whitespace line', () => {
505
+ const input = '```edit\nfile: a.ts\nold: |\n hello\n \nnew: |\n hi\n```'
506
+ const edits = parseMarkdownEdit(input)
507
+ expect(edits).toHaveLength(1)
508
+ expect(edits[0]!.old_string).toBe(' hello\n ')
509
+ })
510
+
511
+ it('handles new_string with empty content (deletion)', () => {
512
+ const input = '```edit\nfile: a.ts\nold: |\n remove me\nnew:\n```'
513
+ const edits = parseMarkdownEdit(input)
514
+ expect(edits).toHaveLength(1)
515
+ expect(edits[0]!.old_string).toBe(' remove me')
516
+ expect(edits[0]!.new_string).toBe('')
517
+ })
396
518
  })
397
519
 
398
520
  // ── Markdown format integration tests ────────────────────────────────────
399
- describe('edit tool markdown format', () => {
521
+ describe('edit tool �?markdown format', () => {
400
522
  let tmpDir: string
401
523
  let applier: FileReadTracker
402
524
 
@@ -491,7 +613,7 @@ describe('edit tool — markdown format', () => {
491
613
  })
492
614
 
493
615
  // ── Advanced edge cases ──────────────────────────────────────────────────
494
- describe('edit tool edge cases', () => {
616
+ describe('edit tool �?edge cases', () => {
495
617
  let tmpDir: string
496
618
  let applier: FileReadTracker
497
619
 
@@ -610,6 +732,39 @@ describe('edit tool — edge cases', () => {
610
732
  expect(r.success).toBe(true)
611
733
  expect(fs.readFileSync(file, 'utf8')).toBe('brand new content\n')
612
734
  })
735
+ it('rejects empty old_string on existing non-empty file', async () => {
736
+ const file = path.join(tmpDir, 'empty-old-on-existing.txt')
737
+ fs.writeFileSync(file, 'some content\n')
738
+ applier.markRead(file)
739
+ const r = await tool().execute({
740
+ edits: [
741
+ {
742
+ file_path: 'empty-old-on-existing.txt',
743
+ old_string: '',
744
+ new_string: 'should not replace',
745
+ },
746
+ ],
747
+ })
748
+ expect(r.success).toBe(false)
749
+ expect(r.error).toContain('already exists')
750
+ })
751
+
752
+ it('rejects create file when file already exists with empty content', async () => {
753
+ const file = path.join(tmpDir, 'empty-existing-file.txt')
754
+ fs.writeFileSync(file, '')
755
+ applier.markRead(file)
756
+ const r = await tool().execute({
757
+ edits: [
758
+ {
759
+ file_path: 'empty-existing-file.txt',
760
+ old_string: '',
761
+ new_string: 'add content',
762
+ },
763
+ ],
764
+ })
765
+ expect(r.success).toBe(false)
766
+ expect(r.error).toContain('already exists')
767
+ })
613
768
  })
614
769
 
615
770
  describe('file path edge cases', () => {
@@ -644,8 +799,117 @@ describe('edit tool — edge cases', () => {
644
799
  fs.readFileSync(path.join(tmpDir, 'file-with-dashes_and_underscores.txt'), 'utf8'),
645
800
  ).toBe('special path')
646
801
  })
802
+
803
+ it('rejects path traversal with Windows drive letter', async () => {
804
+ const r = await tool().execute({
805
+ edits: [
806
+ {
807
+ file_path: 'C:\\Windows\\system32\\drivers\\etc\\hosts',
808
+ old_string: '',
809
+ new_string: 'should not write',
810
+ },
811
+ ],
812
+ })
813
+ expect(r.success).toBe(false)
814
+ expect(r.error).toContain('traversal')
815
+ })
647
816
  })
648
817
 
818
+ describe('path traversal security', () => {
819
+ it('rejects path traversal with ../', async () => {
820
+ const r = await tool().execute({
821
+ edits: [
822
+ {
823
+ file_path: '../../outside-cwd.txt',
824
+ old_string: 'hello',
825
+ new_string: 'world',
826
+ },
827
+ ],
828
+ })
829
+ expect(r.success).toBe(false)
830
+ expect(r.error).toContain('traversal')
831
+ })
832
+
833
+ it('rejects path traversal with deep nested ../', async () => {
834
+ const r = await tool().execute({
835
+ edits: [
836
+ {
837
+ file_path: 'subdir/../../../outside-cwd.txt',
838
+ old_string: '',
839
+ new_string: 'should not create',
840
+ },
841
+ ],
842
+ })
843
+ expect(r.success).toBe(false)
844
+ expect(r.error).toContain('traversal')
845
+ })
846
+
847
+ it('rejects absolute path', async () => {
848
+ const r = await tool().execute({
849
+ edits: [
850
+ {
851
+ file_path: '/etc/passwd',
852
+ old_string: '',
853
+ new_string: 'should not write',
854
+ },
855
+ ],
856
+ })
857
+ expect(r.success).toBe(false)
858
+ expect(r.error).toContain('traversal')
859
+ })
860
+
861
+ it('allows normal paths inside cwd', async () => {
862
+ const r = await tool().execute({
863
+ edits: [
864
+ {
865
+ file_path: 'safe-file.txt',
866
+ old_string: '',
867
+ new_string: 'safe content',
868
+ },
869
+ ],
870
+ })
871
+ expect(r.success).toBe(true)
872
+ })
873
+
874
+ it('rejects path traversal via markdown format', async () => {
875
+ const input = '```edit\nfile: ../../escape.txt\nold:\nnew: |\n hacked\n```'
876
+ const r = await tool().execute({ content: input })
877
+ expect(r.success).toBe(false)
878
+ expect(r.error).toContain('traversal')
879
+ })
880
+
881
+ it('rejects path traversal via symlink (if supported)', async () => {
882
+ let canSymlink = false
883
+ const externalDir = path.join(os.tmpdir(), 'lonny-symlink-target-' + Date.now())
884
+ const symLink = path.join(tmpDir, 'symlink-inside')
885
+ try {
886
+ fs.mkdirSync(externalDir, { recursive: true })
887
+ fs.writeFileSync(path.join(externalDir, 'malicious.txt'), 'evil')
888
+ fs.symlinkSync(externalDir, symLink, 'junction')
889
+ canSymlink = fs.existsSync(symLink)
890
+ } catch {
891
+ canSymlink = false
892
+ }
893
+ if (!canSymlink) return
894
+
895
+ const r = await tool().execute({
896
+ edits: [
897
+ {
898
+ file_path: 'symlink-inside/malicious.txt',
899
+ old_string: 'evil',
900
+ new_string: 'hacked',
901
+ },
902
+ ],
903
+ })
904
+ expect(r.success).toBe(false)
905
+ expect(r.error).toContain('traversal')
906
+
907
+ // Cleanup
908
+ try {
909
+ fs.rmSync(externalDir, { recursive: true, force: true })
910
+ } catch {}
911
+ })
912
+ })
649
913
  describe('error message quality', () => {
650
914
  it('includes proximity hint when old_string not found', async () => {
651
915
  const file = path.join(tmpDir, 'proximity.txt')
@@ -687,7 +951,7 @@ describe('edit tool — edge cases', () => {
687
951
  })
688
952
 
689
953
  it('includes read warning when file not read first', async () => {
690
- // Don't call markRead simulate stale content
954
+ // Don't call markRead �?simulate stale content
691
955
  const file = path.join(tmpDir, 'no-read.txt')
692
956
  fs.writeFileSync(file, 'content\n')
693
957
  const r = await tool().execute({
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, it } from 'vitest'
2
- import { fetchTool } from '../fetch.js'
2
+ import { fetchTool } from '../web/fetch.js'
3
3
 
4
4
  describe('fetch tool', () => {
5
5
  it('rejects missing url', async () => {
@@ -1,7 +1,7 @@
1
1
  import * as fs from 'node:fs'
2
2
  import * as path from 'node:path'
3
3
  import { afterAll, beforeAll, describe, expect, it } from 'vitest'
4
- import { globTool } from '../glob.js'
4
+ import { globTool } from '../codebase/glob.js'
5
5
  import { makeTempDir } from './helpers.js'
6
6
 
7
7
  describe('glob tool', () => {
@@ -1,7 +1,7 @@
1
1
  import * as fs from 'node:fs'
2
2
  import * as path from 'node:path'
3
3
  import { afterAll, beforeAll, describe, expect, it } from 'vitest'
4
- import { createGrepTool } from '../grep.js'
4
+ import { createGrepTool } from '../codebase/grep.js'
5
5
  import { makeTempDir } from './helpers.js'
6
6
 
7
7
  describe('grep tool', () => {
@@ -1,7 +1,7 @@
1
1
  import * as fs from 'node:fs'
2
2
  import * as path from 'node:path'
3
3
  import { afterAll, beforeAll, describe, expect, it } from 'vitest'
4
- import { createLsTool } from '../ls.js'
4
+ import { createLsTool } from '../codebase/ls.js'
5
5
  import { makeTempDir } from './helpers.js'
6
6
 
7
7
  describe('ls tool', () => {
@@ -2,7 +2,7 @@ import * as fs from 'node:fs'
2
2
  import * as path from 'node:path'
3
3
  import { afterAll, beforeAll, describe, expect, it } from 'vitest'
4
4
  import { FileReadTracker } from '../../diff/apply.js'
5
- import { createReadTool } from '../read.js'
5
+ import { createReadTool } from '../codebase/read.js'
6
6
  import { makeTempDir } from './helpers.js'
7
7
 
8
8
  describe('read tool', () => {