lonny-agent 0.2.4 → 0.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (509) hide show
  1. package/.github/workflows/ci.yml +21 -0
  2. package/AGENTS.md +106 -0
  3. package/CHANGELOG.md +24 -0
  4. package/README.md +326 -326
  5. package/dist/agent/__tests__/session-restore.test.d.ts +2 -0
  6. package/dist/agent/__tests__/session-restore.test.d.ts.map +1 -0
  7. package/dist/agent/__tests__/session-restore.test.js +260 -0
  8. package/dist/agent/__tests__/session-restore.test.js.map +1 -0
  9. package/dist/agent/commands.d.ts +26 -0
  10. package/dist/agent/commands.d.ts.map +1 -0
  11. package/dist/agent/commands.js +253 -0
  12. package/dist/agent/commands.js.map +1 -0
  13. package/dist/agent/compaction.js +8 -1
  14. package/dist/agent/compaction.js.map +1 -1
  15. package/dist/agent/index.d.ts +2 -1
  16. package/dist/agent/index.d.ts.map +1 -1
  17. package/dist/agent/index.js +2 -2
  18. package/dist/agent/index.js.map +1 -1
  19. package/dist/agent/llm.d.ts +4 -0
  20. package/dist/agent/llm.d.ts.map +1 -1
  21. package/dist/agent/memory.d.ts +12 -0
  22. package/dist/agent/memory.d.ts.map +1 -0
  23. package/dist/agent/memory.js +51 -0
  24. package/dist/agent/memory.js.map +1 -0
  25. package/dist/agent/prompt-builder.d.ts +6 -1
  26. package/dist/agent/prompt-builder.d.ts.map +1 -1
  27. package/dist/agent/prompt-builder.js +100 -33
  28. package/dist/agent/prompt-builder.js.map +1 -1
  29. package/dist/agent/providers/google.d.ts.map +1 -1
  30. package/dist/agent/providers/google.js.map +1 -1
  31. package/dist/agent/providers/openai.d.ts +1 -0
  32. package/dist/agent/providers/openai.d.ts.map +1 -1
  33. package/dist/agent/providers/openai.js +59 -6
  34. package/dist/agent/providers/openai.js.map +1 -1
  35. package/dist/agent/session-chat.d.ts +3 -0
  36. package/dist/agent/session-chat.d.ts.map +1 -0
  37. package/dist/agent/session-chat.js +384 -0
  38. package/dist/agent/session-chat.js.map +1 -0
  39. package/dist/agent/session-display.d.ts +29 -0
  40. package/dist/agent/session-display.d.ts.map +1 -0
  41. package/dist/agent/session-display.js +179 -0
  42. package/dist/agent/session-display.js.map +1 -0
  43. package/dist/agent/session-persistence.d.ts +55 -0
  44. package/dist/agent/session-persistence.d.ts.map +1 -0
  45. package/dist/agent/session-persistence.js +101 -0
  46. package/dist/agent/session-persistence.js.map +1 -0
  47. package/dist/agent/session-utils.d.ts +5 -0
  48. package/dist/agent/session-utils.d.ts.map +1 -0
  49. package/dist/agent/session-utils.js +51 -0
  50. package/dist/agent/session-utils.js.map +1 -0
  51. package/dist/agent/session.d.ts +19 -17
  52. package/dist/agent/session.d.ts.map +1 -1
  53. package/dist/agent/session.js +234 -578
  54. package/dist/agent/session.js.map +1 -1
  55. package/dist/cli/index.d.ts +4 -0
  56. package/dist/cli/index.d.ts.map +1 -1
  57. package/dist/cli/index.js +67 -2
  58. package/dist/cli/index.js.map +1 -1
  59. package/dist/config/index.d.ts +2 -2
  60. package/dist/config/index.d.ts.map +1 -1
  61. package/dist/config/index.js +7 -1
  62. package/dist/config/index.js.map +1 -1
  63. package/dist/index.js +24 -3
  64. package/dist/index.js.map +1 -1
  65. package/dist/tools/__tests__/bash.test.js +169 -2
  66. package/dist/tools/__tests__/bash.test.js.map +1 -1
  67. package/dist/tools/__tests__/edit.test.js +1091 -340
  68. package/dist/tools/__tests__/edit.test.js.map +1 -1
  69. package/dist/tools/__tests__/fetch.test.js +1 -1
  70. package/dist/tools/__tests__/fetch.test.js.map +1 -1
  71. package/dist/tools/__tests__/glob.test.js +1 -1
  72. package/dist/tools/__tests__/glob.test.js.map +1 -1
  73. package/dist/tools/__tests__/grep.test.js +33 -1
  74. package/dist/tools/__tests__/grep.test.js.map +1 -1
  75. package/dist/tools/__tests__/ls.test.js +1 -1
  76. package/dist/tools/__tests__/ls.test.js.map +1 -1
  77. package/dist/tools/__tests__/read.test.js +1 -1
  78. package/dist/tools/__tests__/read.test.js.map +1 -1
  79. package/dist/tools/__tests__/registry.test.js +313 -0
  80. package/dist/tools/__tests__/registry.test.js.map +1 -1
  81. package/dist/tools/__tests__/sed.test.d.ts +2 -0
  82. package/dist/tools/__tests__/sed.test.d.ts.map +1 -0
  83. package/dist/tools/__tests__/sed.test.js +228 -0
  84. package/dist/tools/__tests__/sed.test.js.map +1 -0
  85. package/dist/tools/__tests__/write_plan.test.js +1 -1
  86. package/dist/tools/__tests__/write_plan.test.js.map +1 -1
  87. package/dist/tools/bash.d.ts.map +1 -1
  88. package/dist/tools/bash.js +711 -18
  89. package/dist/tools/bash.js.map +1 -1
  90. package/dist/tools/codebase/ast/__tests__/ast-edit-demo.d.ts +2 -0
  91. package/dist/tools/codebase/ast/__tests__/ast-edit-demo.d.ts.map +1 -0
  92. package/dist/tools/codebase/ast/__tests__/ast-edit-demo.js +7 -0
  93. package/dist/tools/codebase/ast/__tests__/ast-edit-demo.js.map +1 -0
  94. package/dist/tools/codebase/ast/__tests__/ast-edit-final.d.ts +6 -0
  95. package/dist/tools/codebase/ast/__tests__/ast-edit-final.d.ts.map +1 -0
  96. package/dist/tools/codebase/ast/__tests__/ast-edit-final.js +11 -0
  97. package/dist/tools/codebase/ast/__tests__/ast-edit-final.js.map +1 -0
  98. package/dist/tools/codebase/ast/__tests__/ast-edit-test.d.ts +2 -0
  99. package/dist/tools/codebase/ast/__tests__/ast-edit-test.d.ts.map +1 -0
  100. package/dist/tools/codebase/ast/__tests__/ast-edit-test.js +9 -0
  101. package/dist/tools/codebase/ast/__tests__/ast-edit-test.js.map +1 -0
  102. package/dist/tools/codebase/ast/__tests__/integration.test.d.ts +2 -0
  103. package/dist/tools/codebase/ast/__tests__/integration.test.d.ts.map +1 -0
  104. package/dist/tools/codebase/ast/__tests__/integration.test.js +415 -0
  105. package/dist/tools/codebase/ast/__tests__/integration.test.js.map +1 -0
  106. package/dist/tools/codebase/ast/__tests__/tree-sitter.test.d.ts +2 -0
  107. package/dist/tools/codebase/ast/__tests__/tree-sitter.test.d.ts.map +1 -0
  108. package/dist/tools/codebase/ast/__tests__/tree-sitter.test.js +414 -0
  109. package/dist/tools/codebase/ast/__tests__/tree-sitter.test.js.map +1 -0
  110. package/dist/tools/codebase/ast/adapter.d.ts +23 -0
  111. package/dist/tools/codebase/ast/adapter.d.ts.map +1 -0
  112. package/dist/tools/codebase/ast/adapter.js +11 -0
  113. package/dist/tools/codebase/ast/adapter.js.map +1 -0
  114. package/dist/tools/codebase/ast/tools.d.ts +3 -0
  115. package/dist/tools/codebase/ast/tools.d.ts.map +1 -0
  116. package/dist/tools/codebase/ast/tools.js +286 -0
  117. package/dist/tools/codebase/ast/tools.js.map +1 -0
  118. package/dist/tools/codebase/ast/tree-sitter-adapter.d.ts +3 -0
  119. package/dist/tools/codebase/ast/tree-sitter-adapter.d.ts.map +1 -0
  120. package/dist/tools/codebase/ast/tree-sitter-adapter.js +412 -0
  121. package/dist/tools/codebase/ast/tree-sitter-adapter.js.map +1 -0
  122. package/dist/tools/codebase/ast/tree-sitter-converters.d.ts +4 -0
  123. package/dist/tools/codebase/ast/tree-sitter-converters.d.ts.map +1 -0
  124. package/dist/tools/codebase/ast/tree-sitter-converters.js +447 -0
  125. package/dist/tools/codebase/ast/tree-sitter-converters.js.map +1 -0
  126. package/dist/tools/codebase/ast/types.d.ts +103 -0
  127. package/dist/tools/codebase/ast/types.d.ts.map +1 -0
  128. package/dist/tools/codebase/ast/types.js +2 -0
  129. package/dist/tools/codebase/ast/types.js.map +1 -0
  130. package/dist/tools/codebase/ast/wasm/tree-sitter-python.wasm +0 -0
  131. package/dist/tools/codebase/ast/wasm/tree-sitter-tsx.wasm +0 -0
  132. package/dist/tools/codebase/ast/wasm/tree-sitter-typescript.wasm +0 -0
  133. package/dist/tools/codebase/ast/wasm/tree-sitter.wasm +0 -0
  134. package/dist/tools/codebase/ast/wasm/web-tree-sitter.wasm +0 -0
  135. package/dist/tools/codebase/find.d.ts +3 -0
  136. package/dist/tools/codebase/find.d.ts.map +1 -0
  137. package/dist/tools/codebase/find.js +61 -0
  138. package/dist/tools/codebase/find.js.map +1 -0
  139. package/dist/tools/codebase/glob.d.ts +3 -0
  140. package/dist/tools/codebase/glob.d.ts.map +1 -0
  141. package/dist/tools/codebase/glob.js +37 -0
  142. package/dist/tools/codebase/glob.js.map +1 -0
  143. package/dist/tools/codebase/grep.d.ts +3 -0
  144. package/dist/tools/codebase/grep.d.ts.map +1 -0
  145. package/dist/tools/codebase/grep.js +169 -0
  146. package/dist/tools/codebase/grep.js.map +1 -0
  147. package/dist/tools/codebase/ls.d.ts +3 -0
  148. package/dist/tools/codebase/ls.d.ts.map +1 -0
  149. package/dist/tools/codebase/ls.js +39 -0
  150. package/dist/tools/codebase/ls.js.map +1 -0
  151. package/dist/tools/codebase/read.d.ts +10 -0
  152. package/dist/tools/codebase/read.d.ts.map +1 -0
  153. package/dist/tools/codebase/read.js +81 -0
  154. package/dist/tools/codebase/read.js.map +1 -0
  155. package/dist/tools/delete_memory.d.ts +3 -0
  156. package/dist/tools/delete_memory.d.ts.map +1 -0
  157. package/dist/tools/delete_memory.js +46 -0
  158. package/dist/tools/delete_memory.js.map +1 -0
  159. package/dist/tools/edit/diff-compute.d.ts +4 -0
  160. package/dist/tools/edit/diff-compute.d.ts.map +1 -0
  161. package/dist/tools/edit/diff-compute.js +18 -0
  162. package/dist/tools/edit/diff-compute.js.map +1 -0
  163. package/dist/tools/edit/diff-render.d.ts +18 -0
  164. package/dist/tools/edit/diff-render.d.ts.map +1 -0
  165. package/dist/tools/edit/diff-render.js +128 -0
  166. package/dist/tools/edit/diff-render.js.map +1 -0
  167. package/dist/tools/edit/edit.d.ts +4 -0
  168. package/dist/tools/edit/edit.d.ts.map +1 -0
  169. package/dist/tools/edit/edit.js +440 -0
  170. package/dist/tools/edit/edit.js.map +1 -0
  171. package/dist/tools/edit/matcher.d.ts +24 -0
  172. package/dist/tools/edit/matcher.d.ts.map +1 -0
  173. package/dist/tools/edit/matcher.js +66 -0
  174. package/dist/tools/edit/matcher.js.map +1 -0
  175. package/dist/tools/edit/parser.d.ts +10 -0
  176. package/dist/tools/edit/parser.d.ts.map +1 -0
  177. package/dist/tools/edit/parser.js +120 -0
  178. package/dist/tools/edit/parser.js.map +1 -0
  179. package/dist/tools/edit/types.d.ts +16 -0
  180. package/dist/tools/edit/types.d.ts.map +1 -0
  181. package/dist/tools/edit/types.js +2 -0
  182. package/dist/tools/edit/types.js.map +1 -0
  183. package/dist/tools/edit/write_plan.d.ts +5 -0
  184. package/dist/tools/edit/write_plan.d.ts.map +1 -0
  185. package/dist/tools/edit/write_plan.js +98 -0
  186. package/dist/tools/edit/write_plan.js.map +1 -0
  187. package/dist/tools/edit.d.ts +50 -9
  188. package/dist/tools/edit.d.ts.map +1 -1
  189. package/dist/tools/edit.js +394 -159
  190. package/dist/tools/edit.js.map +1 -1
  191. package/dist/tools/execute/bash/constants.d.ts +24 -0
  192. package/dist/tools/execute/bash/constants.d.ts.map +1 -0
  193. package/dist/tools/execute/bash/constants.js +88 -0
  194. package/dist/tools/execute/bash/constants.js.map +1 -0
  195. package/dist/tools/execute/bash/errors.d.ts +39 -0
  196. package/dist/tools/execute/bash/errors.d.ts.map +1 -0
  197. package/dist/tools/execute/bash/errors.js +249 -0
  198. package/dist/tools/execute/bash/errors.js.map +1 -0
  199. package/dist/tools/execute/bash/execution.d.ts +18 -0
  200. package/dist/tools/execute/bash/execution.d.ts.map +1 -0
  201. package/dist/tools/execute/bash/execution.js +207 -0
  202. package/dist/tools/execute/bash/execution.js.map +1 -0
  203. package/dist/tools/execute/bash/index.d.ts +3 -0
  204. package/dist/tools/execute/bash/index.d.ts.map +1 -0
  205. package/dist/tools/execute/bash/index.js +117 -0
  206. package/dist/tools/execute/bash/index.js.map +1 -0
  207. package/dist/tools/execute/bash/platform.d.ts +6 -0
  208. package/dist/tools/execute/bash/platform.d.ts.map +1 -0
  209. package/dist/tools/execute/bash/platform.js +64 -0
  210. package/dist/tools/execute/bash/platform.js.map +1 -0
  211. package/dist/tools/execute/bash/security.d.ts +12 -0
  212. package/dist/tools/execute/bash/security.d.ts.map +1 -0
  213. package/dist/tools/execute/bash/security.js +26 -0
  214. package/dist/tools/execute/bash/security.js.map +1 -0
  215. package/dist/tools/execute/bash/validation.d.ts +16 -0
  216. package/dist/tools/execute/bash/validation.d.ts.map +1 -0
  217. package/dist/tools/execute/bash/validation.js +57 -0
  218. package/dist/tools/execute/bash/validation.js.map +1 -0
  219. package/dist/tools/execute/bash.d.ts +3 -0
  220. package/dist/tools/execute/bash.d.ts.map +1 -0
  221. package/dist/tools/execute/bash.js +728 -0
  222. package/dist/tools/execute/bash.js.map +1 -0
  223. package/dist/tools/execute/git.d.ts +3 -0
  224. package/dist/tools/execute/git.d.ts.map +1 -0
  225. package/dist/tools/execute/git.js +183 -0
  226. package/dist/tools/execute/git.js.map +1 -0
  227. package/dist/tools/execute/task_complete.d.ts +7 -0
  228. package/dist/tools/execute/task_complete.d.ts.map +1 -0
  229. package/dist/tools/execute/task_complete.js +38 -0
  230. package/dist/tools/execute/task_complete.js.map +1 -0
  231. package/dist/tools/fetch.d.ts.map +1 -1
  232. package/dist/tools/fetch.js +2 -0
  233. package/dist/tools/fetch.js.map +1 -1
  234. package/dist/tools/find.d.ts.map +1 -1
  235. package/dist/tools/find.js +2 -0
  236. package/dist/tools/find.js.map +1 -1
  237. package/dist/tools/git.d.ts.map +1 -1
  238. package/dist/tools/git.js +2 -0
  239. package/dist/tools/git.js.map +1 -1
  240. package/dist/tools/glob.d.ts.map +1 -1
  241. package/dist/tools/glob.js +2 -0
  242. package/dist/tools/glob.js.map +1 -1
  243. package/dist/tools/grep.d.ts.map +1 -1
  244. package/dist/tools/grep.js +56 -30
  245. package/dist/tools/grep.js.map +1 -1
  246. package/dist/tools/install/clawhub.d.ts +26 -0
  247. package/dist/tools/install/clawhub.d.ts.map +1 -0
  248. package/dist/tools/install/clawhub.js +182 -0
  249. package/dist/tools/install/clawhub.js.map +1 -0
  250. package/dist/tools/install/constants.d.ts +5 -0
  251. package/dist/tools/install/constants.d.ts.map +1 -0
  252. package/dist/tools/install/constants.js +5 -0
  253. package/dist/tools/install/constants.js.map +1 -0
  254. package/dist/tools/install/install_skill.d.ts +3 -0
  255. package/dist/tools/install/install_skill.d.ts.map +1 -0
  256. package/dist/tools/install/install_skill.js +132 -0
  257. package/dist/tools/install/install_skill.js.map +1 -0
  258. package/dist/tools/install/npm.d.ts +8 -0
  259. package/dist/tools/install/npm.d.ts.map +1 -0
  260. package/dist/tools/install/npm.js +52 -0
  261. package/dist/tools/install/npm.js.map +1 -0
  262. package/dist/tools/install/skill-content.d.ts +9 -0
  263. package/dist/tools/install/skill-content.d.ts.map +1 -0
  264. package/dist/tools/install/skill-content.js +89 -0
  265. package/dist/tools/install/skill-content.js.map +1 -0
  266. package/dist/tools/install_skill.d.ts.map +1 -1
  267. package/dist/tools/install_skill.js +2 -0
  268. package/dist/tools/install_skill.js.map +1 -1
  269. package/dist/tools/list_memory.d.ts +3 -0
  270. package/dist/tools/list_memory.d.ts.map +1 -0
  271. package/dist/tools/list_memory.js +55 -0
  272. package/dist/tools/list_memory.js.map +1 -0
  273. package/dist/tools/ls.d.ts.map +1 -1
  274. package/dist/tools/ls.js +2 -0
  275. package/dist/tools/ls.js.map +1 -1
  276. package/dist/tools/memory/delete_memory.d.ts +3 -0
  277. package/dist/tools/memory/delete_memory.d.ts.map +1 -0
  278. package/dist/tools/memory/delete_memory.js +46 -0
  279. package/dist/tools/memory/delete_memory.js.map +1 -0
  280. package/dist/tools/memory/list_memory.d.ts +3 -0
  281. package/dist/tools/memory/list_memory.d.ts.map +1 -0
  282. package/dist/tools/memory/list_memory.js +55 -0
  283. package/dist/tools/memory/list_memory.js.map +1 -0
  284. package/dist/tools/memory/save_memory.d.ts +3 -0
  285. package/dist/tools/memory/save_memory.d.ts.map +1 -0
  286. package/dist/tools/memory/save_memory.js +45 -0
  287. package/dist/tools/memory/save_memory.js.map +1 -0
  288. package/dist/tools/read.d.ts.map +1 -1
  289. package/dist/tools/read.js +2 -0
  290. package/dist/tools/read.js.map +1 -1
  291. package/dist/tools/registry.d.ts +7 -8
  292. package/dist/tools/registry.d.ts.map +1 -1
  293. package/dist/tools/registry.js +114 -46
  294. package/dist/tools/registry.js.map +1 -1
  295. package/dist/tools/save_memory.d.ts +3 -0
  296. package/dist/tools/save_memory.d.ts.map +1 -0
  297. package/dist/tools/save_memory.js +45 -0
  298. package/dist/tools/save_memory.js.map +1 -0
  299. package/dist/tools/search.d.ts.map +1 -1
  300. package/dist/tools/search.js +2 -0
  301. package/dist/tools/search.js.map +1 -1
  302. package/dist/tools/sed.d.ts +4 -0
  303. package/dist/tools/sed.d.ts.map +1 -0
  304. package/dist/tools/sed.js +121 -0
  305. package/dist/tools/sed.js.map +1 -0
  306. package/dist/tools/tree.d.ts +12 -0
  307. package/dist/tools/tree.d.ts.map +1 -0
  308. package/dist/tools/tree.js +87 -0
  309. package/dist/tools/tree.js.map +1 -0
  310. package/dist/tools/types.d.ts +12 -0
  311. package/dist/tools/types.d.ts.map +1 -1
  312. package/dist/tools/web/fetch.d.ts +3 -0
  313. package/dist/tools/web/fetch.d.ts.map +1 -0
  314. package/dist/tools/web/fetch.js +49 -0
  315. package/dist/tools/web/fetch.js.map +1 -0
  316. package/dist/tools/web/search.d.ts +3 -0
  317. package/dist/tools/web/search.d.ts.map +1 -0
  318. package/dist/tools/web/search.js +134 -0
  319. package/dist/tools/web/search.js.map +1 -0
  320. package/dist/tools/write_plan.d.ts.map +1 -1
  321. package/dist/tools/write_plan.js +2 -0
  322. package/dist/tools/write_plan.js.map +1 -1
  323. package/dist/tui/commands.d.ts +4 -0
  324. package/dist/tui/commands.d.ts.map +1 -0
  325. package/dist/tui/commands.js +118 -0
  326. package/dist/tui/commands.js.map +1 -0
  327. package/dist/tui/components/colors.d.ts +36 -0
  328. package/dist/tui/components/colors.d.ts.map +1 -0
  329. package/dist/tui/components/colors.js +48 -0
  330. package/dist/tui/components/colors.js.map +1 -0
  331. package/dist/tui/components/header-bar.d.ts +26 -0
  332. package/dist/tui/components/header-bar.d.ts.map +1 -0
  333. package/dist/tui/components/header-bar.js +78 -0
  334. package/dist/tui/components/header-bar.js.map +1 -0
  335. package/dist/tui/components/index.d.ts +9 -0
  336. package/dist/tui/components/index.d.ts.map +1 -0
  337. package/dist/tui/components/index.js +9 -0
  338. package/dist/tui/components/index.js.map +1 -0
  339. package/dist/tui/components/landing-screen.d.ts +12 -0
  340. package/dist/tui/components/landing-screen.d.ts.map +1 -0
  341. package/dist/tui/components/landing-screen.js +53 -0
  342. package/dist/tui/components/landing-screen.js.map +1 -0
  343. package/dist/tui/components/pixel-logo.d.ts +5 -0
  344. package/dist/tui/components/pixel-logo.d.ts.map +1 -0
  345. package/dist/tui/components/pixel-logo.js +27 -0
  346. package/dist/tui/components/pixel-logo.js.map +1 -0
  347. package/dist/tui/components/plan-utils.d.ts +11 -0
  348. package/dist/tui/components/plan-utils.d.ts.map +1 -0
  349. package/dist/tui/components/plan-utils.js +62 -0
  350. package/dist/tui/components/plan-utils.js.map +1 -0
  351. package/dist/tui/components/plans-list.d.ts +18 -0
  352. package/dist/tui/components/plans-list.d.ts.map +1 -0
  353. package/dist/tui/components/plans-list.js +46 -0
  354. package/dist/tui/components/plans-list.js.map +1 -0
  355. package/dist/tui/components/rich-footer.d.ts +29 -0
  356. package/dist/tui/components/rich-footer.d.ts.map +1 -0
  357. package/dist/tui/components/rich-footer.js +124 -0
  358. package/dist/tui/components/rich-footer.js.map +1 -0
  359. package/dist/tui/components/todo-panel.d.ts +14 -0
  360. package/dist/tui/components/todo-panel.d.ts.map +1 -0
  361. package/dist/tui/components/todo-panel.js +83 -0
  362. package/dist/tui/components/todo-panel.js.map +1 -0
  363. package/dist/tui/components.d.ts.map +1 -1
  364. package/dist/tui/components.js +14 -7
  365. package/dist/tui/components.js.map +1 -1
  366. package/dist/tui/index.d.ts +2 -1
  367. package/dist/tui/index.d.ts.map +1 -1
  368. package/dist/tui/index.js +99 -505
  369. package/dist/tui/index.js.map +1 -1
  370. package/dist/tui/overlays.d.ts +28 -0
  371. package/dist/tui/overlays.d.ts.map +1 -0
  372. package/dist/tui/overlays.js +154 -0
  373. package/dist/tui/overlays.js.map +1 -0
  374. package/dist/tui/themes.d.ts +5 -0
  375. package/dist/tui/themes.d.ts.map +1 -0
  376. package/dist/tui/themes.js +38 -0
  377. package/dist/tui/themes.js.map +1 -0
  378. package/dist/web/index.d.ts.map +1 -1
  379. package/dist/web/index.js +62 -101
  380. package/dist/web/index.js.map +1 -1
  381. package/dist/web/public/app.js +5 -855
  382. package/dist/web/public/confirm.js +70 -0
  383. package/dist/web/public/index.html +121 -120
  384. package/dist/web/public/input.js +107 -0
  385. package/dist/web/public/messages.js +411 -0
  386. package/dist/web/public/sidebar.js +82 -0
  387. package/dist/web/public/state.js +72 -0
  388. package/dist/web/public/style.css +247 -70
  389. package/dist/web/public/utils.js +37 -0
  390. package/dist/web/public/websocket.js +267 -0
  391. package/dist/web/public/ws.js +19 -0
  392. package/dist/web/session-bridge.d.ts.map +1 -1
  393. package/dist/web/session-bridge.js +85 -10
  394. package/dist/web/session-bridge.js.map +1 -1
  395. package/docs/ast-tools.md +117 -0
  396. package/package.json +11 -5
  397. package/scripts/check-line-count.mjs +53 -0
  398. package/scripts/copy-ast-wasm.mjs +32 -0
  399. package/scripts/copy-native.mjs +24 -0
  400. package/scripts/copy-web.mjs +15 -0
  401. package/src/agent/__tests__/session-restore.test.ts +284 -0
  402. package/src/agent/commands.ts +274 -0
  403. package/src/agent/compaction.ts +9 -2
  404. package/src/agent/index.ts +7 -2
  405. package/src/agent/llm.ts +4 -0
  406. package/src/agent/memory.ts +60 -0
  407. package/src/agent/prompt-builder.ts +111 -33
  408. package/src/agent/providers/google.ts +0 -1
  409. package/src/agent/providers/openai.ts +79 -8
  410. package/src/agent/session-chat.ts +480 -0
  411. package/src/agent/session-display.ts +195 -0
  412. package/src/agent/session-persistence.ts +146 -0
  413. package/src/agent/session-utils.ts +54 -0
  414. package/src/agent/session.ts +269 -670
  415. package/src/cli/index.ts +73 -4
  416. package/src/config/index.ts +14 -7
  417. package/src/index.ts +30 -3
  418. package/src/tools/__tests__/bash.test.ts +188 -2
  419. package/src/tools/__tests__/edit.test.ts +1216 -356
  420. package/src/tools/__tests__/fetch.test.ts +1 -1
  421. package/src/tools/__tests__/glob.test.ts +1 -1
  422. package/src/tools/__tests__/grep.test.ts +36 -1
  423. package/src/tools/__tests__/ls.test.ts +1 -1
  424. package/src/tools/__tests__/read.test.ts +1 -1
  425. package/src/tools/__tests__/registry.test.ts +332 -0
  426. package/src/tools/__tests__/write_plan.test.ts +1 -1
  427. package/src/tools/codebase/ast/__tests__/integration.test.ts +452 -0
  428. package/src/tools/codebase/ast/__tests__/tree-sitter.test.ts +466 -0
  429. package/src/tools/codebase/ast/adapter.ts +44 -0
  430. package/src/tools/codebase/ast/tools.ts +322 -0
  431. package/src/tools/codebase/ast/tree-sitter-adapter.ts +468 -0
  432. package/src/tools/codebase/ast/tree-sitter-converters.ts +480 -0
  433. package/src/tools/codebase/ast/types.ts +102 -0
  434. package/src/tools/codebase/ast/wasm/tree-sitter-python.wasm +0 -0
  435. package/src/tools/codebase/ast/wasm/tree-sitter-tsx.wasm +0 -0
  436. package/src/tools/codebase/ast/wasm/tree-sitter-typescript.wasm +0 -0
  437. package/src/tools/codebase/ast/wasm/tree-sitter.wasm +0 -0
  438. package/src/tools/codebase/ast/wasm/web-tree-sitter.wasm +0 -0
  439. package/src/tools/{find.ts → codebase/find.ts} +4 -2
  440. package/src/tools/{glob.ts → codebase/glob.ts} +4 -2
  441. package/src/tools/{grep.ts → codebase/grep.ts} +61 -32
  442. package/src/tools/{ls.ts → codebase/ls.ts} +4 -2
  443. package/src/tools/{read.ts → codebase/read.ts} +5 -3
  444. package/src/tools/edit/diff-compute.ts +21 -0
  445. package/src/tools/edit/diff-render.ts +145 -0
  446. package/src/tools/edit/edit.ts +468 -0
  447. package/src/tools/edit/matcher.ts +70 -0
  448. package/src/tools/edit/parser.ts +131 -0
  449. package/src/tools/edit/types.ts +19 -0
  450. package/src/tools/{write_plan.ts → edit/write_plan.ts} +4 -2
  451. package/src/tools/execute/bash/constants.ts +93 -0
  452. package/src/tools/execute/bash/errors.ts +304 -0
  453. package/src/tools/execute/bash/execution.ts +211 -0
  454. package/src/tools/execute/bash/index.ts +131 -0
  455. package/src/tools/execute/bash/platform.ts +65 -0
  456. package/src/tools/execute/bash/security.ts +28 -0
  457. package/src/tools/execute/bash/validation.ts +78 -0
  458. package/src/tools/execute/git.ts +209 -0
  459. package/src/tools/execute/task_complete.ts +40 -0
  460. package/src/tools/install/clawhub.ts +222 -0
  461. package/src/tools/install/constants.ts +4 -0
  462. package/src/tools/install/install_skill.ts +150 -0
  463. package/src/tools/install/npm.ts +60 -0
  464. package/src/tools/install/skill-content.ts +108 -0
  465. package/src/tools/memory/delete_memory.ts +46 -0
  466. package/src/tools/memory/list_memory.ts +53 -0
  467. package/src/tools/memory/save_memory.ts +45 -0
  468. package/src/tools/registry.ts +121 -50
  469. package/src/tools/tree.ts +103 -0
  470. package/src/tools/types.ts +13 -0
  471. package/src/tools/{fetch.ts → web/fetch.ts} +4 -2
  472. package/src/tools/{search.ts → web/search.ts} +4 -2
  473. package/src/tui/commands.ts +135 -0
  474. package/src/tui/components/colors.ts +52 -0
  475. package/src/tui/components/header-bar.ts +88 -0
  476. package/src/tui/components/index.ts +8 -0
  477. package/src/tui/components/landing-screen.ts +67 -0
  478. package/src/tui/components/pixel-logo.ts +33 -0
  479. package/src/tui/components/plan-utils.ts +69 -0
  480. package/src/tui/components/plans-list.ts +55 -0
  481. package/src/tui/components/rich-footer.ts +146 -0
  482. package/src/tui/components/todo-panel.ts +95 -0
  483. package/src/tui/index.ts +110 -561
  484. package/src/tui/overlays.ts +203 -0
  485. package/src/tui/themes.ts +41 -0
  486. package/src/web/index.ts +66 -107
  487. package/src/web/public/app.js +5 -855
  488. package/src/web/public/confirm.js +70 -0
  489. package/src/web/public/index.html +121 -120
  490. package/src/web/public/input.js +107 -0
  491. package/src/web/public/messages.js +411 -0
  492. package/src/web/public/sidebar.js +82 -0
  493. package/src/web/public/state.js +72 -0
  494. package/src/web/public/style.css +247 -70
  495. package/src/web/public/utils.js +37 -0
  496. package/src/web/public/websocket.js +267 -0
  497. package/src/web/public/ws.js +19 -0
  498. package/src/web/session-bridge.ts +90 -12
  499. package/.claude/settings.local.json +0 -14
  500. package/.kilo/plans/1780064105789-mighty-pixel.md +0 -171
  501. package/.kilo/plans/1780293725888-quick-wizard.md +0 -62
  502. package/.lonny/plan-web-cwd-status.md +0 -38
  503. package/AGENT.md +0 -99
  504. package/src/tools/bash.ts +0 -39
  505. package/src/tools/edit.ts +0 -554
  506. package/src/tools/exec.ts +0 -348
  507. package/src/tools/git.ts +0 -76
  508. package/src/tools/install_skill.ts +0 -539
  509. package/src/tui/components.ts +0 -635
@@ -0,0 +1,53 @@
1
+ import { readdirSync, readFileSync } from 'node:fs'
2
+ import { dirname, join, relative } from 'node:path'
3
+ import { fileURLToPath } from 'node:url'
4
+
5
+ const MAX_LINES = 500
6
+ const __dirname = dirname(fileURLToPath(import.meta.url))
7
+ const ROOT = join(__dirname, '..')
8
+ const SRC = join(ROOT, 'src')
9
+
10
+ /** Recursively collect .ts files in a directory (skip node_modules, dist, __tests__). */
11
+ function walk(dir) {
12
+ const entries = readdirSync(dir, { withFileTypes: true })
13
+ const files = []
14
+ for (const entry of entries) {
15
+ const full = join(dir, entry.name)
16
+ if (entry.isDirectory()) {
17
+ if (
18
+ entry.name === 'node_modules' ||
19
+ entry.name === 'dist' ||
20
+ entry.name === '__tests__' ||
21
+ entry.name === 'pi-tui' ||
22
+ entry.name.startsWith('.')
23
+ )
24
+ continue
25
+ files.push(...walk(full))
26
+ } else if (entry.name.endsWith('.ts') && !entry.name.endsWith('.d.ts')) {
27
+ files.push(full)
28
+ }
29
+ }
30
+ return files
31
+ }
32
+
33
+ let exitCode = 0
34
+ const files = walk(SRC)
35
+
36
+ for (const file of files) {
37
+ try {
38
+ const content = readFileSync(file, 'utf-8')
39
+ const lines = content.split('\n').length
40
+ if (lines > MAX_LINES) {
41
+ const rel = relative(ROOT, file).replace(/\\/g, '/')
42
+ console.log(`⚠ ${rel} (${lines} lines) exceeds ${MAX_LINES}-line limit`)
43
+ exitCode = 1
44
+ }
45
+ } catch {
46
+ // skip unreadable files
47
+ }
48
+ }
49
+
50
+ if (exitCode === 0) {
51
+ console.log(`✓ All ${files.length} source files are under ${MAX_LINES} lines`)
52
+ }
53
+ process.exit(exitCode)
@@ -0,0 +1,32 @@
1
+ import { copyFileSync, mkdirSync } from 'node:fs'
2
+ import { createRequire } from 'node:module'
3
+ import { dirname, join } from 'node:path'
4
+ import { fileURLToPath } from 'node:url'
5
+
6
+ const __dirname = dirname(fileURLToPath(import.meta.url))
7
+ const require = createRequire(import.meta.url)
8
+
9
+ const targets = [
10
+ join(__dirname, '..', 'src', 'tools', 'codebase', 'ast', 'wasm'),
11
+ join(__dirname, '..', 'dist', 'tools', 'codebase', 'ast', 'wasm'),
12
+ ]
13
+
14
+ const wasmMap = [
15
+ { id: 'web-tree-sitter/web-tree-sitter.wasm', out: 'web-tree-sitter.wasm' },
16
+ { id: 'tree-sitter-typescript/tree-sitter-typescript.wasm', out: 'tree-sitter-typescript.wasm' },
17
+ { id: 'tree-sitter-typescript/tree-sitter-tsx.wasm', out: 'tree-sitter-tsx.wasm' },
18
+ { id: 'tree-sitter-python/tree-sitter-python.wasm', out: 'tree-sitter-python.wasm' },
19
+ ]
20
+
21
+ for (const dest of targets) {
22
+ mkdirSync(dest, { recursive: true })
23
+ for (const { id, out } of wasmMap) {
24
+ try {
25
+ const src = require.resolve(id)
26
+ copyFileSync(src, join(dest, out))
27
+ console.log(` ✓ ${dest.endsWith('wasm') ? out : out} → ${dest.slice(dest.indexOf('tools'))}`)
28
+ } catch (err) {
29
+ console.warn(` ⚠ Failed to copy ${out}: ${err.message}`)
30
+ }
31
+ }
32
+ }
@@ -0,0 +1,24 @@
1
+ // copy:native — recursively copies src/pi-tui/native/ to dist/pi-tui/native/
2
+ import { copyFileSync, existsSync, mkdirSync, readdirSync, statSync } from 'fs'
3
+ import { join } from 'path'
4
+ import { fileURLToPath } from 'url'
5
+
6
+ const __dirname = fileURLToPath(new URL('..', import.meta.url))
7
+
8
+ function cpDir(s, d) {
9
+ mkdirSync(d, { recursive: true })
10
+ for (const e of readdirSync(s)) {
11
+ const sp = join(s, e)
12
+ const dp = join(d, e)
13
+ if (statSync(sp).isDirectory()) {
14
+ cpDir(sp, dp)
15
+ } else {
16
+ copyFileSync(sp, dp)
17
+ }
18
+ }
19
+ }
20
+
21
+ const src = join(__dirname, 'src', 'pi-tui', 'native')
22
+ const dst = join(__dirname, 'dist', 'pi-tui', 'native')
23
+
24
+ if (existsSync(src)) cpDir(src, dst)
@@ -0,0 +1,15 @@
1
+ // copy:web — copies src/web/public/ to dist/web/public/
2
+ import { copyFileSync, existsSync, mkdirSync, readdirSync } from 'fs'
3
+ import { join } from 'path'
4
+ import { fileURLToPath } from 'url'
5
+
6
+ const __dirname = fileURLToPath(new URL('..', import.meta.url))
7
+ const src = join(__dirname, 'src', 'web', 'public')
8
+ const dst = join(__dirname, 'dist', 'web', 'public')
9
+
10
+ if (existsSync(src)) {
11
+ if (!existsSync(dst)) mkdirSync(dst, { recursive: true })
12
+ for (const f of readdirSync(src)) {
13
+ copyFileSync(join(src, f), join(dst, f))
14
+ }
15
+ }
@@ -0,0 +1,284 @@
1
+ import * as crypto from 'node:crypto'
2
+ import * as fs from 'node:fs'
3
+ import * as os from 'node:os'
4
+ import * as path from 'node:path'
5
+ import { afterEach, beforeEach, describe, expect, test } from 'vitest'
6
+ import type { Config } from '../../config/index.js'
7
+ import type { LLMMessage } from '../llm.js'
8
+ import { Session } from '../session.js'
9
+
10
+ /**
11
+ * Helper: build a minimal Config with a project-subdir cwd for test session files.
12
+ * Using the project directory ensures detectPackageManager etc. find a valid cwd.
13
+ * Each test gets a fresh subdirectory that gets cleaned up after.
14
+ */
15
+ function testConfig(): Config {
16
+ const testDir = path.join(process.cwd(), '.test-sessions', crypto.randomUUID().slice(0, 8))
17
+ fs.mkdirSync(testDir, { recursive: true })
18
+ // Create a package.json so detectPackageManager doesn't fail async
19
+ fs.writeFileSync(path.join(testDir, 'package.json'), JSON.stringify({ name: 'test' }), 'utf-8')
20
+ return {
21
+ mode: 'code',
22
+ model: 'test-model',
23
+ provider: 'openai',
24
+ apiKey: 'test-key',
25
+ baseUrl: 'http://test',
26
+ cwd: testDir,
27
+ autoApprove: true,
28
+ thinking: false,
29
+ reasoningEffort: 'medium',
30
+ enableCache: false,
31
+ strictTools: false,
32
+ contextWindow: 128_000,
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Build the session file path matching what save() would write.
38
+ */
39
+ function sessionFilePath(cwd: string, sessionId: string): string {
40
+ const { createHash } = require('node:crypto')
41
+ const absPath = path.resolve(cwd)
42
+ const hash = createHash('sha256').update(absPath, 'utf-8').digest('hex').slice(0, 12)
43
+ const dirName = path.basename(absPath)
44
+ const safeName = dirName.replace(/[<>:"/\\|?*]/g, '_')
45
+ const base = `${safeName}-${hash}`
46
+ const sessionDir = path.join(os.homedir(), '.lonny', 'sessions')
47
+ return path.join(sessionDir, `${base}-${sessionId}.json`)
48
+ }
49
+
50
+ describe('Session save/load cycle', () => {
51
+ let config: Config
52
+ let sessionId: string
53
+ let filePath: string
54
+
55
+ beforeEach(() => {
56
+ config = testConfig()
57
+ const { randomUUID } = require('node:crypto')
58
+ sessionId = randomUUID().slice(0, 8)
59
+ filePath = sessionFilePath(config.cwd, sessionId)
60
+ // Ensure session directory exists
61
+ const dir = path.dirname(filePath)
62
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true })
63
+ })
64
+
65
+ afterEach(() => {
66
+ try {
67
+ if (fs.existsSync(filePath)) fs.unlinkSync(filePath)
68
+ } catch {
69
+ /* ignore */
70
+ }
71
+ // Don't clean up .test-sessions dir — buildSystemPrompt fires async and may
72
+ // still be reading the cwd. The dir is gitignored and cleaned on restart.
73
+ })
74
+
75
+ test('restores assistant message with text + tool_calls + tool results', async () => {
76
+ const messages: LLMMessage[] = [
77
+ { role: 'system', content: 'You are a helpful assistant.' },
78
+ { role: 'user', content: 'Fix the bug in the code.' },
79
+ {
80
+ role: 'assistant',
81
+ content: 'I see the root cause. Three fixes needed:\n\n1. Remove X\n2. Add Y',
82
+ tool_calls: [
83
+ { id: 'call_abc', name: 'edit', input: { file: 'test.ts', old: 'a', new: 'b' } },
84
+ { id: 'call_def', name: 'read', input: { file: 'test.ts' } },
85
+ ],
86
+ },
87
+ { role: 'tool', content: 'Edited test.ts', tool_call_id: 'call_abc', name: 'edit' },
88
+ { role: 'tool', content: 'File contents...', tool_call_id: 'call_def', name: 'read' },
89
+ { role: 'assistant', content: 'Done with fixes.' },
90
+ { role: 'user', content: 'Great, thanks!' },
91
+ ]
92
+
93
+ // Write session file (simulating what save() would produce)
94
+ const data = {
95
+ id: sessionId,
96
+ cwd: path.resolve(config.cwd),
97
+ messages,
98
+ totalInputTokens: 100,
99
+ totalOutputTokens: 50,
100
+ totalApiCalls: 2,
101
+ mode: 'code',
102
+ model: 'test-model',
103
+ provider: 'test-provider',
104
+ createdAt: new Date().toISOString(),
105
+ updatedAt: new Date().toISOString(),
106
+ }
107
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8')
108
+
109
+ // Load the session from disk
110
+ const session = await Session.load(config)
111
+ expect(session).not.toBeNull()
112
+
113
+ // Verify ALL messages are preserved
114
+ expect(session!.messages.length).toBe(messages.length)
115
+ for (let i = 0; i < messages.length; i++) {
116
+ expect(session!.messages[i].role).toBe(messages[i].role)
117
+ }
118
+
119
+ // Verify the critical assistant message (text + tool_calls)
120
+ const assistantWithTools = session!.messages.find(
121
+ m => m.role === 'assistant' && m.tool_calls && m.tool_calls.length > 0,
122
+ )
123
+ expect(assistantWithTools).toBeDefined()
124
+ expect(assistantWithTools!.content).toContain('I see the root cause')
125
+ expect(assistantWithTools!.tool_calls).toHaveLength(2)
126
+
127
+ // Verify tool results still reference the correct tool_call_ids
128
+ const toolMsgs = session!.messages.filter(m => m.role === 'tool')
129
+ expect(toolMsgs).toHaveLength(2)
130
+ expect(toolMsgs[0].tool_call_id).toBe('call_abc')
131
+ expect(toolMsgs[1].tool_call_id).toBe('call_def')
132
+ })
133
+
134
+ test('preserves assistant message with text-only (no tool_calls)', async () => {
135
+ const messages: LLMMessage[] = [
136
+ { role: 'system', content: 'system prompt' },
137
+ { role: 'user', content: 'Hello' },
138
+ { role: 'assistant', content: 'Hi there!' },
139
+ { role: 'user', content: 'How are you?' },
140
+ { role: 'assistant', content: "I'm great, thanks!" },
141
+ ]
142
+
143
+ const data = {
144
+ id: sessionId,
145
+ cwd: path.resolve(config.cwd),
146
+ messages,
147
+ totalInputTokens: 0,
148
+ totalOutputTokens: 0,
149
+ totalApiCalls: 0,
150
+ mode: 'code',
151
+ model: 'test-model',
152
+ provider: 'test-provider',
153
+ createdAt: new Date().toISOString(),
154
+ updatedAt: new Date().toISOString(),
155
+ }
156
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8')
157
+
158
+ const session = await Session.load(config)
159
+ expect(session).not.toBeNull()
160
+ expect(session!.messages.length).toBe(messages.length)
161
+ expect(session!.messages[2].content).toBe('Hi there!')
162
+ expect(session!.messages[4].content).toBe("I'm great, thanks!")
163
+ })
164
+
165
+ test('loads session with most messages when multiple session files exist', async () => {
166
+ // Create a "newer but emptier" session file
167
+ const newSessionId = 'new_empty_' + require('node:crypto').randomUUID().slice(0, 4)
168
+ const newFilePath = sessionFilePath(config.cwd, newSessionId)
169
+ const emptyMessages: LLMMessage[] = [
170
+ { role: 'system', content: 'system prompt' },
171
+ { role: 'user', content: 'Hello' },
172
+ ]
173
+ const newData = {
174
+ id: newSessionId,
175
+ cwd: path.resolve(config.cwd),
176
+ messages: emptyMessages,
177
+ totalInputTokens: 0,
178
+ totalOutputTokens: 0,
179
+ totalApiCalls: 0,
180
+ mode: 'code',
181
+ model: 'test-model',
182
+ provider: 'test-provider',
183
+ createdAt: new Date(Date.now() + 60_000).toISOString(), // 1 minute in the future
184
+ updatedAt: new Date(Date.now() + 60_000).toISOString(), // 1 minute in the future
185
+ }
186
+ fs.writeFileSync(newFilePath, JSON.stringify(newData, null, 2), 'utf-8')
187
+
188
+ // Create an "older but fuller" session file
189
+ const fullMessages: LLMMessage[] = [
190
+ { role: 'system', content: 'system prompt' },
191
+ { role: 'user', content: 'Do the first thing.' },
192
+ {
193
+ role: 'assistant',
194
+ content: 'Working on it.',
195
+ tool_calls: [{ id: 't1', name: 'bash', input: { command: 'echo 1' } }],
196
+ },
197
+ { role: 'tool', content: '1', tool_call_id: 't1', name: 'bash' },
198
+ { role: 'user', content: 'Now the second thing.' },
199
+ { role: 'assistant', content: 'Doing it.' },
200
+ { role: 'user', content: 'Great!' },
201
+ ]
202
+
203
+ const data = {
204
+ id: sessionId,
205
+ cwd: path.resolve(config.cwd),
206
+ messages: fullMessages,
207
+ totalInputTokens: 0,
208
+ totalOutputTokens: 0,
209
+ totalApiCalls: 0,
210
+ mode: 'code',
211
+ model: 'test-model',
212
+ provider: 'test-provider',
213
+ createdAt: new Date().toISOString(),
214
+ updatedAt: new Date().toISOString(), // OLDER timestamp
215
+ }
216
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8')
217
+
218
+ // Load — should pick the FULL session (7 messages) over the NEWER but EMPTIER one (2 messages)
219
+ const session = await Session.load(config)
220
+ expect(session).not.toBeNull()
221
+ expect(session!.messages.length).toBe(7)
222
+ expect(session!.messages[1].content).toBe('Do the first thing.')
223
+
224
+ // Clean up the extra file
225
+ try {
226
+ fs.unlinkSync(newFilePath)
227
+ } catch {
228
+ /* ignore */
229
+ }
230
+ })
231
+
232
+ test('sanitizeMessages keeps complete assistant+tool sequences', async () => {
233
+ // Two complete turns, each with tool_calls+results — should all survive
234
+ const messages: LLMMessage[] = [
235
+ { role: 'system', content: 'system prompt' },
236
+ { role: 'user', content: 'Do the first thing.' },
237
+ {
238
+ role: 'assistant',
239
+ content: 'Working on it.',
240
+ tool_calls: [{ id: 't1', name: 'bash', input: { command: 'echo 1' } }],
241
+ },
242
+ { role: 'tool', content: '1', tool_call_id: 't1', name: 'bash' },
243
+ { role: 'user', content: 'Now the second thing.' },
244
+ {
245
+ role: 'assistant',
246
+ content: 'Doing it.',
247
+ tool_calls: [
248
+ { id: 't2', name: 'bash', input: { command: 'echo 2' } },
249
+ { id: 't3', name: 'edit', input: { file: 'x.ts', old: 'a', new: 'b' } },
250
+ ],
251
+ },
252
+ { role: 'tool', content: '2', tool_call_id: 't2', name: 'bash' },
253
+ { role: 'tool', content: 'Edited x.ts', tool_call_id: 't3', name: 'edit' },
254
+ { role: 'assistant', content: 'Done!' },
255
+ ]
256
+
257
+ const data = {
258
+ id: sessionId,
259
+ cwd: path.resolve(config.cwd),
260
+ messages,
261
+ totalInputTokens: 0,
262
+ totalOutputTokens: 0,
263
+ totalApiCalls: 0,
264
+ mode: 'code',
265
+ model: 'test-model',
266
+ provider: 'test-provider',
267
+ createdAt: new Date().toISOString(),
268
+ updatedAt: new Date().toISOString(),
269
+ }
270
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8')
271
+
272
+ const session = await Session.load(config)
273
+ expect(session).not.toBeNull()
274
+ expect(session!.messages.length).toBe(messages.length)
275
+
276
+ // Both assistant messages with tool_calls should be present
277
+ const assistantWithTools = session!.messages.filter(
278
+ m => m.role === 'assistant' && m.tool_calls && m.tool_calls.length > 0,
279
+ )
280
+ expect(assistantWithTools).toHaveLength(2)
281
+ expect(assistantWithTools[0].content).toBe('Working on it.')
282
+ expect(assistantWithTools[1].content).toBe('Doing it.')
283
+ })
284
+ })
@@ -0,0 +1,274 @@
1
+ import type { Config } from '../config/index.js'
2
+ import { resetTokenUsage } from '../config/tokens.js'
3
+ import { fmtErr } from '../tools/errors.js'
4
+ import { resetGlobalEventBus } from './event-bus.js'
5
+ import type { LLMMessage } from './llm.js'
6
+ import { ensurePromptsDir, loadPromptTemplates } from './prompt-templates.js'
7
+ import { Session } from './session.js'
8
+ import { ensureSkillsDir, loadSkills } from './skills.js'
9
+
10
+ export interface CommandUI {
11
+ write(text: string): void
12
+ replaceContent(text: string): void
13
+ onStateChange(): void
14
+ onNewSession(session: Session): void
15
+ }
16
+
17
+ export interface CommandEnv {
18
+ session: Session
19
+ config: Config
20
+ ui: CommandUI
21
+ isRunning: boolean
22
+ }
23
+
24
+ export function handleNew(env: CommandEnv): Session {
25
+ env.session.stop()
26
+ Session.clearSavedSession(env.config.cwd)
27
+ resetTokenUsage(env.config.cwd)
28
+ resetGlobalEventBus()
29
+ const newSession = new Session(env.config, env.session.output)
30
+ newSession.onPlanWritten = env.session.onPlanWritten
31
+ env.ui.replaceContent('')
32
+ env.ui.onNewSession(newSession)
33
+ env.ui.onStateChange()
34
+ return newSession
35
+ }
36
+
37
+ export async function handleMode(env: CommandEnv, mode: string): Promise<boolean> {
38
+ if (mode !== 'code' && mode !== 'plan' && mode !== 'ask' && mode !== 'loop') {
39
+ env.ui.write(`Usage: /mode code|plan|ask|loop (current: ${env.session.config.mode})`)
40
+ return true
41
+ }
42
+ await env.session.setMode(mode)
43
+ env.ui.write(`Switched to ${mode} mode`)
44
+ env.ui.onStateChange()
45
+ return true
46
+ }
47
+
48
+ export function handleModel(env: CommandEnv, model: string): boolean {
49
+ if (!model) {
50
+ env.ui.write(`Current model: ${env.session.config.model}`)
51
+ return true
52
+ }
53
+ env.session.config.model = model
54
+ // Trigger system prompt rebuild by re-setting mode
55
+ env.session.setMode(env.session.config.mode)
56
+ env.ui.write(`Model switched to ${model}`)
57
+ env.ui.onStateChange()
58
+ return true
59
+ }
60
+
61
+ export function handlePrompts(env: CommandEnv): boolean {
62
+ const templates = loadPromptTemplates(env.config.cwd)
63
+ if (templates.length === 0) {
64
+ env.ui.write('No prompt templates found. Create .md files in .lonny/prompts/')
65
+ } else {
66
+ let msg = `Prompt Templates (${templates.length}):\n`
67
+ for (const t of templates) {
68
+ msg += ` \u2022 ${t.name}`
69
+ if (t.description) msg += ` \u2014 ${t.description}`
70
+ msg += '\n'
71
+ }
72
+ env.ui.write(msg.trimEnd())
73
+ }
74
+ return true
75
+ }
76
+
77
+ export function handleSkills(env: CommandEnv): boolean {
78
+ const skills = loadSkills(env.config.cwd)
79
+ if (skills.length === 0) {
80
+ env.ui.write('No skills loaded. Create .md files in .lonny/skills/')
81
+ } else {
82
+ let msg = `Active Skills (${skills.length}):\n`
83
+ for (const s of skills) {
84
+ msg += ` \u2022 ${s.name}`
85
+ if (s.description) msg += ` \u2014 ${s.description}`
86
+ msg += '\n'
87
+ }
88
+ env.ui.write(msg.trimEnd())
89
+ }
90
+ return true
91
+ }
92
+
93
+ export function handleSessions(env: CommandEnv): boolean {
94
+ const allSessions = Session.listSessions()
95
+ if (allSessions.length === 0) {
96
+ env.ui.write('No saved sessions found.')
97
+ return true
98
+ }
99
+ let msg = `Saved Sessions (${allSessions.length}):\n`
100
+ msg +=
101
+ ' ID Title Mode Messages Tokens Updated\n'
102
+ for (const s of allSessions) {
103
+ const id = s.id.padEnd(9)
104
+ const title = (s.title || '(untitled)').slice(0, 42).padEnd(43)
105
+ const mode = s.mode.padEnd(7)
106
+ const msgs = String(s.messageCount).padEnd(9)
107
+ const tokens = String(s.totalInputTokens + s.totalOutputTokens).padEnd(9)
108
+ const date = s.updatedAt.slice(0, 10)
109
+ msg += ` ${id} ${title} ${mode} ${msgs} ${tokens} ${date}\n`
110
+ }
111
+ msg += '\n Use /session delete <id> to delete a session'
112
+ env.ui.write(msg)
113
+ return true
114
+ }
115
+
116
+ export async function handleSessionCommand(env: CommandEnv, arg: string): Promise<boolean> {
117
+ if (arg === 'delete' || arg.startsWith('delete ')) {
118
+ const id = arg.slice(arg.startsWith('delete ') ? 7 : 6).trim()
119
+ if (id) {
120
+ const deleted = Session.deleteSession(id)
121
+ if (deleted) {
122
+ env.ui.write(`Deleted session ${id}`)
123
+ } else {
124
+ env.ui.write(`Session not found: ${id}`)
125
+ }
126
+ } else {
127
+ env.ui.write('Usage: /session delete <id>')
128
+ }
129
+ return true
130
+ }
131
+
132
+ if (arg.startsWith('title ')) {
133
+ const title = arg.slice(6).trim()
134
+ if (title) {
135
+ env.session.sessionTitle = title
136
+ env.session.save()
137
+ env.ui.write(`Session titled: ${title}`)
138
+ } else {
139
+ env.ui.write('Usage: /session title <name>')
140
+ }
141
+ return true
142
+ }
143
+
144
+ if (arg === 'export') {
145
+ try {
146
+ const filePath = env.session.exportSession()
147
+ env.ui.write(`Session exported to ${filePath}`)
148
+ } catch (err) {
149
+ env.ui.write(`Export failed: ${fmtErr(err)}`)
150
+ }
151
+ return true
152
+ }
153
+
154
+ if (arg === 'switch' || arg.startsWith('switch ')) {
155
+ const id = arg.startsWith('switch ') ? arg.slice(7).trim() : ''
156
+ if (!id) {
157
+ env.ui.write('Usage: /session switch <id>. Use /sessions to list session IDs.')
158
+ return true
159
+ }
160
+ if (env.isRunning) {
161
+ env.ui.write('Cannot switch session while agent is running. Wait or use /stop first.')
162
+ return true
163
+ }
164
+ const switched = await Session.loadById(id, env.config, env.session.output)
165
+ if (switched) {
166
+ switched.onPlanWritten = env.session.onPlanWritten
167
+ const preview = lastUserQuestion(switched.messages)
168
+ let msg = `Switched to session ${switched.sessionTitle || switched.sessionId}`
169
+ if (preview) msg += ` Last question: ${preview}`
170
+ env.ui.replaceContent(msg + '\n')
171
+ env.ui.onNewSession(switched)
172
+ env.ui.onStateChange()
173
+ } else {
174
+ env.ui.write(`Session not found: ${id}`)
175
+ }
176
+ return true
177
+ }
178
+
179
+ // Default: show current session info
180
+ const id = env.session.sessionId
181
+ const title = env.session.sessionTitle || '(untitled)'
182
+ const mode = env.session.config.mode
183
+ const model = env.session.config.model
184
+ const provider = env.session.config.provider
185
+ const msgs = env.session.messages.length
186
+ const totalIn = env.session.totalInputTokens
187
+ const totalOut = env.session.totalOutputTokens
188
+ const totalApi = env.session.totalApiCalls
189
+ const created = env.session.sessionCreatedAt.slice(0, 10)
190
+ const info =
191
+ '' +
192
+ `ID: ${id}\n` +
193
+ `Title: ${title}\n` +
194
+ `Mode: ${mode}\n` +
195
+ `Model: ${model} (${provider})\n` +
196
+ `Messages: ${msgs}\n` +
197
+ `Tokens: ${totalIn + totalOut} (in: ${totalIn}, out: ${totalOut})\n` +
198
+ `API Calls: ${totalApi}\n` +
199
+ `Created: ${created}\n`
200
+ env.ui.write(info.trimEnd())
201
+ return true
202
+ }
203
+
204
+ export async function handleFork(env: CommandEnv): Promise<boolean> {
205
+ if (env.isRunning) {
206
+ env.ui.write('Cannot fork while agent is running. Wait or use /stop first.')
207
+ return true
208
+ }
209
+ if (env.session.messages.length <= 1) {
210
+ env.ui.write('No conversation to fork. Start a conversation first.')
211
+ return true
212
+ }
213
+ const forked = env.session.fork()
214
+ forked.onPlanWritten = env.session.onPlanWritten
215
+ const baseTitle = forked.sessionTitle || 'forked session'
216
+ env.ui.write(`Forked new session: ${baseTitle}`)
217
+ env.ui.write(` ID: ${forked.sessionId}`)
218
+ env.ui.onNewSession(forked)
219
+ env.ui.onStateChange()
220
+ return true
221
+ }
222
+
223
+ export function handleInit(env: CommandEnv): boolean {
224
+ ensureSkillsDir(env.config.cwd)
225
+ ensurePromptsDir(env.config.cwd)
226
+ env.ui.write('Initialized .lonny/skills/ and .lonny/prompts/')
227
+ return true
228
+ }
229
+
230
+ export function handleStop(env: CommandEnv): boolean {
231
+ if (!env.isRunning) {
232
+ env.ui.write('Agent is not running.')
233
+ return true
234
+ }
235
+ env.session.stop()
236
+ env.ui.write('Stopping agent...')
237
+ return true
238
+ }
239
+
240
+ function lastUserQuestion(messages: LLMMessage[]): string | null {
241
+ const lastUserMsg = [...messages].reverse().find(m => m.role === 'user')
242
+ if (!lastUserMsg || typeof lastUserMsg.content !== 'string') return null
243
+ return lastUserMsg.content.length > 80
244
+ ? `${lastUserMsg.content.slice(0, 80)}\u2026`
245
+ : lastUserMsg.content
246
+ }
247
+
248
+ export async function dispatchCommand(env: CommandEnv, cmd: string, arg: string): Promise<boolean> {
249
+ switch (cmd) {
250
+ case 'new':
251
+ handleNew(env)
252
+ return true
253
+ case 'mode':
254
+ return handleMode(env, arg)
255
+ case 'model':
256
+ return handleModel(env, arg)
257
+ case 'prompts':
258
+ return handlePrompts(env)
259
+ case 'skills':
260
+ return handleSkills(env)
261
+ case 'sessions':
262
+ return handleSessions(env)
263
+ case 'session':
264
+ return handleSessionCommand(env, arg)
265
+ case 'fork':
266
+ return handleFork(env)
267
+ case 'init':
268
+ return handleInit(env)
269
+ case 'stop':
270
+ return handleStop(env)
271
+ default:
272
+ return false
273
+ }
274
+ }