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