lonny-agent 0.2.4 → 0.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (509) hide show
  1. package/.github/workflows/ci.yml +21 -0
  2. package/AGENTS.md +106 -0
  3. package/CHANGELOG.md +24 -0
  4. package/README.md +326 -326
  5. package/dist/agent/__tests__/session-restore.test.d.ts +2 -0
  6. package/dist/agent/__tests__/session-restore.test.d.ts.map +1 -0
  7. package/dist/agent/__tests__/session-restore.test.js +260 -0
  8. package/dist/agent/__tests__/session-restore.test.js.map +1 -0
  9. package/dist/agent/commands.d.ts +26 -0
  10. package/dist/agent/commands.d.ts.map +1 -0
  11. package/dist/agent/commands.js +253 -0
  12. package/dist/agent/commands.js.map +1 -0
  13. package/dist/agent/compaction.js +8 -1
  14. package/dist/agent/compaction.js.map +1 -1
  15. package/dist/agent/index.d.ts +2 -1
  16. package/dist/agent/index.d.ts.map +1 -1
  17. package/dist/agent/index.js +2 -2
  18. package/dist/agent/index.js.map +1 -1
  19. package/dist/agent/llm.d.ts +4 -0
  20. package/dist/agent/llm.d.ts.map +1 -1
  21. package/dist/agent/memory.d.ts +12 -0
  22. package/dist/agent/memory.d.ts.map +1 -0
  23. package/dist/agent/memory.js +51 -0
  24. package/dist/agent/memory.js.map +1 -0
  25. package/dist/agent/prompt-builder.d.ts +6 -1
  26. package/dist/agent/prompt-builder.d.ts.map +1 -1
  27. package/dist/agent/prompt-builder.js +100 -33
  28. package/dist/agent/prompt-builder.js.map +1 -1
  29. package/dist/agent/providers/google.d.ts.map +1 -1
  30. package/dist/agent/providers/google.js.map +1 -1
  31. package/dist/agent/providers/openai.d.ts +1 -0
  32. package/dist/agent/providers/openai.d.ts.map +1 -1
  33. package/dist/agent/providers/openai.js +59 -6
  34. package/dist/agent/providers/openai.js.map +1 -1
  35. package/dist/agent/session-chat.d.ts +3 -0
  36. package/dist/agent/session-chat.d.ts.map +1 -0
  37. package/dist/agent/session-chat.js +384 -0
  38. package/dist/agent/session-chat.js.map +1 -0
  39. package/dist/agent/session-display.d.ts +29 -0
  40. package/dist/agent/session-display.d.ts.map +1 -0
  41. package/dist/agent/session-display.js +179 -0
  42. package/dist/agent/session-display.js.map +1 -0
  43. package/dist/agent/session-persistence.d.ts +55 -0
  44. package/dist/agent/session-persistence.d.ts.map +1 -0
  45. package/dist/agent/session-persistence.js +101 -0
  46. package/dist/agent/session-persistence.js.map +1 -0
  47. package/dist/agent/session-utils.d.ts +5 -0
  48. package/dist/agent/session-utils.d.ts.map +1 -0
  49. package/dist/agent/session-utils.js +51 -0
  50. package/dist/agent/session-utils.js.map +1 -0
  51. package/dist/agent/session.d.ts +19 -17
  52. package/dist/agent/session.d.ts.map +1 -1
  53. package/dist/agent/session.js +234 -578
  54. package/dist/agent/session.js.map +1 -1
  55. package/dist/cli/index.d.ts +4 -0
  56. package/dist/cli/index.d.ts.map +1 -1
  57. package/dist/cli/index.js +67 -2
  58. package/dist/cli/index.js.map +1 -1
  59. package/dist/config/index.d.ts +2 -2
  60. package/dist/config/index.d.ts.map +1 -1
  61. package/dist/config/index.js +7 -1
  62. package/dist/config/index.js.map +1 -1
  63. package/dist/index.js +24 -3
  64. package/dist/index.js.map +1 -1
  65. package/dist/tools/__tests__/bash.test.js +169 -2
  66. package/dist/tools/__tests__/bash.test.js.map +1 -1
  67. package/dist/tools/__tests__/edit.test.js +1091 -340
  68. package/dist/tools/__tests__/edit.test.js.map +1 -1
  69. package/dist/tools/__tests__/fetch.test.js +1 -1
  70. package/dist/tools/__tests__/fetch.test.js.map +1 -1
  71. package/dist/tools/__tests__/glob.test.js +1 -1
  72. package/dist/tools/__tests__/glob.test.js.map +1 -1
  73. package/dist/tools/__tests__/grep.test.js +33 -1
  74. package/dist/tools/__tests__/grep.test.js.map +1 -1
  75. package/dist/tools/__tests__/ls.test.js +1 -1
  76. package/dist/tools/__tests__/ls.test.js.map +1 -1
  77. package/dist/tools/__tests__/read.test.js +1 -1
  78. package/dist/tools/__tests__/read.test.js.map +1 -1
  79. package/dist/tools/__tests__/registry.test.js +313 -0
  80. package/dist/tools/__tests__/registry.test.js.map +1 -1
  81. package/dist/tools/__tests__/sed.test.d.ts +2 -0
  82. package/dist/tools/__tests__/sed.test.d.ts.map +1 -0
  83. package/dist/tools/__tests__/sed.test.js +228 -0
  84. package/dist/tools/__tests__/sed.test.js.map +1 -0
  85. package/dist/tools/__tests__/write_plan.test.js +1 -1
  86. package/dist/tools/__tests__/write_plan.test.js.map +1 -1
  87. package/dist/tools/bash.d.ts.map +1 -1
  88. package/dist/tools/bash.js +711 -18
  89. package/dist/tools/bash.js.map +1 -1
  90. package/dist/tools/codebase/ast/__tests__/ast-edit-demo.d.ts +2 -0
  91. package/dist/tools/codebase/ast/__tests__/ast-edit-demo.d.ts.map +1 -0
  92. package/dist/tools/codebase/ast/__tests__/ast-edit-demo.js +7 -0
  93. package/dist/tools/codebase/ast/__tests__/ast-edit-demo.js.map +1 -0
  94. package/dist/tools/codebase/ast/__tests__/ast-edit-final.d.ts +6 -0
  95. package/dist/tools/codebase/ast/__tests__/ast-edit-final.d.ts.map +1 -0
  96. package/dist/tools/codebase/ast/__tests__/ast-edit-final.js +11 -0
  97. package/dist/tools/codebase/ast/__tests__/ast-edit-final.js.map +1 -0
  98. package/dist/tools/codebase/ast/__tests__/ast-edit-test.d.ts +2 -0
  99. package/dist/tools/codebase/ast/__tests__/ast-edit-test.d.ts.map +1 -0
  100. package/dist/tools/codebase/ast/__tests__/ast-edit-test.js +9 -0
  101. package/dist/tools/codebase/ast/__tests__/ast-edit-test.js.map +1 -0
  102. package/dist/tools/codebase/ast/__tests__/integration.test.d.ts +2 -0
  103. package/dist/tools/codebase/ast/__tests__/integration.test.d.ts.map +1 -0
  104. package/dist/tools/codebase/ast/__tests__/integration.test.js +415 -0
  105. package/dist/tools/codebase/ast/__tests__/integration.test.js.map +1 -0
  106. package/dist/tools/codebase/ast/__tests__/tree-sitter.test.d.ts +2 -0
  107. package/dist/tools/codebase/ast/__tests__/tree-sitter.test.d.ts.map +1 -0
  108. package/dist/tools/codebase/ast/__tests__/tree-sitter.test.js +414 -0
  109. package/dist/tools/codebase/ast/__tests__/tree-sitter.test.js.map +1 -0
  110. package/dist/tools/codebase/ast/adapter.d.ts +23 -0
  111. package/dist/tools/codebase/ast/adapter.d.ts.map +1 -0
  112. package/dist/tools/codebase/ast/adapter.js +11 -0
  113. package/dist/tools/codebase/ast/adapter.js.map +1 -0
  114. package/dist/tools/codebase/ast/tools.d.ts +3 -0
  115. package/dist/tools/codebase/ast/tools.d.ts.map +1 -0
  116. package/dist/tools/codebase/ast/tools.js +286 -0
  117. package/dist/tools/codebase/ast/tools.js.map +1 -0
  118. package/dist/tools/codebase/ast/tree-sitter-adapter.d.ts +3 -0
  119. package/dist/tools/codebase/ast/tree-sitter-adapter.d.ts.map +1 -0
  120. package/dist/tools/codebase/ast/tree-sitter-adapter.js +412 -0
  121. package/dist/tools/codebase/ast/tree-sitter-adapter.js.map +1 -0
  122. package/dist/tools/codebase/ast/tree-sitter-converters.d.ts +4 -0
  123. package/dist/tools/codebase/ast/tree-sitter-converters.d.ts.map +1 -0
  124. package/dist/tools/codebase/ast/tree-sitter-converters.js +447 -0
  125. package/dist/tools/codebase/ast/tree-sitter-converters.js.map +1 -0
  126. package/dist/tools/codebase/ast/types.d.ts +103 -0
  127. package/dist/tools/codebase/ast/types.d.ts.map +1 -0
  128. package/dist/tools/codebase/ast/types.js +2 -0
  129. package/dist/tools/codebase/ast/types.js.map +1 -0
  130. package/dist/tools/codebase/ast/wasm/tree-sitter-python.wasm +0 -0
  131. package/dist/tools/codebase/ast/wasm/tree-sitter-tsx.wasm +0 -0
  132. package/dist/tools/codebase/ast/wasm/tree-sitter-typescript.wasm +0 -0
  133. package/dist/tools/codebase/ast/wasm/tree-sitter.wasm +0 -0
  134. package/dist/tools/codebase/ast/wasm/web-tree-sitter.wasm +0 -0
  135. package/dist/tools/codebase/find.d.ts +3 -0
  136. package/dist/tools/codebase/find.d.ts.map +1 -0
  137. package/dist/tools/codebase/find.js +61 -0
  138. package/dist/tools/codebase/find.js.map +1 -0
  139. package/dist/tools/codebase/glob.d.ts +3 -0
  140. package/dist/tools/codebase/glob.d.ts.map +1 -0
  141. package/dist/tools/codebase/glob.js +37 -0
  142. package/dist/tools/codebase/glob.js.map +1 -0
  143. package/dist/tools/codebase/grep.d.ts +3 -0
  144. package/dist/tools/codebase/grep.d.ts.map +1 -0
  145. package/dist/tools/codebase/grep.js +169 -0
  146. package/dist/tools/codebase/grep.js.map +1 -0
  147. package/dist/tools/codebase/ls.d.ts +3 -0
  148. package/dist/tools/codebase/ls.d.ts.map +1 -0
  149. package/dist/tools/codebase/ls.js +39 -0
  150. package/dist/tools/codebase/ls.js.map +1 -0
  151. package/dist/tools/codebase/read.d.ts +10 -0
  152. package/dist/tools/codebase/read.d.ts.map +1 -0
  153. package/dist/tools/codebase/read.js +81 -0
  154. package/dist/tools/codebase/read.js.map +1 -0
  155. package/dist/tools/delete_memory.d.ts +3 -0
  156. package/dist/tools/delete_memory.d.ts.map +1 -0
  157. package/dist/tools/delete_memory.js +46 -0
  158. package/dist/tools/delete_memory.js.map +1 -0
  159. package/dist/tools/edit/diff-compute.d.ts +4 -0
  160. package/dist/tools/edit/diff-compute.d.ts.map +1 -0
  161. package/dist/tools/edit/diff-compute.js +18 -0
  162. package/dist/tools/edit/diff-compute.js.map +1 -0
  163. package/dist/tools/edit/diff-render.d.ts +18 -0
  164. package/dist/tools/edit/diff-render.d.ts.map +1 -0
  165. package/dist/tools/edit/diff-render.js +128 -0
  166. package/dist/tools/edit/diff-render.js.map +1 -0
  167. package/dist/tools/edit/edit.d.ts +4 -0
  168. package/dist/tools/edit/edit.d.ts.map +1 -0
  169. package/dist/tools/edit/edit.js +440 -0
  170. package/dist/tools/edit/edit.js.map +1 -0
  171. package/dist/tools/edit/matcher.d.ts +24 -0
  172. package/dist/tools/edit/matcher.d.ts.map +1 -0
  173. package/dist/tools/edit/matcher.js +66 -0
  174. package/dist/tools/edit/matcher.js.map +1 -0
  175. package/dist/tools/edit/parser.d.ts +10 -0
  176. package/dist/tools/edit/parser.d.ts.map +1 -0
  177. package/dist/tools/edit/parser.js +120 -0
  178. package/dist/tools/edit/parser.js.map +1 -0
  179. package/dist/tools/edit/types.d.ts +16 -0
  180. package/dist/tools/edit/types.d.ts.map +1 -0
  181. package/dist/tools/edit/types.js +2 -0
  182. package/dist/tools/edit/types.js.map +1 -0
  183. package/dist/tools/edit/write_plan.d.ts +5 -0
  184. package/dist/tools/edit/write_plan.d.ts.map +1 -0
  185. package/dist/tools/edit/write_plan.js +98 -0
  186. package/dist/tools/edit/write_plan.js.map +1 -0
  187. package/dist/tools/edit.d.ts +50 -9
  188. package/dist/tools/edit.d.ts.map +1 -1
  189. package/dist/tools/edit.js +394 -159
  190. package/dist/tools/edit.js.map +1 -1
  191. package/dist/tools/execute/bash/constants.d.ts +24 -0
  192. package/dist/tools/execute/bash/constants.d.ts.map +1 -0
  193. package/dist/tools/execute/bash/constants.js +88 -0
  194. package/dist/tools/execute/bash/constants.js.map +1 -0
  195. package/dist/tools/execute/bash/errors.d.ts +39 -0
  196. package/dist/tools/execute/bash/errors.d.ts.map +1 -0
  197. package/dist/tools/execute/bash/errors.js +249 -0
  198. package/dist/tools/execute/bash/errors.js.map +1 -0
  199. package/dist/tools/execute/bash/execution.d.ts +18 -0
  200. package/dist/tools/execute/bash/execution.d.ts.map +1 -0
  201. package/dist/tools/execute/bash/execution.js +207 -0
  202. package/dist/tools/execute/bash/execution.js.map +1 -0
  203. package/dist/tools/execute/bash/index.d.ts +3 -0
  204. package/dist/tools/execute/bash/index.d.ts.map +1 -0
  205. package/dist/tools/execute/bash/index.js +117 -0
  206. package/dist/tools/execute/bash/index.js.map +1 -0
  207. package/dist/tools/execute/bash/platform.d.ts +6 -0
  208. package/dist/tools/execute/bash/platform.d.ts.map +1 -0
  209. package/dist/tools/execute/bash/platform.js +64 -0
  210. package/dist/tools/execute/bash/platform.js.map +1 -0
  211. package/dist/tools/execute/bash/security.d.ts +12 -0
  212. package/dist/tools/execute/bash/security.d.ts.map +1 -0
  213. package/dist/tools/execute/bash/security.js +26 -0
  214. package/dist/tools/execute/bash/security.js.map +1 -0
  215. package/dist/tools/execute/bash/validation.d.ts +16 -0
  216. package/dist/tools/execute/bash/validation.d.ts.map +1 -0
  217. package/dist/tools/execute/bash/validation.js +57 -0
  218. package/dist/tools/execute/bash/validation.js.map +1 -0
  219. package/dist/tools/execute/bash.d.ts +3 -0
  220. package/dist/tools/execute/bash.d.ts.map +1 -0
  221. package/dist/tools/execute/bash.js +728 -0
  222. package/dist/tools/execute/bash.js.map +1 -0
  223. package/dist/tools/execute/git.d.ts +3 -0
  224. package/dist/tools/execute/git.d.ts.map +1 -0
  225. package/dist/tools/execute/git.js +183 -0
  226. package/dist/tools/execute/git.js.map +1 -0
  227. package/dist/tools/execute/task_complete.d.ts +7 -0
  228. package/dist/tools/execute/task_complete.d.ts.map +1 -0
  229. package/dist/tools/execute/task_complete.js +38 -0
  230. package/dist/tools/execute/task_complete.js.map +1 -0
  231. package/dist/tools/fetch.d.ts.map +1 -1
  232. package/dist/tools/fetch.js +2 -0
  233. package/dist/tools/fetch.js.map +1 -1
  234. package/dist/tools/find.d.ts.map +1 -1
  235. package/dist/tools/find.js +2 -0
  236. package/dist/tools/find.js.map +1 -1
  237. package/dist/tools/git.d.ts.map +1 -1
  238. package/dist/tools/git.js +2 -0
  239. package/dist/tools/git.js.map +1 -1
  240. package/dist/tools/glob.d.ts.map +1 -1
  241. package/dist/tools/glob.js +2 -0
  242. package/dist/tools/glob.js.map +1 -1
  243. package/dist/tools/grep.d.ts.map +1 -1
  244. package/dist/tools/grep.js +56 -30
  245. package/dist/tools/grep.js.map +1 -1
  246. package/dist/tools/install/clawhub.d.ts +26 -0
  247. package/dist/tools/install/clawhub.d.ts.map +1 -0
  248. package/dist/tools/install/clawhub.js +182 -0
  249. package/dist/tools/install/clawhub.js.map +1 -0
  250. package/dist/tools/install/constants.d.ts +5 -0
  251. package/dist/tools/install/constants.d.ts.map +1 -0
  252. package/dist/tools/install/constants.js +5 -0
  253. package/dist/tools/install/constants.js.map +1 -0
  254. package/dist/tools/install/install_skill.d.ts +3 -0
  255. package/dist/tools/install/install_skill.d.ts.map +1 -0
  256. package/dist/tools/install/install_skill.js +132 -0
  257. package/dist/tools/install/install_skill.js.map +1 -0
  258. package/dist/tools/install/npm.d.ts +8 -0
  259. package/dist/tools/install/npm.d.ts.map +1 -0
  260. package/dist/tools/install/npm.js +52 -0
  261. package/dist/tools/install/npm.js.map +1 -0
  262. package/dist/tools/install/skill-content.d.ts +9 -0
  263. package/dist/tools/install/skill-content.d.ts.map +1 -0
  264. package/dist/tools/install/skill-content.js +89 -0
  265. package/dist/tools/install/skill-content.js.map +1 -0
  266. package/dist/tools/install_skill.d.ts.map +1 -1
  267. package/dist/tools/install_skill.js +2 -0
  268. package/dist/tools/install_skill.js.map +1 -1
  269. package/dist/tools/list_memory.d.ts +3 -0
  270. package/dist/tools/list_memory.d.ts.map +1 -0
  271. package/dist/tools/list_memory.js +55 -0
  272. package/dist/tools/list_memory.js.map +1 -0
  273. package/dist/tools/ls.d.ts.map +1 -1
  274. package/dist/tools/ls.js +2 -0
  275. package/dist/tools/ls.js.map +1 -1
  276. package/dist/tools/memory/delete_memory.d.ts +3 -0
  277. package/dist/tools/memory/delete_memory.d.ts.map +1 -0
  278. package/dist/tools/memory/delete_memory.js +46 -0
  279. package/dist/tools/memory/delete_memory.js.map +1 -0
  280. package/dist/tools/memory/list_memory.d.ts +3 -0
  281. package/dist/tools/memory/list_memory.d.ts.map +1 -0
  282. package/dist/tools/memory/list_memory.js +55 -0
  283. package/dist/tools/memory/list_memory.js.map +1 -0
  284. package/dist/tools/memory/save_memory.d.ts +3 -0
  285. package/dist/tools/memory/save_memory.d.ts.map +1 -0
  286. package/dist/tools/memory/save_memory.js +45 -0
  287. package/dist/tools/memory/save_memory.js.map +1 -0
  288. package/dist/tools/read.d.ts.map +1 -1
  289. package/dist/tools/read.js +2 -0
  290. package/dist/tools/read.js.map +1 -1
  291. package/dist/tools/registry.d.ts +7 -8
  292. package/dist/tools/registry.d.ts.map +1 -1
  293. package/dist/tools/registry.js +114 -46
  294. package/dist/tools/registry.js.map +1 -1
  295. package/dist/tools/save_memory.d.ts +3 -0
  296. package/dist/tools/save_memory.d.ts.map +1 -0
  297. package/dist/tools/save_memory.js +45 -0
  298. package/dist/tools/save_memory.js.map +1 -0
  299. package/dist/tools/search.d.ts.map +1 -1
  300. package/dist/tools/search.js +2 -0
  301. package/dist/tools/search.js.map +1 -1
  302. package/dist/tools/sed.d.ts +4 -0
  303. package/dist/tools/sed.d.ts.map +1 -0
  304. package/dist/tools/sed.js +121 -0
  305. package/dist/tools/sed.js.map +1 -0
  306. package/dist/tools/tree.d.ts +12 -0
  307. package/dist/tools/tree.d.ts.map +1 -0
  308. package/dist/tools/tree.js +87 -0
  309. package/dist/tools/tree.js.map +1 -0
  310. package/dist/tools/types.d.ts +12 -0
  311. package/dist/tools/types.d.ts.map +1 -1
  312. package/dist/tools/web/fetch.d.ts +3 -0
  313. package/dist/tools/web/fetch.d.ts.map +1 -0
  314. package/dist/tools/web/fetch.js +49 -0
  315. package/dist/tools/web/fetch.js.map +1 -0
  316. package/dist/tools/web/search.d.ts +3 -0
  317. package/dist/tools/web/search.d.ts.map +1 -0
  318. package/dist/tools/web/search.js +134 -0
  319. package/dist/tools/web/search.js.map +1 -0
  320. package/dist/tools/write_plan.d.ts.map +1 -1
  321. package/dist/tools/write_plan.js +2 -0
  322. package/dist/tools/write_plan.js.map +1 -1
  323. package/dist/tui/commands.d.ts +4 -0
  324. package/dist/tui/commands.d.ts.map +1 -0
  325. package/dist/tui/commands.js +118 -0
  326. package/dist/tui/commands.js.map +1 -0
  327. package/dist/tui/components/colors.d.ts +36 -0
  328. package/dist/tui/components/colors.d.ts.map +1 -0
  329. package/dist/tui/components/colors.js +48 -0
  330. package/dist/tui/components/colors.js.map +1 -0
  331. package/dist/tui/components/header-bar.d.ts +26 -0
  332. package/dist/tui/components/header-bar.d.ts.map +1 -0
  333. package/dist/tui/components/header-bar.js +78 -0
  334. package/dist/tui/components/header-bar.js.map +1 -0
  335. package/dist/tui/components/index.d.ts +9 -0
  336. package/dist/tui/components/index.d.ts.map +1 -0
  337. package/dist/tui/components/index.js +9 -0
  338. package/dist/tui/components/index.js.map +1 -0
  339. package/dist/tui/components/landing-screen.d.ts +12 -0
  340. package/dist/tui/components/landing-screen.d.ts.map +1 -0
  341. package/dist/tui/components/landing-screen.js +53 -0
  342. package/dist/tui/components/landing-screen.js.map +1 -0
  343. package/dist/tui/components/pixel-logo.d.ts +5 -0
  344. package/dist/tui/components/pixel-logo.d.ts.map +1 -0
  345. package/dist/tui/components/pixel-logo.js +27 -0
  346. package/dist/tui/components/pixel-logo.js.map +1 -0
  347. package/dist/tui/components/plan-utils.d.ts +11 -0
  348. package/dist/tui/components/plan-utils.d.ts.map +1 -0
  349. package/dist/tui/components/plan-utils.js +62 -0
  350. package/dist/tui/components/plan-utils.js.map +1 -0
  351. package/dist/tui/components/plans-list.d.ts +18 -0
  352. package/dist/tui/components/plans-list.d.ts.map +1 -0
  353. package/dist/tui/components/plans-list.js +46 -0
  354. package/dist/tui/components/plans-list.js.map +1 -0
  355. package/dist/tui/components/rich-footer.d.ts +29 -0
  356. package/dist/tui/components/rich-footer.d.ts.map +1 -0
  357. package/dist/tui/components/rich-footer.js +124 -0
  358. package/dist/tui/components/rich-footer.js.map +1 -0
  359. package/dist/tui/components/todo-panel.d.ts +14 -0
  360. package/dist/tui/components/todo-panel.d.ts.map +1 -0
  361. package/dist/tui/components/todo-panel.js +83 -0
  362. package/dist/tui/components/todo-panel.js.map +1 -0
  363. package/dist/tui/components.d.ts.map +1 -1
  364. package/dist/tui/components.js +14 -7
  365. package/dist/tui/components.js.map +1 -1
  366. package/dist/tui/index.d.ts +2 -1
  367. package/dist/tui/index.d.ts.map +1 -1
  368. package/dist/tui/index.js +99 -505
  369. package/dist/tui/index.js.map +1 -1
  370. package/dist/tui/overlays.d.ts +28 -0
  371. package/dist/tui/overlays.d.ts.map +1 -0
  372. package/dist/tui/overlays.js +154 -0
  373. package/dist/tui/overlays.js.map +1 -0
  374. package/dist/tui/themes.d.ts +5 -0
  375. package/dist/tui/themes.d.ts.map +1 -0
  376. package/dist/tui/themes.js +38 -0
  377. package/dist/tui/themes.js.map +1 -0
  378. package/dist/web/index.d.ts.map +1 -1
  379. package/dist/web/index.js +62 -101
  380. package/dist/web/index.js.map +1 -1
  381. package/dist/web/public/app.js +5 -855
  382. package/dist/web/public/confirm.js +70 -0
  383. package/dist/web/public/index.html +121 -120
  384. package/dist/web/public/input.js +107 -0
  385. package/dist/web/public/messages.js +411 -0
  386. package/dist/web/public/sidebar.js +82 -0
  387. package/dist/web/public/state.js +72 -0
  388. package/dist/web/public/style.css +247 -70
  389. package/dist/web/public/utils.js +37 -0
  390. package/dist/web/public/websocket.js +267 -0
  391. package/dist/web/public/ws.js +19 -0
  392. package/dist/web/session-bridge.d.ts.map +1 -1
  393. package/dist/web/session-bridge.js +85 -10
  394. package/dist/web/session-bridge.js.map +1 -1
  395. package/docs/ast-tools.md +117 -0
  396. package/package.json +11 -5
  397. package/scripts/check-line-count.mjs +53 -0
  398. package/scripts/copy-ast-wasm.mjs +32 -0
  399. package/scripts/copy-native.mjs +24 -0
  400. package/scripts/copy-web.mjs +15 -0
  401. package/src/agent/__tests__/session-restore.test.ts +284 -0
  402. package/src/agent/commands.ts +274 -0
  403. package/src/agent/compaction.ts +9 -2
  404. package/src/agent/index.ts +7 -2
  405. package/src/agent/llm.ts +4 -0
  406. package/src/agent/memory.ts +60 -0
  407. package/src/agent/prompt-builder.ts +111 -33
  408. package/src/agent/providers/google.ts +0 -1
  409. package/src/agent/providers/openai.ts +79 -8
  410. package/src/agent/session-chat.ts +480 -0
  411. package/src/agent/session-display.ts +195 -0
  412. package/src/agent/session-persistence.ts +146 -0
  413. package/src/agent/session-utils.ts +54 -0
  414. package/src/agent/session.ts +269 -670
  415. package/src/cli/index.ts +73 -4
  416. package/src/config/index.ts +14 -7
  417. package/src/index.ts +30 -3
  418. package/src/tools/__tests__/bash.test.ts +188 -2
  419. package/src/tools/__tests__/edit.test.ts +1216 -356
  420. package/src/tools/__tests__/fetch.test.ts +1 -1
  421. package/src/tools/__tests__/glob.test.ts +1 -1
  422. package/src/tools/__tests__/grep.test.ts +36 -1
  423. package/src/tools/__tests__/ls.test.ts +1 -1
  424. package/src/tools/__tests__/read.test.ts +1 -1
  425. package/src/tools/__tests__/registry.test.ts +332 -0
  426. package/src/tools/__tests__/write_plan.test.ts +1 -1
  427. package/src/tools/codebase/ast/__tests__/integration.test.ts +452 -0
  428. package/src/tools/codebase/ast/__tests__/tree-sitter.test.ts +466 -0
  429. package/src/tools/codebase/ast/adapter.ts +44 -0
  430. package/src/tools/codebase/ast/tools.ts +322 -0
  431. package/src/tools/codebase/ast/tree-sitter-adapter.ts +468 -0
  432. package/src/tools/codebase/ast/tree-sitter-converters.ts +480 -0
  433. package/src/tools/codebase/ast/types.ts +102 -0
  434. package/src/tools/codebase/ast/wasm/tree-sitter-python.wasm +0 -0
  435. package/src/tools/codebase/ast/wasm/tree-sitter-tsx.wasm +0 -0
  436. package/src/tools/codebase/ast/wasm/tree-sitter-typescript.wasm +0 -0
  437. package/src/tools/codebase/ast/wasm/tree-sitter.wasm +0 -0
  438. package/src/tools/codebase/ast/wasm/web-tree-sitter.wasm +0 -0
  439. package/src/tools/{find.ts → codebase/find.ts} +4 -2
  440. package/src/tools/{glob.ts → codebase/glob.ts} +4 -2
  441. package/src/tools/{grep.ts → codebase/grep.ts} +61 -32
  442. package/src/tools/{ls.ts → codebase/ls.ts} +4 -2
  443. package/src/tools/{read.ts → codebase/read.ts} +5 -3
  444. package/src/tools/edit/diff-compute.ts +21 -0
  445. package/src/tools/edit/diff-render.ts +145 -0
  446. package/src/tools/edit/edit.ts +468 -0
  447. package/src/tools/edit/matcher.ts +70 -0
  448. package/src/tools/edit/parser.ts +131 -0
  449. package/src/tools/edit/types.ts +19 -0
  450. package/src/tools/{write_plan.ts → edit/write_plan.ts} +4 -2
  451. package/src/tools/execute/bash/constants.ts +93 -0
  452. package/src/tools/execute/bash/errors.ts +304 -0
  453. package/src/tools/execute/bash/execution.ts +211 -0
  454. package/src/tools/execute/bash/index.ts +131 -0
  455. package/src/tools/execute/bash/platform.ts +65 -0
  456. package/src/tools/execute/bash/security.ts +28 -0
  457. package/src/tools/execute/bash/validation.ts +78 -0
  458. package/src/tools/execute/git.ts +209 -0
  459. package/src/tools/execute/task_complete.ts +40 -0
  460. package/src/tools/install/clawhub.ts +222 -0
  461. package/src/tools/install/constants.ts +4 -0
  462. package/src/tools/install/install_skill.ts +150 -0
  463. package/src/tools/install/npm.ts +60 -0
  464. package/src/tools/install/skill-content.ts +108 -0
  465. package/src/tools/memory/delete_memory.ts +46 -0
  466. package/src/tools/memory/list_memory.ts +53 -0
  467. package/src/tools/memory/save_memory.ts +45 -0
  468. package/src/tools/registry.ts +121 -50
  469. package/src/tools/tree.ts +103 -0
  470. package/src/tools/types.ts +13 -0
  471. package/src/tools/{fetch.ts → web/fetch.ts} +4 -2
  472. package/src/tools/{search.ts → web/search.ts} +4 -2
  473. package/src/tui/commands.ts +135 -0
  474. package/src/tui/components/colors.ts +52 -0
  475. package/src/tui/components/header-bar.ts +88 -0
  476. package/src/tui/components/index.ts +8 -0
  477. package/src/tui/components/landing-screen.ts +67 -0
  478. package/src/tui/components/pixel-logo.ts +33 -0
  479. package/src/tui/components/plan-utils.ts +69 -0
  480. package/src/tui/components/plans-list.ts +55 -0
  481. package/src/tui/components/rich-footer.ts +146 -0
  482. package/src/tui/components/todo-panel.ts +95 -0
  483. package/src/tui/index.ts +110 -561
  484. package/src/tui/overlays.ts +203 -0
  485. package/src/tui/themes.ts +41 -0
  486. package/src/web/index.ts +66 -107
  487. package/src/web/public/app.js +5 -855
  488. package/src/web/public/confirm.js +70 -0
  489. package/src/web/public/index.html +121 -120
  490. package/src/web/public/input.js +107 -0
  491. package/src/web/public/messages.js +411 -0
  492. package/src/web/public/sidebar.js +82 -0
  493. package/src/web/public/state.js +72 -0
  494. package/src/web/public/style.css +247 -70
  495. package/src/web/public/utils.js +37 -0
  496. package/src/web/public/websocket.js +267 -0
  497. package/src/web/public/ws.js +19 -0
  498. package/src/web/session-bridge.ts +90 -12
  499. package/.claude/settings.local.json +0 -14
  500. package/.kilo/plans/1780064105789-mighty-pixel.md +0 -171
  501. package/.kilo/plans/1780293725888-quick-wizard.md +0 -62
  502. package/.lonny/plan-web-cwd-status.md +0 -38
  503. package/AGENT.md +0 -99
  504. package/src/tools/bash.ts +0 -39
  505. package/src/tools/edit.ts +0 -554
  506. package/src/tools/exec.ts +0 -348
  507. package/src/tools/git.ts +0 -76
  508. package/src/tools/install_skill.ts +0 -539
  509. package/src/tui/components.ts +0 -635
@@ -1,195 +1,17 @@
1
- import { createHash } from 'node:crypto';
2
1
  import * as fs from 'node:fs';
3
- import * as os from 'node:os';
4
2
  import * as path from 'node:path';
5
- import { saveTokenUsage } from '../config/tokens.js';
6
3
  import { FileReadTracker } from '../diff/apply.js';
7
- import { fmtErr } from '../tools/errors.js';
8
4
  import { ToolRegistry } from '../tools/registry.js';
9
- import { compact, shouldCompact } from './compaction.js';
10
- import { EventChannels, getGlobalEventBus } from './event-bus.js';
11
5
  import { buildSystemPrompt } from './prompt-builder.js';
12
6
  import { AnthropicProvider } from './providers/anthropic.js';
13
7
  import { GoogleProvider } from './providers/google.js';
14
8
  import { OllamaProvider } from './providers/ollama.js';
15
9
  import { OpenAIProvider } from './providers/openai.js';
16
- function getSessionDir() {
17
- return path.join(os.homedir(), '.lonny', 'sessions');
18
- }
19
- function getSessionFilePath(cwd) {
20
- const absPath = path.resolve(cwd);
21
- const hash = createHash('sha256').update(absPath, 'utf-8').digest('hex').slice(0, 12);
22
- const dirName = path.basename(absPath);
23
- const safeName = dirName.replace(/[<>:"/\\|?*]/g, '_');
24
- return path.join(getSessionDir(), `${safeName}-${hash}.json`);
25
- }
26
- function ensureDir(dir) {
27
- if (!fs.existsSync(dir)) {
28
- fs.mkdirSync(dir, { recursive: true });
29
- }
30
- }
31
- // ── Colors ─────────────────────────────────────────────────────────────────
32
- const CY = '\x1b[36m';
33
- const GR = '\x1b[32m';
34
- const YE = '\x1b[33m';
35
- const RE = '\x1b[31m';
36
- const MG = '\x1b[35m';
37
- const GY = '\x1b[90m';
38
- const RS = '\x1b[0m';
39
- const BLD = '\x1b[1m';
40
- const TH = '\x1b[48;2;22;22;32m\x1b[38;2;150;150;170m'; // dark bg + dim fg for thinking
41
- /** Get terminal width (columns), default to 80. */
42
- function termWidth() {
43
- return process.stdout.columns ?? 80;
44
- }
45
- /** Visible width of a string (strip ANSI codes). ASCII=1, CJK/non-ASCII=2. */
46
- function visibleWidth(s) {
47
- let w = 0;
48
- for (let i = 0; i < s.length; i++) {
49
- if (s.charCodeAt(i) === 0x1b) {
50
- // Skip past escape sequence
51
- while (i < s.length && s[i] !== 'm')
52
- i++;
53
- continue;
54
- }
55
- w += s.charCodeAt(i) > 0x7e ? 2 : 1;
56
- }
57
- return w;
58
- }
59
- /** Visible prefix width for thinking box lines: " │" = 3 */
60
- const THINK_PREFIX_WIDTH = 3;
61
- /** Build the top border of the thinking box */
62
- function thinkTopBorder() {
63
- return `\n ${GY}╭───────${RS}${TH} Think ${GY}────────────────────${RS}\n`;
64
- }
65
- /** Build the bottom border of the thinking box */
66
- function thinkBottomBorder() {
67
- return ` ${GY}╰${'─'.repeat(42)}${RS}\n\n`;
68
- }
69
- function writeOut(text, output) {
70
- if (output) {
71
- output.write(text);
72
- }
73
- else {
74
- process.stdout.write(text);
75
- }
76
- }
77
- function printUserMessage(prompt, output) {
78
- // When suppressToolOutput is true (Web UI mode), the frontend already
79
- // displays the user message, so skip sending it as a chunk.
80
- if (output?.suppressToolOutput)
81
- return;
82
- const line = ` ${GY}┃${RS} ${BLD}${CY}You${RS}`;
83
- writeOut(`\n${line} ${prompt}\n\n`, output);
84
- }
85
- function printToolInvocation(tc, output) {
86
- const detail = formatToolInput(tc);
87
- const isWrite = tc.name === 'write_plan' || tc.name === 'edit';
88
- const icon = isWrite ? `${YE}◆${RS}` : `${GR}◇${RS}`;
89
- const label = isWrite ? `${YE}${tc.name}${RS}` : `${GR}${tc.name}${RS}`;
90
- writeOut(`\n ${GY}│${RS} ${icon} ${label}${detail ? ` ${GY}${detail}${RS}` : ''}\n`, output);
91
- }
92
- function printToolResult(tc, result, output) {
93
- if (!result.success) {
94
- writeOut(` ${GY}│${RS} ${RE}✖${RS} ${RE}${result.error}${RS}\n`, output);
95
- return;
96
- }
97
- if (tc.name === 'read') {
98
- const fileCount = (result.output.match(/^=== /gm) || []).length;
99
- writeOut(` ${GY}│${RS} ${GR}✔${RS} read ${fileCount} file(s)\n`, output);
100
- for (const line of result.output.split('\n')) {
101
- if (line.startsWith('=== ')) {
102
- const fp = line.slice(4, line.includes(' ===') ? line.indexOf(' ===') + 4 : undefined);
103
- writeOut(` ${GY}│${RS} ${GY}${fp}${RS}\n`, output);
104
- }
105
- }
106
- }
107
- else if (tc.name === 'glob') {
108
- const count = result.output.split('\n').filter(l => l && !l.startsWith('No')).length;
109
- writeOut(` ${GY}│${RS} ${GR}✔${RS} glob ${count} match(es)\n`, output);
110
- }
111
- else if (tc.name === 'grep') {
112
- const count = result.output.split('\n').filter(l => l && !l.startsWith('No')).length;
113
- writeOut(` ${GY}│${RS} ${GR}✔${RS} grep ${count} match(es)\n`, output);
114
- }
115
- else if (tc.name === 'bash') {
116
- const outLines = result.output.split('\n');
117
- const summary = outLines.length > 1 ? `(${outLines.length} lines)` : '';
118
- writeOut(` ${GY}│${RS} ${GR}✔${RS} bash ${summary}\n`, output);
119
- }
120
- else if (tc.name === 'edit') {
121
- writeOut(` ${GY}│${RS} ${GR}✔${RS} edit\n`, output);
122
- if (result.output) {
123
- for (const l of result.output.split('\n')) {
124
- if (l.trim())
125
- writeOut(` ${GY}│${RS} ${l.trim()}\n`, output);
126
- }
127
- }
128
- }
129
- else if (tc.name === 'write_plan') {
130
- writeOut(` ${GY}│${RS} ${GR}✔${RS} ${result.output || tc.name}\n`, output);
131
- }
132
- else if (tc.name === 'search') {
133
- writeOut(` ${GY}│${RS} ${GR}✔${RS} search: ${String(tc.input.query || '').slice(0, 80)}\n`, output);
134
- }
135
- else {
136
- writeOut(` ${GY}│${RS} ${GR}✔${RS} ${tc.name}\n`, output);
137
- }
138
- }
139
- function isSingleEditShape(v) {
140
- return (typeof v === 'object' &&
141
- v !== null &&
142
- 'file_path' in v &&
143
- 'old_string' in v &&
144
- 'new_string' in v);
145
- }
146
- export function formatToolInput(tc) {
147
- const parts = [];
148
- if (tc.name === 'read' && Array.isArray(tc.input.paths)) {
149
- parts.push(tc.input.paths.join(', '));
150
- }
151
- else if (tc.name === 'glob' && typeof tc.input.pattern === 'string') {
152
- parts.push(tc.input.pattern);
153
- }
154
- else if (tc.name === 'grep') {
155
- if (typeof tc.input.pattern === 'string')
156
- parts.push(`/${tc.input.pattern}/`);
157
- if (typeof tc.input.include === 'string')
158
- parts.push(`in:${tc.input.include}`);
159
- }
160
- else if (tc.name === 'ls') {
161
- parts.push(typeof tc.input.path === 'string' ? tc.input.path : '.');
162
- }
163
- else if (tc.name === 'bash') {
164
- const cmd = typeof tc.input.command === 'string' ? tc.input.command : '';
165
- parts.push(cmd.length > 80 ? `${cmd.slice(0, 80)}\u2026` : cmd);
166
- }
167
- else if (tc.name === 'search') {
168
- if (typeof tc.input.query === 'string')
169
- parts.push(tc.input.query.slice(0, 120));
170
- }
171
- else if (tc.name === 'write_plan') {
172
- if (typeof tc.input.filename === 'string')
173
- parts.push(tc.input.filename);
174
- }
175
- else if (tc.name === 'edit') {
176
- if (Array.isArray(tc.input.edits) && tc.input.edits.every(isSingleEditShape)) {
177
- const paths = tc.input.edits.map(e => e.file_path);
178
- parts.push(paths.join(', '));
179
- }
180
- }
181
- return parts.join(' \u2502 ');
182
- }
183
- function printTokenStats(turnIn, turnOut, totalIn, totalOut, turnApi, totalApi, output) {
184
- const bus = getGlobalEventBus();
185
- bus.emit(EventChannels.TOKEN_STATS, { turnIn, turnOut, totalIn, totalOut, turnApi, totalApi });
186
- // Skip terminal output in Web UI mode
187
- if (output?.suppressToolOutput)
188
- return;
189
- const total = totalIn + totalOut;
190
- const msg = ` ${GY}┃${RS} ${GY}${BLD}▴${RS}${GY}${turnIn}${RS} ${GY}${BLD}▾${RS}${GY}${turnOut}${RS} ${GY}total${RS} ${total} ${GY}calls${RS} ${turnApi}(${totalApi})`;
191
- writeOut(`\n${msg}\n`, output);
192
- }
10
+ import { runChat } from './session-chat.js';
11
+ import { ensureDir, findLegacySessionFile, generateId, getSessionDir, getSessionFilePath, getSessionFilesForCwd, migrateSessionData, } from './session-persistence.js';
12
+ import { sanitizeMessages } from './session-utils.js';
13
+ export { formatToolInput } from './session-display.js';
14
+ // ── Session class ────────────────────────────────────────────────────────────
193
15
  export class Session {
194
16
  messages;
195
17
  provider;
@@ -198,12 +20,9 @@ export class Session {
198
20
  config;
199
21
  output;
200
22
  _onPlanWritten;
201
- /** Set the plan-written callback and propagate to ToolRegistry */
202
23
  set onPlanWritten(cb) {
203
24
  this._onPlanWritten = cb;
204
- // Update context so setMode() picks it up too
205
25
  this.registry.updateContext({ onPlanWritten: cb });
206
- // Re-register write_plan tool with the new callback
207
26
  if (this.registry.has('write_plan')) {
208
27
  this.registry.reRegisterWritePlan(this.config.cwd, cb);
209
28
  }
@@ -217,6 +36,10 @@ export class Session {
217
36
  turnOutputTokens = 0;
218
37
  turnApiCalls = 0;
219
38
  totalApiCalls = 0;
39
+ turnCacheHitTokens = 0;
40
+ turnCacheMissTokens = 0;
41
+ totalCacheHitTokens = 0;
42
+ totalCacheMissTokens = 0;
220
43
  stopped = false;
221
44
  abortController = null;
222
45
  constructor(config, output) {
@@ -242,457 +65,290 @@ export class Session {
242
65
  else {
243
66
  this.provider = new AnthropicProvider(config.apiKey, config.baseUrl, config.model);
244
67
  }
245
- // Placeholder until initSystemPrompt() is called
246
68
  this.messages = [{ role: 'system', content: '' }];
247
- // Initialize system prompt asynchronously
248
69
  this.initSystemPrompt(config);
249
70
  }
250
- /** Initialize the system prompt asynchronously */
251
71
  initSystemPrompt(config) {
252
- buildSystemPrompt(config).then(prompt => {
253
- this.messages = [{ role: 'system', content: prompt }];
72
+ buildSystemPrompt(config, this.registry.getDefinitions()).then(prompt => {
73
+ if (this.messages.length <= 1) {
74
+ this.messages = [{ role: 'system', content: prompt }];
75
+ }
254
76
  });
255
77
  }
256
- /** Persist the current session to ~/.lonny/sessions/ */
78
+ sessionId = generateId();
79
+ sessionTitle = '';
80
+ sessionCreatedAt = new Date().toISOString();
257
81
  save() {
258
82
  const dir = getSessionDir();
259
83
  ensureDir(dir);
260
- const filePath = getSessionFilePath(this.config.cwd);
84
+ const filePath = getSessionFilePath(this.config.cwd, this.sessionId);
85
+ const now = new Date().toISOString();
86
+ let title = this.sessionTitle;
87
+ if (!title) {
88
+ const firstUserMsg = this.messages.find(m => m.role === 'user');
89
+ if (firstUserMsg && typeof firstUserMsg.content === 'string') {
90
+ title = firstUserMsg.content.slice(0, 80).replace(/\n/g, ' ');
91
+ }
92
+ }
261
93
  const data = {
94
+ id: this.sessionId,
95
+ title: title || undefined,
262
96
  cwd: path.resolve(this.config.cwd),
263
97
  messages: this.messages,
264
98
  totalInputTokens: this.totalInputTokens,
265
99
  totalOutputTokens: this.totalOutputTokens,
266
100
  totalApiCalls: this.totalApiCalls,
101
+ totalCacheHitTokens: this.totalCacheHitTokens || undefined,
102
+ totalCacheMissTokens: this.totalCacheMissTokens || undefined,
267
103
  mode: this.config.mode,
268
104
  model: this.config.model,
269
105
  provider: this.config.provider,
270
- updatedAt: new Date().toISOString(),
106
+ createdAt: this.sessionCreatedAt,
107
+ updatedAt: now,
271
108
  };
272
109
  fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8');
273
110
  }
274
- /** Try to load a saved session for the given cwd. Returns null if none exists. */
275
111
  static async load(config, output) {
276
- const filePath = getSessionFilePath(config.cwd);
277
- let data;
278
- try {
279
- data = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
112
+ const cwdSessions = getSessionFilesForCwd(config.cwd);
113
+ console.log(`[session] Found ${cwdSessions.length} session files for cwd "${config.cwd}":`, cwdSessions
114
+ .map(f => `${f.fileName} (updatedAt=${f.data.updatedAt?.slice(0, 19)}, messages=${f.data.messages?.length || 0})`)
115
+ .join(', '));
116
+ if (cwdSessions.length > 0) {
117
+ cwdSessions.sort((a, b) => {
118
+ const aLen = a.data.messages?.length || 0;
119
+ const bLen = b.data.messages?.length || 0;
120
+ if (aLen !== bLen)
121
+ return bLen - aLen;
122
+ return b.data.updatedAt.localeCompare(a.data.updatedAt);
123
+ });
124
+ console.log(`[session] Loading session file: ${cwdSessions[0].fileName}`);
125
+ return Session.loadFromData(cwdSessions[0].data, config, output);
280
126
  }
281
- catch {
127
+ const legacyPath = findLegacySessionFile(config.cwd);
128
+ if (legacyPath) {
129
+ try {
130
+ const raw = JSON.parse(fs.readFileSync(legacyPath, 'utf-8'));
131
+ const data = migrateSessionData(raw);
132
+ const session = await Session.loadFromData(data, config, output);
133
+ session.save();
134
+ try {
135
+ fs.unlinkSync(legacyPath);
136
+ }
137
+ catch {
138
+ /* ignore */
139
+ }
140
+ return session;
141
+ }
142
+ catch {
143
+ return null;
144
+ }
145
+ }
146
+ return null;
147
+ }
148
+ static async loadById(sessionId, config, output) {
149
+ const allSessions = Session.listSessions();
150
+ const target = allSessions.find(s => s.id === sessionId);
151
+ if (!target)
282
152
  return null;
153
+ const legacyPath = findLegacySessionFile(config.cwd);
154
+ if (legacyPath) {
155
+ try {
156
+ const raw = JSON.parse(fs.readFileSync(legacyPath, 'utf-8'));
157
+ const data = migrateSessionData(raw);
158
+ if (data.id === sessionId) {
159
+ return Session.loadFromData(data, config, output);
160
+ }
161
+ }
162
+ catch {
163
+ /* ignore */
164
+ }
283
165
  }
284
- // Verify the cwd matches (in case of hash collision or directory rename)
285
- const savedAbs = path.resolve(data.cwd);
286
- const currentAbs = path.resolve(config.cwd);
287
- if (savedAbs !== currentAbs) {
166
+ const filePath = getSessionFilePath(target.cwd, sessionId);
167
+ try {
168
+ const raw = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
169
+ return Session.loadFromData(migrateSessionData(raw), config, output);
170
+ }
171
+ catch {
288
172
  return null;
289
173
  }
174
+ }
175
+ static async loadFromData(data, config, output) {
290
176
  const session = new Session(config, output);
291
- // Restore messages (replace the default system prompt with the saved one)
292
177
  session.messages = data.messages;
293
- // Refresh the system prompt only if config actually changed (model, mode, etc.)
294
- // Compare the saved data vs current config to decide
295
- if (data.model !== config.model ||
296
- data.provider !== config.provider ||
297
- data.mode !== config.mode) {
298
- const prompt = await buildSystemPrompt(config);
178
+ console.log(`[session] Loading session ${data.id}: ${data.messages.length} messages from disk`);
179
+ session.messages = sanitizeMessages(session.messages);
180
+ console.log(`[session] After sanitize: ${session.messages.length} messages (removed ${data.messages.length - session.messages.length})`);
181
+ session.sessionId = data.id;
182
+ session.sessionTitle = data.title || '';
183
+ session.sessionCreatedAt = data.createdAt;
184
+ config.mode = data.mode;
185
+ session.registry.setMode(data.mode);
186
+ if (data.model !== config.model || data.provider !== config.provider) {
187
+ const prompt = await buildSystemPrompt(config, session.registry.getDefinitions());
299
188
  session.messages[0] = { role: 'system', content: prompt };
300
189
  }
301
- // Restore token stats
302
190
  session.totalInputTokens = data.totalInputTokens;
303
191
  session.totalOutputTokens = data.totalOutputTokens;
304
192
  session.totalApiCalls = data.totalApiCalls;
193
+ session.totalCacheHitTokens = data.totalCacheHitTokens ?? 0;
194
+ session.totalCacheMissTokens = data.totalCacheMissTokens ?? 0;
305
195
  return session;
306
196
  }
307
- /** Remove the saved session file for the given cwd. */
308
197
  static clearSavedSession(cwd) {
309
- const filePath = getSessionFilePath(cwd);
198
+ const cwdSessions = getSessionFilesForCwd(cwd);
199
+ for (const { fileName } of cwdSessions) {
200
+ try {
201
+ fs.unlinkSync(path.join(getSessionDir(), fileName));
202
+ }
203
+ catch {
204
+ /* ignore */
205
+ }
206
+ }
207
+ const legacyPath = findLegacySessionFile(cwd);
208
+ if (legacyPath) {
209
+ try {
210
+ fs.unlinkSync(legacyPath);
211
+ }
212
+ catch {
213
+ /* ignore */
214
+ }
215
+ }
216
+ }
217
+ static listSessions(maxCount) {
218
+ const dir = getSessionDir();
310
219
  try {
311
- fs.unlinkSync(filePath);
220
+ if (!fs.existsSync(dir))
221
+ return [];
222
+ const files = fs.readdirSync(dir).filter(f => f.endsWith('.json'));
223
+ const sessions = [];
224
+ for (const fileName of files) {
225
+ try {
226
+ const raw = JSON.parse(fs.readFileSync(path.join(dir, fileName), 'utf-8'));
227
+ const data = migrateSessionData(raw);
228
+ const firstUserMsg = data.messages.find(m => m.role === 'user');
229
+ let title = data.title || '';
230
+ if (!title && firstUserMsg && typeof firstUserMsg.content === 'string') {
231
+ title = firstUserMsg.content.slice(0, 80).replace(/\n/g, ' ');
232
+ }
233
+ sessions.push({
234
+ id: data.id,
235
+ cwd: data.cwd,
236
+ title,
237
+ messageCount: data.messages.length,
238
+ mode: data.mode,
239
+ model: data.model,
240
+ provider: data.provider,
241
+ totalInputTokens: data.totalInputTokens,
242
+ totalOutputTokens: data.totalOutputTokens,
243
+ totalApiCalls: data.totalApiCalls,
244
+ createdAt: data.createdAt,
245
+ updatedAt: data.updatedAt,
246
+ fileName,
247
+ });
248
+ }
249
+ catch {
250
+ // Skip corrupted files
251
+ }
252
+ }
253
+ sessions.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
254
+ if (maxCount && maxCount > 0) {
255
+ return sessions.slice(0, maxCount);
256
+ }
257
+ return sessions;
258
+ }
259
+ catch {
260
+ return [];
261
+ }
262
+ }
263
+ static deleteSession(id) {
264
+ const sessions = Session.listSessions();
265
+ const target = sessions.find(s => s.id === id);
266
+ if (!target)
267
+ return false;
268
+ try {
269
+ fs.unlinkSync(path.join(getSessionDir(), target.fileName));
270
+ return true;
312
271
  }
313
272
  catch {
314
- // Ignore if file doesn't exist
273
+ return false;
315
274
  }
316
275
  }
276
+ clearSavedSession() {
277
+ Session.clearSavedSession(this.config.cwd);
278
+ }
279
+ fork() {
280
+ const forked = new Session(this.config, this.output);
281
+ forked.messages = [...this.messages];
282
+ forked.onPlanWritten = this.onPlanWritten;
283
+ const baseTitle = this.sessionTitle || 'forked session';
284
+ forked.sessionTitle = `${baseTitle} (fork)`;
285
+ forked.sessionId = generateId();
286
+ forked.sessionCreatedAt = new Date().toISOString();
287
+ forked.save();
288
+ return forked;
289
+ }
290
+ exportSession(filePath) {
291
+ const dir = filePath || path.join(getSessionDir(), `export-${this.sessionId}.json`);
292
+ const now = new Date().toISOString();
293
+ let title = this.sessionTitle;
294
+ if (!title) {
295
+ const firstUserMsg = this.messages.find(m => m.role === 'user');
296
+ if (firstUserMsg && typeof firstUserMsg.content === 'string') {
297
+ title = firstUserMsg.content.slice(0, 80).replace(/\n/g, ' ');
298
+ }
299
+ }
300
+ const exportData = {
301
+ id: this.sessionId,
302
+ title: title || '(untitled)',
303
+ cwd: path.resolve(this.config.cwd),
304
+ mode: this.config.mode,
305
+ model: this.config.model,
306
+ provider: this.config.provider,
307
+ totalInputTokens: this.totalInputTokens,
308
+ totalOutputTokens: this.totalOutputTokens,
309
+ totalApiCalls: this.totalApiCalls,
310
+ totalCacheHitTokens: this.totalCacheHitTokens,
311
+ totalCacheMissTokens: this.totalCacheMissTokens,
312
+ createdAt: this.sessionCreatedAt,
313
+ exportedAt: now,
314
+ messages: this.messages.map(m => ({
315
+ role: m.role,
316
+ content: typeof m.content === 'string' ? m.content : null,
317
+ tool_calls: m.tool_calls
318
+ ? m.tool_calls.map(tc => ({
319
+ id: tc.id,
320
+ name: tc.name,
321
+ input: tc.input,
322
+ }))
323
+ : undefined,
324
+ reasoning_content: m.reasoning_content,
325
+ })),
326
+ };
327
+ const fullPath = path.resolve(dir);
328
+ ensureDir(path.dirname(fullPath));
329
+ fs.writeFileSync(fullPath, JSON.stringify(exportData, null, 2), 'utf-8');
330
+ return fullPath;
331
+ }
317
332
  async setMode(mode) {
318
333
  this.config.mode = mode;
319
- const prompt = await buildSystemPrompt(this.config);
320
- this.messages[0] = { role: 'system', content: prompt };
321
334
  this.registry.setMode(mode);
335
+ const prompt = await buildSystemPrompt(this.config, this.registry.getDefinitions());
336
+ this.messages[0] = { role: 'system', content: prompt };
322
337
  this.save();
323
338
  }
324
- /** Stop the current conversation gracefully */
325
339
  stop() {
326
340
  this.stopped = true;
327
- // Abort any in-flight LLM stream to stop token consumption immediately
328
341
  this.abortController?.abort();
329
342
  }
330
- /** Check if the session was stopped */
331
343
  isStopped() {
332
344
  return this.stopped;
333
345
  }
334
- /** Reset the stopped flag for a new conversation */
335
346
  resetStopped() {
336
347
  this.stopped = false;
337
- // Create new AbortController for next conversation
338
348
  this.abortController = new AbortController();
339
349
  }
340
350
  async chat(userPrompt) {
341
- const bus = getGlobalEventBus();
342
- const out = this.output;
343
- printUserMessage(userPrompt, out);
344
- this.messages.push({ role: 'user', content: userPrompt });
345
- // Save immediately after user message so page refresh doesn't lose it
346
- this.save();
347
- // Reset per-turn counters
348
- this.turnInputTokens = 0;
349
- this.turnOutputTokens = 0;
350
- this.turnApiCalls = 0;
351
- let iterations = 0;
352
- const maxIterations = 30;
353
- // Reset stopped flag for new conversation
354
- this.resetStopped();
355
- // Create a new AbortController for this chat invocation
356
- // (a new controller is needed each time because abort() is one-shot)
357
- this.abortController = new AbortController();
358
- // Declare toolCalls outside the loop so we can reference it in stop check
359
- let toolCalls = [];
360
- while (iterations < maxIterations) {
361
- // Check if stop was requested
362
- if (this.isStopped()) {
363
- bus.emit(EventChannels.TURN_END, { iterations, toolCallCount: toolCalls.length });
364
- this.save();
365
- return;
366
- }
367
- iterations++;
368
- this.turnApiCalls++;
369
- this.totalApiCalls++;
370
- toolCalls = [];
371
- let fullResponse = '';
372
- let reasoningContent;
373
- let reasoningOutput = false;
374
- let reasoningLineStart = false;
375
- bus.emit(EventChannels.TURN_START, { prompt: userPrompt, iteration: iterations });
376
- bus.emit(EventChannels.LLM_STREAM_START, { iteration: iterations });
377
- const stream = this.provider.chat(this.messages, this.registry.getDefinitions(), this.abortController.signal);
378
- try {
379
- for await (const chunk of stream) {
380
- if (chunk.reasoning_content) {
381
- reasoningContent = chunk.reasoning_content;
382
- // Stream reasoning content in real-time (only when no text in same chunk)
383
- if (!chunk.text) {
384
- // Emit thinking via EventBus for Web UI
385
- bus.emit(EventChannels.THINKING, { text: chunk.reasoning_content });
386
- if (!reasoningOutput) {
387
- reasoningOutput = true;
388
- reasoningLineStart = true;
389
- if (!out?.suppressToolOutput) {
390
- writeOut(thinkTopBorder(), out);
391
- }
392
- }
393
- // Terminal display with box drawing (skip in Web UI mode, handled by EventBus)
394
- if (!out?.suppressToolOutput) {
395
- // Track column position on current line for wrapping
396
- let thinkCol = 0;
397
- // Handle newlines in streamed content - add left border on each new line
398
- // Also manually wrap long lines so wrapped lines keep the │ prefix.
399
- let remaining = chunk.reasoning_content;
400
- const maxContentWidth = termWidth() - THINK_PREFIX_WIDTH;
401
- while (remaining.length > 0) {
402
- if (reasoningLineStart) {
403
- writeOut(` ${GY}│${RS}${TH}`, out);
404
- reasoningLineStart = false;
405
- thinkCol = 0;
406
- }
407
- const nlIdx = remaining.indexOf('\n');
408
- if (nlIdx === -1) {
409
- // No newline — write as much as fits on current line, wrap if needed
410
- while (remaining.length > 0) {
411
- const segWidth = visibleWidth(remaining);
412
- const avail = maxContentWidth - thinkCol;
413
- if (segWidth <= avail) {
414
- // Fits entirely on current line
415
- writeOut(remaining, out);
416
- thinkCol += segWidth;
417
- remaining = '';
418
- }
419
- else if (avail <= 0) {
420
- // Current line is full, wrap to next
421
- writeOut(`${RS}\n`, out);
422
- writeOut(` ${GY}│${RS}${TH}`, out);
423
- thinkCol = 0;
424
- }
425
- else {
426
- // Write first part that fits, then wrap
427
- // Find character boundary that fits within avail
428
- let cut = avail;
429
- while (cut > 0 && visibleWidth(remaining.slice(0, cut)) > avail)
430
- cut--;
431
- if (cut <= 0)
432
- cut = 1;
433
- writeOut(remaining.slice(0, cut), out);
434
- writeOut(`${RS}\n`, out);
435
- writeOut(` ${GY}│${RS}${TH}`, out);
436
- thinkCol = 0;
437
- remaining = remaining.slice(cut);
438
- }
439
- }
440
- }
441
- else {
442
- // Has newline — process the segment up to newline
443
- const segment = remaining.slice(0, nlIdx);
444
- const segWidth = visibleWidth(segment);
445
- const avail = maxContentWidth - thinkCol;
446
- if (segWidth <= avail) {
447
- // Segment fits on current line
448
- writeOut(segment, out);
449
- writeOut(`${RS}\n`, out);
450
- reasoningLineStart = true;
451
- thinkCol = 0;
452
- }
453
- else {
454
- // Segment too long — write what fits, wrap, then rest
455
- let rest = segment;
456
- // Write remainder of current line
457
- if (avail > 0) {
458
- let cut = avail;
459
- while (cut > 0 && visibleWidth(rest.slice(0, cut)) > avail)
460
- cut--;
461
- if (cut <= 0)
462
- cut = 1;
463
- writeOut(rest.slice(0, cut), out);
464
- rest = rest.slice(cut);
465
- }
466
- writeOut(`${RS}\n`, out);
467
- reasoningLineStart = true;
468
- thinkCol = 0;
469
- // Write rest of segment on continuation line(s)
470
- if (rest.length > 0) {
471
- writeOut(` ${GY}│${RS}${TH}`, out);
472
- reasoningLineStart = false;
473
- while (rest.length > 0) {
474
- const rw = visibleWidth(rest);
475
- if (rw <= maxContentWidth) {
476
- writeOut(rest, out);
477
- thinkCol = rw;
478
- rest = '';
479
- }
480
- else {
481
- let cut = maxContentWidth;
482
- while (cut > 0 && visibleWidth(rest.slice(0, cut)) > maxContentWidth)
483
- cut--;
484
- if (cut <= 0)
485
- cut = 1;
486
- writeOut(rest.slice(0, cut), out);
487
- writeOut(`${RS}\n`, out);
488
- writeOut(` ${GY}│${RS}${TH}`, out);
489
- rest = rest.slice(cut);
490
- }
491
- }
492
- }
493
- writeOut(`${RS}\n`, out);
494
- reasoningLineStart = true;
495
- thinkCol = 0;
496
- }
497
- remaining = remaining.slice(nlIdx + 1);
498
- }
499
- }
500
- }
501
- }
502
- }
503
- if (chunk.type === 'text' && chunk.text) {
504
- if (reasoningOutput) {
505
- bus.emit(EventChannels.THINKING_END, {});
506
- if (!out?.suppressToolOutput) {
507
- writeOut(`${RS}\n`, out);
508
- writeOut(thinkBottomBorder(), out);
509
- }
510
- reasoningOutput = false;
511
- reasoningLineStart = false;
512
- }
513
- fullResponse += chunk.text;
514
- writeOut(chunk.text, out);
515
- }
516
- else if (chunk.type === 'tool_use' && chunk.tool_call) {
517
- toolCalls.push(chunk.tool_call);
518
- }
519
- else if (chunk.type === 'complete') {
520
- if (chunk.usage) {
521
- this.turnInputTokens += chunk.usage.input_tokens;
522
- this.turnOutputTokens += chunk.usage.output_tokens;
523
- this.totalInputTokens += chunk.usage.input_tokens;
524
- this.totalOutputTokens += chunk.usage.output_tokens;
525
- }
526
- if (chunk.finish_reason === 'stop' || chunk.finish_reason === 'end_turn') {
527
- if (toolCalls.length === 0) {
528
- const finalAssistantMsg = {
529
- role: 'assistant',
530
- content: fullResponse || null,
531
- reasoning_content: reasoningContent,
532
- };
533
- this.messages.push(finalAssistantMsg);
534
- printTokenStats(this.turnInputTokens, this.turnOutputTokens, this.totalInputTokens, this.totalOutputTokens, this.turnApiCalls, this.totalApiCalls, out);
535
- writeOut('\n\n', out);
536
- saveTokenUsage(this.config.cwd, this.turnInputTokens, this.turnOutputTokens, this.turnApiCalls);
537
- bus.emit(EventChannels.TURN_END, { iterations, toolCallCount: 0 });
538
- this.save();
539
- return;
540
- }
541
- }
542
- }
543
- }
544
- }
545
- catch (e) {
546
- const errMsg = fmtErr(e);
547
- const partialContent = fullResponse ? fullResponse.slice(0, 500) : '(empty)';
548
- if (!out?.suppressToolOutput) {
549
- writeOut(`\n${RE}Stream error:${RS} ${errMsg}`, out);
550
- writeOut(`\n ${GY}┃${RS} Partial response: ${partialContent}\n`, out);
551
- }
552
- console.error('[session] Stream error:', errMsg, '| Partial response:', partialContent);
553
- if (reasoningOutput) {
554
- bus.emit(EventChannels.THINKING_END, {});
555
- if (!out?.suppressToolOutput) {
556
- writeOut(`${RS}\n`, out);
557
- writeOut(thinkBottomBorder(), out);
558
- }
559
- }
560
- bus.emit(EventChannels.LLM_STREAM_END, { iteration: iterations, toolCallCount: 0 });
561
- bus.emit(EventChannels.TURN_END, { iterations, toolCallCount: 0 });
562
- // If there are pending toolCalls that weren't executed due to abort,
563
- // add an assistant message so the model knows they weren't executed
564
- if (toolCalls.length > 0) {
565
- const interruptedMsg = {
566
- role: 'assistant',
567
- content: null,
568
- tool_calls: toolCalls,
569
- reasoning_content: reasoningContent,
570
- };
571
- this.messages.push(interruptedMsg);
572
- }
573
- saveTokenUsage(this.config.cwd, this.turnInputTokens, this.turnOutputTokens, this.turnApiCalls);
574
- this.save();
575
- return;
576
- }
577
- bus.emit(EventChannels.LLM_STREAM_END, {
578
- iteration: iterations,
579
- toolCallCount: toolCalls.length,
580
- });
581
- // Close reasoning display if still open (model ended with tool calls, no text)
582
- if (reasoningOutput) {
583
- bus.emit(EventChannels.THINKING_END, {});
584
- if (!out?.suppressToolOutput) {
585
- writeOut(`${RS}\n`, out);
586
- writeOut(thinkBottomBorder(), out);
587
- }
588
- reasoningOutput = false;
589
- reasoningLineStart = false;
590
- }
591
- if (toolCalls.length === 0) {
592
- if (fullResponse) {
593
- const finalAssistantMsg = {
594
- role: 'assistant',
595
- content: fullResponse,
596
- reasoning_content: reasoningContent,
597
- };
598
- this.messages.push(finalAssistantMsg);
599
- printTokenStats(this.turnInputTokens, this.turnOutputTokens, this.totalInputTokens, this.totalOutputTokens, this.turnApiCalls, this.totalApiCalls, out);
600
- writeOut('\n\n', out);
601
- }
602
- saveTokenUsage(this.config.cwd, this.turnInputTokens, this.turnOutputTokens, this.turnApiCalls);
603
- bus.emit(EventChannels.TURN_END, { iterations, toolCallCount: 0 });
604
- this.save();
605
- return;
606
- }
607
- const assistantMsg = {
608
- role: 'assistant',
609
- content: fullResponse || null,
610
- tool_calls: toolCalls,
611
- reasoning_content: reasoningContent,
612
- };
613
- this.messages.push(assistantMsg);
614
- this.save();
615
- // ── User confirmation for write-type tool calls ──
616
- if (!this.config.autoApprove && this.output?.confirmTool && toolCalls.length > 0) {
617
- const writeTools = ['edit', 'bash', 'write_plan', 'exec', 'install_skill'];
618
- const needsConfirm = toolCalls.filter(tc => writeTools.includes(tc.name));
619
- if (needsConfirm.length > 0) {
620
- const approved = await this.output.confirmTool(needsConfirm);
621
- if (!approved) {
622
- const rejectMsg = {
623
- role: 'tool',
624
- content: 'USER_REJECTED: The user declined to execute the requested tool calls. Try a different approach.',
625
- tool_call_id: needsConfirm[0].id,
626
- name: 'user_feedback',
627
- };
628
- this.messages.push(rejectMsg);
629
- bus.emit(EventChannels.TURN_END, { iterations, toolCallCount: toolCalls.length });
630
- continue;
631
- }
632
- }
633
- }
634
- for (let i = 0; i < toolCalls.length; i++) {
635
- const tc = toolCalls[i];
636
- // Check if stop was requested after each tool call
637
- if (this.isStopped()) {
638
- // Add assistant message with remaining tool_calls so model knows they weren't executed
639
- const remainingToolCalls = toolCalls.slice(i);
640
- if (remainingToolCalls.length > 0) {
641
- const interruptedMsg = {
642
- role: 'assistant',
643
- content: null,
644
- tool_calls: remainingToolCalls,
645
- };
646
- this.messages.push(interruptedMsg);
647
- }
648
- bus.emit(EventChannels.TURN_END, { iterations, toolCallCount: toolCalls.length });
649
- this.save();
650
- return;
651
- }
652
- bus.emit(EventChannels.TOOL_CALL, { name: tc.name, input: tc.input, id: tc.id });
653
- if (!out?.suppressToolOutput) {
654
- printToolInvocation(tc, out);
655
- }
656
- const result = await this.registry.dispatch(tc);
657
- if (result.success) {
658
- bus.emit(EventChannels.TOOL_RESULT, { name: tc.name, id: tc.id, output: result.output });
659
- }
660
- else {
661
- bus.emit(EventChannels.TOOL_ERROR, { name: tc.name, id: tc.id, error: result.error });
662
- }
663
- if (!out?.suppressToolOutput) {
664
- printToolResult(tc, result, out);
665
- }
666
- const resultMsg = {
667
- role: 'tool',
668
- content: result.success ? result.output : `ERROR: ${result.error}`,
669
- tool_call_id: tc.id,
670
- name: tc.name,
671
- };
672
- this.messages.push(resultMsg);
673
- }
674
- this.save();
675
- // End the current turn before next iteration — frontend creates a new message
676
- bus.emit(EventChannels.TURN_END, { iterations, toolCallCount: toolCalls.length });
677
- // Check if compaction is needed
678
- if (shouldCompact(this.messages, this.config.contextWindow)) {
679
- const before = this.messages.length;
680
- const result = compact(this.messages, this.config.contextWindow);
681
- if (result.compressed) {
682
- this.messages = result.messages;
683
- bus.emit(EventChannels.COMPACTION_TRIGGERED, { before, after: result.newCount });
684
- if (out && !out.suppressToolOutput) {
685
- out.write(`\n ${GY}┃${RS} ${GY}📦 Compressed context: ${before} → ${result.newCount} messages${RS}\n`);
686
- }
687
- }
688
- }
689
- }
690
- if (iterations >= maxIterations) {
691
- printTokenStats(this.turnInputTokens, this.turnOutputTokens, this.totalInputTokens, this.totalOutputTokens, this.turnApiCalls, this.totalApiCalls, out);
692
- writeOut('\nAgent reached maximum iterations. Stopping.\n', out);
693
- }
694
- saveTokenUsage(this.config.cwd, this.turnInputTokens, this.turnOutputTokens, this.turnApiCalls);
695
- this.save();
351
+ return runChat(this, userPrompt);
696
352
  }
697
353
  }
698
354
  //# sourceMappingURL=session.js.map