trellis-herbivore 0.1.0

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 (629) hide show
  1. package/bin/trellis.js +3 -0
  2. package/dist/cli/index.d.ts +3 -0
  3. package/dist/cli/index.d.ts.map +1 -0
  4. package/dist/cli/index.js +174 -0
  5. package/dist/cli/index.js.map +1 -0
  6. package/dist/commands/channel/adapters/claude.d.ts +38 -0
  7. package/dist/commands/channel/adapters/claude.d.ts.map +1 -0
  8. package/dist/commands/channel/adapters/claude.js +209 -0
  9. package/dist/commands/channel/adapters/claude.js.map +1 -0
  10. package/dist/commands/channel/adapters/codex.d.ts +77 -0
  11. package/dist/commands/channel/adapters/codex.d.ts.map +1 -0
  12. package/dist/commands/channel/adapters/codex.js +495 -0
  13. package/dist/commands/channel/adapters/codex.js.map +1 -0
  14. package/dist/commands/channel/adapters/index.d.ts +79 -0
  15. package/dist/commands/channel/adapters/index.d.ts.map +1 -0
  16. package/dist/commands/channel/adapters/index.js +109 -0
  17. package/dist/commands/channel/adapters/index.js.map +1 -0
  18. package/dist/commands/channel/adapters/types.d.ts +33 -0
  19. package/dist/commands/channel/adapters/types.d.ts.map +1 -0
  20. package/dist/commands/channel/adapters/types.js +2 -0
  21. package/dist/commands/channel/adapters/types.js.map +1 -0
  22. package/dist/commands/channel/agent-loader.d.ts +32 -0
  23. package/dist/commands/channel/agent-loader.d.ts.map +1 -0
  24. package/dist/commands/channel/agent-loader.js +154 -0
  25. package/dist/commands/channel/agent-loader.js.map +1 -0
  26. package/dist/commands/channel/context-loader.d.ts +26 -0
  27. package/dist/commands/channel/context-loader.d.ts.map +1 -0
  28. package/dist/commands/channel/context-loader.js +290 -0
  29. package/dist/commands/channel/context-loader.js.map +1 -0
  30. package/dist/commands/channel/context.d.ts +16 -0
  31. package/dist/commands/channel/context.d.ts.map +1 -0
  32. package/dist/commands/channel/context.js +83 -0
  33. package/dist/commands/channel/context.js.map +1 -0
  34. package/dist/commands/channel/create.d.ts +27 -0
  35. package/dist/commands/channel/create.d.ts.map +1 -0
  36. package/dist/commands/channel/create.js +39 -0
  37. package/dist/commands/channel/create.js.map +1 -0
  38. package/dist/commands/channel/dev-parse-trace.d.ts +14 -0
  39. package/dist/commands/channel/dev-parse-trace.d.ts.map +1 -0
  40. package/dist/commands/channel/dev-parse-trace.js +70 -0
  41. package/dist/commands/channel/dev-parse-trace.js.map +1 -0
  42. package/dist/commands/channel/index.d.ts +3 -0
  43. package/dist/commands/channel/index.d.ts.map +1 -0
  44. package/dist/commands/channel/index.js +496 -0
  45. package/dist/commands/channel/index.js.map +1 -0
  46. package/dist/commands/channel/kill.d.ts +7 -0
  47. package/dist/commands/channel/kill.d.ts.map +1 -0
  48. package/dist/commands/channel/kill.js +121 -0
  49. package/dist/commands/channel/kill.js.map +1 -0
  50. package/dist/commands/channel/list.d.ts +17 -0
  51. package/dist/commands/channel/list.d.ts.map +1 -0
  52. package/dist/commands/channel/list.js +233 -0
  53. package/dist/commands/channel/list.js.map +1 -0
  54. package/dist/commands/channel/messages.d.ts +16 -0
  55. package/dist/commands/channel/messages.d.ts.map +1 -0
  56. package/dist/commands/channel/messages.js +237 -0
  57. package/dist/commands/channel/messages.js.map +1 -0
  58. package/dist/commands/channel/rm.d.ts +27 -0
  59. package/dist/commands/channel/rm.d.ts.map +1 -0
  60. package/dist/commands/channel/rm.js +216 -0
  61. package/dist/commands/channel/rm.js.map +1 -0
  62. package/dist/commands/channel/run.d.ts +31 -0
  63. package/dist/commands/channel/run.d.ts.map +1 -0
  64. package/dist/commands/channel/run.js +137 -0
  65. package/dist/commands/channel/run.js.map +1 -0
  66. package/dist/commands/channel/send.d.ts +12 -0
  67. package/dist/commands/channel/send.d.ts.map +1 -0
  68. package/dist/commands/channel/send.js +24 -0
  69. package/dist/commands/channel/send.js.map +1 -0
  70. package/dist/commands/channel/spawn.d.ts +25 -0
  71. package/dist/commands/channel/spawn.d.ts.map +1 -0
  72. package/dist/commands/channel/spawn.js +192 -0
  73. package/dist/commands/channel/spawn.js.map +1 -0
  74. package/dist/commands/channel/store/events.d.ts +39 -0
  75. package/dist/commands/channel/store/events.d.ts.map +1 -0
  76. package/dist/commands/channel/store/events.js +87 -0
  77. package/dist/commands/channel/store/events.js.map +1 -0
  78. package/dist/commands/channel/store/filter.d.ts +3 -0
  79. package/dist/commands/channel/store/filter.d.ts.map +1 -0
  80. package/dist/commands/channel/store/filter.js +2 -0
  81. package/dist/commands/channel/store/filter.js.map +1 -0
  82. package/dist/commands/channel/store/lock.d.ts +23 -0
  83. package/dist/commands/channel/store/lock.d.ts.map +1 -0
  84. package/dist/commands/channel/store/lock.js +99 -0
  85. package/dist/commands/channel/store/lock.js.map +1 -0
  86. package/dist/commands/channel/store/paths.d.ts +63 -0
  87. package/dist/commands/channel/store/paths.d.ts.map +1 -0
  88. package/dist/commands/channel/store/paths.js +246 -0
  89. package/dist/commands/channel/store/paths.js.map +1 -0
  90. package/dist/commands/channel/store/schema.d.ts +27 -0
  91. package/dist/commands/channel/store/schema.d.ts.map +1 -0
  92. package/dist/commands/channel/store/schema.js +34 -0
  93. package/dist/commands/channel/store/schema.js.map +1 -0
  94. package/dist/commands/channel/store/thread-state.d.ts +5 -0
  95. package/dist/commands/channel/store/thread-state.d.ts.map +1 -0
  96. package/dist/commands/channel/store/thread-state.js +16 -0
  97. package/dist/commands/channel/store/thread-state.js.map +1 -0
  98. package/dist/commands/channel/store/watch.d.ts +19 -0
  99. package/dist/commands/channel/store/watch.d.ts.map +1 -0
  100. package/dist/commands/channel/store/watch.js +130 -0
  101. package/dist/commands/channel/store/watch.js.map +1 -0
  102. package/dist/commands/channel/supervisor/inbox.d.ts +25 -0
  103. package/dist/commands/channel/supervisor/inbox.d.ts.map +1 -0
  104. package/dist/commands/channel/supervisor/inbox.js +99 -0
  105. package/dist/commands/channel/supervisor/inbox.js.map +1 -0
  106. package/dist/commands/channel/supervisor/shutdown.d.ts +66 -0
  107. package/dist/commands/channel/supervisor/shutdown.d.ts.map +1 -0
  108. package/dist/commands/channel/supervisor/shutdown.js +143 -0
  109. package/dist/commands/channel/supervisor/shutdown.js.map +1 -0
  110. package/dist/commands/channel/supervisor/stdout.d.ts +49 -0
  111. package/dist/commands/channel/supervisor/stdout.d.ts.map +1 -0
  112. package/dist/commands/channel/supervisor/stdout.js +107 -0
  113. package/dist/commands/channel/supervisor/stdout.js.map +1 -0
  114. package/dist/commands/channel/supervisor.d.ts +47 -0
  115. package/dist/commands/channel/supervisor.d.ts.map +1 -0
  116. package/dist/commands/channel/supervisor.js +283 -0
  117. package/dist/commands/channel/supervisor.js.map +1 -0
  118. package/dist/commands/channel/text-body.d.ts +13 -0
  119. package/dist/commands/channel/text-body.d.ts.map +1 -0
  120. package/dist/commands/channel/text-body.js +47 -0
  121. package/dist/commands/channel/text-body.js.map +1 -0
  122. package/dist/commands/channel/threads.d.ts +39 -0
  123. package/dist/commands/channel/threads.d.ts.map +1 -0
  124. package/dist/commands/channel/threads.js +106 -0
  125. package/dist/commands/channel/threads.js.map +1 -0
  126. package/dist/commands/channel/title.d.ts +12 -0
  127. package/dist/commands/channel/title.d.ts.map +1 -0
  128. package/dist/commands/channel/title.js +24 -0
  129. package/dist/commands/channel/title.js.map +1 -0
  130. package/dist/commands/channel/wait.d.ts +18 -0
  131. package/dist/commands/channel/wait.d.ts.map +1 -0
  132. package/dist/commands/channel/wait.js +76 -0
  133. package/dist/commands/channel/wait.js.map +1 -0
  134. package/dist/commands/init.d.ts +57 -0
  135. package/dist/commands/init.d.ts.map +1 -0
  136. package/dist/commands/init.js +1466 -0
  137. package/dist/commands/init.js.map +1 -0
  138. package/dist/commands/mem.d.ts +234 -0
  139. package/dist/commands/mem.d.ts.map +1 -0
  140. package/dist/commands/mem.js +1869 -0
  141. package/dist/commands/mem.js.map +1 -0
  142. package/dist/commands/uninstall.d.ts +27 -0
  143. package/dist/commands/uninstall.d.ts.map +1 -0
  144. package/dist/commands/uninstall.js +339 -0
  145. package/dist/commands/uninstall.js.map +1 -0
  146. package/dist/commands/update.d.ts +72 -0
  147. package/dist/commands/update.d.ts.map +1 -0
  148. package/dist/commands/update.js +1926 -0
  149. package/dist/commands/update.js.map +1 -0
  150. package/dist/commands/upgrade.d.ts +28 -0
  151. package/dist/commands/upgrade.d.ts.map +1 -0
  152. package/dist/commands/upgrade.js +84 -0
  153. package/dist/commands/upgrade.js.map +1 -0
  154. package/dist/configurators/antigravity.d.ts +7 -0
  155. package/dist/configurators/antigravity.d.ts.map +1 -0
  156. package/dist/configurators/antigravity.js +19 -0
  157. package/dist/configurators/antigravity.js.map +1 -0
  158. package/dist/configurators/claude.d.ts +9 -0
  159. package/dist/configurators/claude.d.ts.map +1 -0
  160. package/dist/configurators/claude.js +72 -0
  161. package/dist/configurators/claude.js.map +1 -0
  162. package/dist/configurators/codebuddy.d.ts +10 -0
  163. package/dist/configurators/codebuddy.d.ts.map +1 -0
  164. package/dist/configurators/codebuddy.js +30 -0
  165. package/dist/configurators/codebuddy.js.map +1 -0
  166. package/dist/configurators/codex.d.ts +8 -0
  167. package/dist/configurators/codex.d.ts.map +1 -0
  168. package/dist/configurators/codex.js +87 -0
  169. package/dist/configurators/codex.js.map +1 -0
  170. package/dist/configurators/copilot.d.ts +10 -0
  171. package/dist/configurators/copilot.d.ts.map +1 -0
  172. package/dist/configurators/copilot.js +51 -0
  173. package/dist/configurators/copilot.js.map +1 -0
  174. package/dist/configurators/cursor.d.ts +10 -0
  175. package/dist/configurators/cursor.d.ts.map +1 -0
  176. package/dist/configurators/cursor.js +29 -0
  177. package/dist/configurators/cursor.js.map +1 -0
  178. package/dist/configurators/droid.d.ts +10 -0
  179. package/dist/configurators/droid.d.ts.map +1 -0
  180. package/dist/configurators/droid.js +30 -0
  181. package/dist/configurators/droid.js.map +1 -0
  182. package/dist/configurators/gemini.d.ts +16 -0
  183. package/dist/configurators/gemini.d.ts.map +1 -0
  184. package/dist/configurators/gemini.js +38 -0
  185. package/dist/configurators/gemini.js.map +1 -0
  186. package/dist/configurators/index.d.ts +65 -0
  187. package/dist/configurators/index.d.ts.map +1 -0
  188. package/dist/configurators/index.js +367 -0
  189. package/dist/configurators/index.js.map +1 -0
  190. package/dist/configurators/kilo.d.ts +7 -0
  191. package/dist/configurators/kilo.d.ts.map +1 -0
  192. package/dist/configurators/kilo.js +19 -0
  193. package/dist/configurators/kilo.js.map +1 -0
  194. package/dist/configurators/kiro.d.ts +8 -0
  195. package/dist/configurators/kiro.d.ts.map +1 -0
  196. package/dist/configurators/kiro.js +24 -0
  197. package/dist/configurators/kiro.js.map +1 -0
  198. package/dist/configurators/opencode.d.ts +14 -0
  199. package/dist/configurators/opencode.d.ts.map +1 -0
  200. package/dist/configurators/opencode.js +96 -0
  201. package/dist/configurators/opencode.js.map +1 -0
  202. package/dist/configurators/pi.d.ts +3 -0
  203. package/dist/configurators/pi.d.ts.map +1 -0
  204. package/dist/configurators/pi.js +45 -0
  205. package/dist/configurators/pi.js.map +1 -0
  206. package/dist/configurators/qoder.d.ts +11 -0
  207. package/dist/configurators/qoder.d.ts.map +1 -0
  208. package/dist/configurators/qoder.js +31 -0
  209. package/dist/configurators/qoder.js.map +1 -0
  210. package/dist/configurators/shared.d.ts +178 -0
  211. package/dist/configurators/shared.d.ts.map +1 -0
  212. package/dist/configurators/shared.js +538 -0
  213. package/dist/configurators/shared.js.map +1 -0
  214. package/dist/configurators/windsurf.d.ts +7 -0
  215. package/dist/configurators/windsurf.d.ts.map +1 -0
  216. package/dist/configurators/windsurf.js +19 -0
  217. package/dist/configurators/windsurf.js.map +1 -0
  218. package/dist/configurators/workflow.d.ts +29 -0
  219. package/dist/configurators/workflow.d.ts.map +1 -0
  220. package/dist/configurators/workflow.js +163 -0
  221. package/dist/configurators/workflow.js.map +1 -0
  222. package/dist/constants/paths.d.ts +70 -0
  223. package/dist/constants/paths.d.ts.map +1 -0
  224. package/dist/constants/paths.js +79 -0
  225. package/dist/constants/paths.js.map +1 -0
  226. package/dist/constants/version.d.ts +9 -0
  227. package/dist/constants/version.d.ts.map +1 -0
  228. package/dist/constants/version.js +15 -0
  229. package/dist/constants/version.js.map +1 -0
  230. package/dist/index.d.ts +9 -0
  231. package/dist/index.d.ts.map +1 -0
  232. package/dist/index.js +9 -0
  233. package/dist/index.js.map +1 -0
  234. package/dist/migrations/index.d.ts +62 -0
  235. package/dist/migrations/index.d.ts.map +1 -0
  236. package/dist/migrations/index.js +187 -0
  237. package/dist/migrations/index.js.map +1 -0
  238. package/dist/migrations/manifests/0.1.9.json +30 -0
  239. package/dist/migrations/manifests/0.2.0.json +49 -0
  240. package/dist/migrations/manifests/0.2.12.json +9 -0
  241. package/dist/migrations/manifests/0.2.13.json +9 -0
  242. package/dist/migrations/manifests/0.2.14.json +175 -0
  243. package/dist/migrations/manifests/0.2.15.json +33 -0
  244. package/dist/migrations/manifests/0.3.0-beta.0.json +297 -0
  245. package/dist/migrations/manifests/0.3.0-beta.1.json +9 -0
  246. package/dist/migrations/manifests/0.3.0-beta.10.json +9 -0
  247. package/dist/migrations/manifests/0.3.0-beta.11.json +9 -0
  248. package/dist/migrations/manifests/0.3.0-beta.12.json +9 -0
  249. package/dist/migrations/manifests/0.3.0-beta.13.json +9 -0
  250. package/dist/migrations/manifests/0.3.0-beta.14.json +9 -0
  251. package/dist/migrations/manifests/0.3.0-beta.15.json +9 -0
  252. package/dist/migrations/manifests/0.3.0-beta.16.json +9 -0
  253. package/dist/migrations/manifests/0.3.0-beta.2.json +9 -0
  254. package/dist/migrations/manifests/0.3.0-beta.3.json +9 -0
  255. package/dist/migrations/manifests/0.3.0-beta.4.json +9 -0
  256. package/dist/migrations/manifests/0.3.0-beta.5.json +9 -0
  257. package/dist/migrations/manifests/0.3.0-beta.6.json +9 -0
  258. package/dist/migrations/manifests/0.3.0-beta.7.json +11 -0
  259. package/dist/migrations/manifests/0.3.0-beta.8.json +9 -0
  260. package/dist/migrations/manifests/0.3.0-beta.9.json +9 -0
  261. package/dist/migrations/manifests/0.3.0-rc.0.json +9 -0
  262. package/dist/migrations/manifests/0.3.0-rc.1.json +9 -0
  263. package/dist/migrations/manifests/0.3.0-rc.2.json +9 -0
  264. package/dist/migrations/manifests/0.3.0-rc.3.json +9 -0
  265. package/dist/migrations/manifests/0.3.0-rc.4.json +9 -0
  266. package/dist/migrations/manifests/0.3.0-rc.5.json +9 -0
  267. package/dist/migrations/manifests/0.3.0-rc.6.json +9 -0
  268. package/dist/migrations/manifests/0.3.0.json +11 -0
  269. package/dist/migrations/manifests/0.3.1.json +9 -0
  270. package/dist/migrations/manifests/0.3.10.json +9 -0
  271. package/dist/migrations/manifests/0.3.2.json +9 -0
  272. package/dist/migrations/manifests/0.3.3.json +9 -0
  273. package/dist/migrations/manifests/0.3.4.json +21 -0
  274. package/dist/migrations/manifests/0.3.5.json +9 -0
  275. package/dist/migrations/manifests/0.3.6.json +9 -0
  276. package/dist/migrations/manifests/0.3.7.json +9 -0
  277. package/dist/migrations/manifests/0.3.8.json +9 -0
  278. package/dist/migrations/manifests/0.3.9.json +9 -0
  279. package/dist/migrations/manifests/0.4.0-beta.1.json +228 -0
  280. package/dist/migrations/manifests/0.4.0-beta.10.json +9 -0
  281. package/dist/migrations/manifests/0.4.0-beta.2.json +9 -0
  282. package/dist/migrations/manifests/0.4.0-beta.3.json +9 -0
  283. package/dist/migrations/manifests/0.4.0-beta.4.json +9 -0
  284. package/dist/migrations/manifests/0.4.0-beta.5.json +9 -0
  285. package/dist/migrations/manifests/0.4.0-beta.6.json +9 -0
  286. package/dist/migrations/manifests/0.4.0-beta.7.json +9 -0
  287. package/dist/migrations/manifests/0.4.0-beta.8.json +34 -0
  288. package/dist/migrations/manifests/0.4.0-beta.9.json +9 -0
  289. package/dist/migrations/manifests/0.4.0-rc.0.json +9 -0
  290. package/dist/migrations/manifests/0.4.0-rc.1.json +9 -0
  291. package/dist/migrations/manifests/0.4.0.json +9 -0
  292. package/dist/migrations/manifests/0.5.0-beta.0.json +1646 -0
  293. package/dist/migrations/manifests/0.5.0-beta.1.json +9 -0
  294. package/dist/migrations/manifests/0.5.0-beta.10.json +9 -0
  295. package/dist/migrations/manifests/0.5.0-beta.11.json +9 -0
  296. package/dist/migrations/manifests/0.5.0-beta.12.json +9 -0
  297. package/dist/migrations/manifests/0.5.0-beta.13.json +9 -0
  298. package/dist/migrations/manifests/0.5.0-beta.14.json +9 -0
  299. package/dist/migrations/manifests/0.5.0-beta.15.json +116 -0
  300. package/dist/migrations/manifests/0.5.0-beta.16.json +9 -0
  301. package/dist/migrations/manifests/0.5.0-beta.17.json +9 -0
  302. package/dist/migrations/manifests/0.5.0-beta.18.json +9 -0
  303. package/dist/migrations/manifests/0.5.0-beta.19.json +9 -0
  304. package/dist/migrations/manifests/0.5.0-beta.2.json +9 -0
  305. package/dist/migrations/manifests/0.5.0-beta.3.json +9 -0
  306. package/dist/migrations/manifests/0.5.0-beta.4.json +9 -0
  307. package/dist/migrations/manifests/0.5.0-beta.5.json +222 -0
  308. package/dist/migrations/manifests/0.5.0-beta.6.json +9 -0
  309. package/dist/migrations/manifests/0.5.0-beta.7.json +9 -0
  310. package/dist/migrations/manifests/0.5.0-beta.8.json +9 -0
  311. package/dist/migrations/manifests/0.5.0-beta.9.json +48 -0
  312. package/dist/migrations/manifests/0.5.0-rc.0.json +9 -0
  313. package/dist/migrations/manifests/0.5.0-rc.1.json +9 -0
  314. package/dist/migrations/manifests/0.5.0-rc.2.json +9 -0
  315. package/dist/migrations/manifests/0.5.0-rc.3.json +9 -0
  316. package/dist/migrations/manifests/0.5.0-rc.4.json +9 -0
  317. package/dist/migrations/manifests/0.5.0-rc.5.json +9 -0
  318. package/dist/migrations/manifests/0.5.0-rc.6.json +9 -0
  319. package/dist/migrations/manifests/0.5.0-rc.7.json +9 -0
  320. package/dist/migrations/manifests/0.5.0.json +9 -0
  321. package/dist/migrations/manifests/0.5.1.json +9 -0
  322. package/dist/migrations/manifests/0.5.10.json +9 -0
  323. package/dist/migrations/manifests/0.5.11.json +16 -0
  324. package/dist/migrations/manifests/0.5.12.json +9 -0
  325. package/dist/migrations/manifests/0.5.13.json +9 -0
  326. package/dist/migrations/manifests/0.5.14.json +9 -0
  327. package/dist/migrations/manifests/0.5.15.json +9 -0
  328. package/dist/migrations/manifests/0.5.2.json +9 -0
  329. package/dist/migrations/manifests/0.5.3.json +9 -0
  330. package/dist/migrations/manifests/0.5.4.json +9 -0
  331. package/dist/migrations/manifests/0.5.5.json +9 -0
  332. package/dist/migrations/manifests/0.5.6.json +9 -0
  333. package/dist/migrations/manifests/0.5.7.json +16 -0
  334. package/dist/migrations/manifests/0.5.8.json +9 -0
  335. package/dist/migrations/manifests/0.5.9.json +9 -0
  336. package/dist/migrations/manifests/0.6.0-beta.0.json +16 -0
  337. package/dist/migrations/manifests/0.6.0-beta.1.json +9 -0
  338. package/dist/migrations/manifests/0.6.0-beta.10.json +9 -0
  339. package/dist/migrations/manifests/0.6.0-beta.11.json +9 -0
  340. package/dist/migrations/manifests/0.6.0-beta.12.json +9 -0
  341. package/dist/migrations/manifests/0.6.0-beta.13.json +9 -0
  342. package/dist/migrations/manifests/0.6.0-beta.14.json +9 -0
  343. package/dist/migrations/manifests/0.6.0-beta.2.json +9 -0
  344. package/dist/migrations/manifests/0.6.0-beta.3.json +9 -0
  345. package/dist/migrations/manifests/0.6.0-beta.4.json +9 -0
  346. package/dist/migrations/manifests/0.6.0-beta.5.json +9 -0
  347. package/dist/migrations/manifests/0.6.0-beta.6.json +16 -0
  348. package/dist/migrations/manifests/0.6.0-beta.7.json +9 -0
  349. package/dist/migrations/manifests/0.6.0-beta.8.json +9 -0
  350. package/dist/migrations/manifests/0.6.0-beta.9.json +9 -0
  351. package/dist/templates/claude/agents/trellis-check.md +114 -0
  352. package/dist/templates/claude/agents/trellis-implement.md +113 -0
  353. package/dist/templates/claude/agents/trellis-research.md +137 -0
  354. package/dist/templates/claude/index.d.ts +22 -0
  355. package/dist/templates/claude/index.d.ts.map +1 -0
  356. package/dist/templates/claude/index.js +46 -0
  357. package/dist/templates/claude/index.js.map +1 -0
  358. package/dist/templates/claude/settings.json +73 -0
  359. package/dist/templates/codebuddy/agents/trellis-check.md +109 -0
  360. package/dist/templates/codebuddy/agents/trellis-implement.md +110 -0
  361. package/dist/templates/codebuddy/agents/trellis-research.md +137 -0
  362. package/dist/templates/codebuddy/index.d.ts +15 -0
  363. package/dist/templates/codebuddy/index.d.ts.map +1 -0
  364. package/dist/templates/codebuddy/index.js +15 -0
  365. package/dist/templates/codebuddy/index.js.map +1 -0
  366. package/dist/templates/codebuddy/settings.json +59 -0
  367. package/dist/templates/codex/agents/trellis-check.toml +84 -0
  368. package/dist/templates/codex/agents/trellis-implement.toml +65 -0
  369. package/dist/templates/codex/agents/trellis-research.toml +73 -0
  370. package/dist/templates/codex/config.toml +35 -0
  371. package/dist/templates/codex/hooks/session-start.py +545 -0
  372. package/dist/templates/codex/hooks.json +15 -0
  373. package/dist/templates/codex/index.d.ts +39 -0
  374. package/dist/templates/codex/index.d.ts.map +1 -0
  375. package/dist/templates/codex/index.js +85 -0
  376. package/dist/templates/codex/index.js.map +1 -0
  377. package/dist/templates/codex/skills/before-dev/SKILL.md +40 -0
  378. package/dist/templates/codex/skills/brainstorm/SKILL.md +112 -0
  379. package/dist/templates/codex/skills/break-loop/SKILL.md +130 -0
  380. package/dist/templates/codex/skills/check/SKILL.md +98 -0
  381. package/dist/templates/codex/skills/check-cross-layer/SKILL.md +158 -0
  382. package/dist/templates/codex/skills/create-command/SKILL.md +101 -0
  383. package/dist/templates/codex/skills/finish-work/SKILL.md +90 -0
  384. package/dist/templates/codex/skills/improve-ut/SKILL.md +69 -0
  385. package/dist/templates/codex/skills/integrate-skill/SKILL.md +221 -0
  386. package/dist/templates/codex/skills/onboard/SKILL.md +363 -0
  387. package/dist/templates/codex/skills/record-session/SKILL.md +67 -0
  388. package/dist/templates/codex/skills/start/SKILL.md +64 -0
  389. package/dist/templates/codex/skills/update-spec/SKILL.md +335 -0
  390. package/dist/templates/common/bundled-skills/trellis-meta/SKILL.md +73 -0
  391. package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/add-project-local-conventions.md +83 -0
  392. package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-agents.md +54 -0
  393. package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-context-loading.md +84 -0
  394. package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-hooks.md +57 -0
  395. package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-skills-or-commands.md +78 -0
  396. package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-spec-structure.md +83 -0
  397. package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-task-lifecycle.md +90 -0
  398. package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-workflow.md +65 -0
  399. package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/overview.md +55 -0
  400. package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/context-injection.md +68 -0
  401. package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/generated-files.md +80 -0
  402. package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/overview.md +51 -0
  403. package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/spec-system.md +102 -0
  404. package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/task-system.md +103 -0
  405. package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/workflow.md +75 -0
  406. package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/workspace-memory.md +71 -0
  407. package/dist/templates/common/bundled-skills/trellis-meta/references/platform-files/agents.md +80 -0
  408. package/dist/templates/common/bundled-skills/trellis-meta/references/platform-files/hooks-and-settings.md +69 -0
  409. package/dist/templates/common/bundled-skills/trellis-meta/references/platform-files/overview.md +59 -0
  410. package/dist/templates/common/bundled-skills/trellis-meta/references/platform-files/platform-map.md +74 -0
  411. package/dist/templates/common/bundled-skills/trellis-meta/references/platform-files/skills-and-commands.md +83 -0
  412. package/dist/templates/common/commands/continue.md +56 -0
  413. package/dist/templates/common/commands/finish-work.md +66 -0
  414. package/dist/templates/common/commands/start.md +59 -0
  415. package/dist/templates/common/index.d.ts +48 -0
  416. package/dist/templates/common/index.d.ts.map +1 -0
  417. package/dist/templates/common/index.js +104 -0
  418. package/dist/templates/common/index.js.map +1 -0
  419. package/dist/templates/common/skills/before-dev.md +35 -0
  420. package/dist/templates/common/skills/brainstorm.md +112 -0
  421. package/dist/templates/common/skills/break-loop.md +125 -0
  422. package/dist/templates/common/skills/check.md +93 -0
  423. package/dist/templates/common/skills/update-spec.md +351 -0
  424. package/dist/templates/copilot/hooks/session-start.py +547 -0
  425. package/dist/templates/copilot/hooks.json +19 -0
  426. package/dist/templates/copilot/index.d.ts +23 -0
  427. package/dist/templates/copilot/index.d.ts.map +1 -0
  428. package/dist/templates/copilot/index.js +54 -0
  429. package/dist/templates/copilot/index.js.map +1 -0
  430. package/dist/templates/copilot/prompts/before-dev.prompt.md +39 -0
  431. package/dist/templates/copilot/prompts/brainstorm.prompt.md +111 -0
  432. package/dist/templates/copilot/prompts/break-loop.prompt.md +129 -0
  433. package/dist/templates/copilot/prompts/check-cross-layer.prompt.md +157 -0
  434. package/dist/templates/copilot/prompts/check.prompt.md +97 -0
  435. package/dist/templates/copilot/prompts/create-command.prompt.md +116 -0
  436. package/dist/templates/copilot/prompts/finish-work.prompt.md +99 -0
  437. package/dist/templates/copilot/prompts/integrate-skill.prompt.md +223 -0
  438. package/dist/templates/copilot/prompts/onboard.prompt.md +362 -0
  439. package/dist/templates/copilot/prompts/parallel.prompt.md +204 -0
  440. package/dist/templates/copilot/prompts/record-session.prompt.md +66 -0
  441. package/dist/templates/copilot/prompts/start.prompt.md +63 -0
  442. package/dist/templates/copilot/prompts/update-spec.prompt.md +358 -0
  443. package/dist/templates/cursor/agents/trellis-check.md +108 -0
  444. package/dist/templates/cursor/agents/trellis-implement.md +109 -0
  445. package/dist/templates/cursor/agents/trellis-research.md +136 -0
  446. package/dist/templates/cursor/hooks.json +30 -0
  447. package/dist/templates/cursor/index.d.ts +13 -0
  448. package/dist/templates/cursor/index.d.ts.map +1 -0
  449. package/dist/templates/cursor/index.js +13 -0
  450. package/dist/templates/cursor/index.js.map +1 -0
  451. package/dist/templates/droid/droids/trellis-check.md +101 -0
  452. package/dist/templates/droid/droids/trellis-implement.md +102 -0
  453. package/dist/templates/droid/droids/trellis-research.md +137 -0
  454. package/dist/templates/droid/index.d.ts +15 -0
  455. package/dist/templates/droid/index.d.ts.map +1 -0
  456. package/dist/templates/droid/index.js +15 -0
  457. package/dist/templates/droid/index.js.map +1 -0
  458. package/dist/templates/droid/settings.json +59 -0
  459. package/dist/templates/extract.d.ts +40 -0
  460. package/dist/templates/extract.d.ts.map +1 -0
  461. package/dist/templates/extract.js +106 -0
  462. package/dist/templates/extract.js.map +1 -0
  463. package/dist/templates/gemini/agents/trellis-check.md +101 -0
  464. package/dist/templates/gemini/agents/trellis-implement.md +102 -0
  465. package/dist/templates/gemini/agents/trellis-research.md +136 -0
  466. package/dist/templates/gemini/index.d.ts +13 -0
  467. package/dist/templates/gemini/index.d.ts.map +1 -0
  468. package/dist/templates/gemini/index.js +13 -0
  469. package/dist/templates/gemini/index.js.map +1 -0
  470. package/dist/templates/gemini/settings.json +28 -0
  471. package/dist/templates/kiro/agents/trellis-check.json +26 -0
  472. package/dist/templates/kiro/agents/trellis-implement.json +26 -0
  473. package/dist/templates/kiro/agents/trellis-research.json +30 -0
  474. package/dist/templates/kiro/index.d.ts +18 -0
  475. package/dist/templates/kiro/index.d.ts.map +1 -0
  476. package/dist/templates/kiro/index.js +18 -0
  477. package/dist/templates/kiro/index.js.map +1 -0
  478. package/dist/templates/markdown/agents.md +21 -0
  479. package/dist/templates/markdown/gitignore.txt +15 -0
  480. package/dist/templates/markdown/index.d.ts +27 -0
  481. package/dist/templates/markdown/index.d.ts.map +1 -0
  482. package/dist/templates/markdown/index.js +52 -0
  483. package/dist/templates/markdown/index.js.map +1 -0
  484. package/dist/templates/markdown/spec/backend/database-guidelines.md.txt +51 -0
  485. package/dist/templates/markdown/spec/backend/directory-structure.md.txt +54 -0
  486. package/dist/templates/markdown/spec/backend/error-handling.md.txt +51 -0
  487. package/dist/templates/markdown/spec/backend/index.md.txt +38 -0
  488. package/dist/templates/markdown/spec/backend/logging-guidelines.md.txt +51 -0
  489. package/dist/templates/markdown/spec/backend/quality-guidelines.md.txt +51 -0
  490. package/dist/templates/markdown/spec/frontend/component-guidelines.md.txt +59 -0
  491. package/dist/templates/markdown/spec/frontend/directory-structure.md.txt +54 -0
  492. package/dist/templates/markdown/spec/frontend/hook-guidelines.md.txt +51 -0
  493. package/dist/templates/markdown/spec/frontend/index.md.txt +39 -0
  494. package/dist/templates/markdown/spec/frontend/quality-guidelines.md.txt +51 -0
  495. package/dist/templates/markdown/spec/frontend/state-management.md.txt +51 -0
  496. package/dist/templates/markdown/spec/frontend/type-safety.md.txt +51 -0
  497. package/dist/templates/markdown/spec/guides/code-reuse-thinking-guide.md.txt +223 -0
  498. package/dist/templates/markdown/spec/guides/cross-layer-thinking-guide.md.txt +259 -0
  499. package/dist/templates/markdown/spec/guides/cross-platform-thinking-guide.md.txt +595 -0
  500. package/dist/templates/markdown/spec/guides/index.md.txt +97 -0
  501. package/dist/templates/markdown/workspace-index.md +125 -0
  502. package/dist/templates/markdown/worktree.yaml.txt +58 -0
  503. package/dist/templates/opencode/agents/trellis-check.md +116 -0
  504. package/dist/templates/opencode/agents/trellis-implement.md +118 -0
  505. package/dist/templates/opencode/agents/trellis-research.md +145 -0
  506. package/dist/templates/opencode/lib/session-utils.js +521 -0
  507. package/dist/templates/opencode/lib/trellis-context.js +381 -0
  508. package/dist/templates/opencode/package.json +5 -0
  509. package/dist/templates/opencode/plugins/inject-subagent-context.js +513 -0
  510. package/dist/templates/opencode/plugins/inject-workflow-state.js +159 -0
  511. package/dist/templates/opencode/plugins/session-start.js +101 -0
  512. package/dist/templates/pi/agents/trellis-check.md +36 -0
  513. package/dist/templates/pi/agents/trellis-implement.md +41 -0
  514. package/dist/templates/pi/agents/trellis-research.md +25 -0
  515. package/dist/templates/pi/extensions/trellis/index.ts.txt +1174 -0
  516. package/dist/templates/pi/index.d.ts +5 -0
  517. package/dist/templates/pi/index.d.ts.map +1 -0
  518. package/dist/templates/pi/index.js +12 -0
  519. package/dist/templates/pi/index.js.map +1 -0
  520. package/dist/templates/pi/settings.json +21 -0
  521. package/dist/templates/qoder/agents/trellis-check.md +102 -0
  522. package/dist/templates/qoder/agents/trellis-implement.md +103 -0
  523. package/dist/templates/qoder/agents/trellis-research.md +137 -0
  524. package/dist/templates/qoder/index.d.ts +15 -0
  525. package/dist/templates/qoder/index.d.ts.map +1 -0
  526. package/dist/templates/qoder/index.js +15 -0
  527. package/dist/templates/qoder/index.js.map +1 -0
  528. package/dist/templates/qoder/settings.json +47 -0
  529. package/dist/templates/shared-hooks/index.d.ts +50 -0
  530. package/dist/templates/shared-hooks/index.d.ts.map +1 -0
  531. package/dist/templates/shared-hooks/index.js +89 -0
  532. package/dist/templates/shared-hooks/index.js.map +1 -0
  533. package/dist/templates/shared-hooks/inject-shell-session-context.py +183 -0
  534. package/dist/templates/shared-hooks/inject-subagent-context.py +771 -0
  535. package/dist/templates/shared-hooks/inject-workflow-state.py +363 -0
  536. package/dist/templates/shared-hooks/session-start.py +827 -0
  537. package/dist/templates/template-utils.d.ts +26 -0
  538. package/dist/templates/template-utils.d.ts.map +1 -0
  539. package/dist/templates/template-utils.js +60 -0
  540. package/dist/templates/template-utils.js.map +1 -0
  541. package/dist/templates/trellis/config.yaml +90 -0
  542. package/dist/templates/trellis/gitignore.txt +32 -0
  543. package/dist/templates/trellis/index.d.ts +52 -0
  544. package/dist/templates/trellis/index.d.ts.map +1 -0
  545. package/dist/templates/trellis/index.js +97 -0
  546. package/dist/templates/trellis/index.js.map +1 -0
  547. package/dist/templates/trellis/scripts/__init__.py +5 -0
  548. package/dist/templates/trellis/scripts/add_session.py +547 -0
  549. package/dist/templates/trellis/scripts/common/__init__.py +92 -0
  550. package/dist/templates/trellis/scripts/common/active_task.py +626 -0
  551. package/dist/templates/trellis/scripts/common/cli_adapter.py +811 -0
  552. package/dist/templates/trellis/scripts/common/config.py +445 -0
  553. package/dist/templates/trellis/scripts/common/developer.py +190 -0
  554. package/dist/templates/trellis/scripts/common/git.py +31 -0
  555. package/dist/templates/trellis/scripts/common/git_context.py +106 -0
  556. package/dist/templates/trellis/scripts/common/io.py +37 -0
  557. package/dist/templates/trellis/scripts/common/log.py +45 -0
  558. package/dist/templates/trellis/scripts/common/packages_context.py +238 -0
  559. package/dist/templates/trellis/scripts/common/paths.py +447 -0
  560. package/dist/templates/trellis/scripts/common/safe_commit.py +285 -0
  561. package/dist/templates/trellis/scripts/common/session_context.py +821 -0
  562. package/dist/templates/trellis/scripts/common/task_context.py +223 -0
  563. package/dist/templates/trellis/scripts/common/task_queue.py +188 -0
  564. package/dist/templates/trellis/scripts/common/task_store.py +698 -0
  565. package/dist/templates/trellis/scripts/common/task_utils.py +274 -0
  566. package/dist/templates/trellis/scripts/common/tasks.py +112 -0
  567. package/dist/templates/trellis/scripts/common/trellis_config.py +131 -0
  568. package/dist/templates/trellis/scripts/common/types.py +110 -0
  569. package/dist/templates/trellis/scripts/common/workflow_phase.py +212 -0
  570. package/dist/templates/trellis/scripts/get_context.py +16 -0
  571. package/dist/templates/trellis/scripts/get_developer.py +26 -0
  572. package/dist/templates/trellis/scripts/hooks/linear_sync.py +243 -0
  573. package/dist/templates/trellis/scripts/init_developer.py +51 -0
  574. package/dist/templates/trellis/scripts/task.py +500 -0
  575. package/dist/templates/trellis/tasks/.gitkeep +0 -0
  576. package/dist/templates/trellis/workflow.md +690 -0
  577. package/dist/types/ai-tools.d.ts +95 -0
  578. package/dist/types/ai-tools.d.ts.map +1 -0
  579. package/dist/types/ai-tools.js +280 -0
  580. package/dist/types/ai-tools.js.map +1 -0
  581. package/dist/types/migration.d.ts +125 -0
  582. package/dist/types/migration.d.ts.map +1 -0
  583. package/dist/types/migration.js +8 -0
  584. package/dist/types/migration.js.map +1 -0
  585. package/dist/utils/compare-versions.d.ts +12 -0
  586. package/dist/utils/compare-versions.d.ts.map +1 -0
  587. package/dist/utils/compare-versions.js +86 -0
  588. package/dist/utils/compare-versions.js.map +1 -0
  589. package/dist/utils/cwd-guard.d.ts +38 -0
  590. package/dist/utils/cwd-guard.d.ts.map +1 -0
  591. package/dist/utils/cwd-guard.js +62 -0
  592. package/dist/utils/cwd-guard.js.map +1 -0
  593. package/dist/utils/file-writer.d.ts +36 -0
  594. package/dist/utils/file-writer.d.ts.map +1 -0
  595. package/dist/utils/file-writer.js +203 -0
  596. package/dist/utils/file-writer.js.map +1 -0
  597. package/dist/utils/manifest-prune.d.ts +61 -0
  598. package/dist/utils/manifest-prune.d.ts.map +1 -0
  599. package/dist/utils/manifest-prune.js +136 -0
  600. package/dist/utils/manifest-prune.js.map +1 -0
  601. package/dist/utils/posix.d.ts +13 -0
  602. package/dist/utils/posix.d.ts.map +1 -0
  603. package/dist/utils/posix.js +15 -0
  604. package/dist/utils/posix.js.map +1 -0
  605. package/dist/utils/project-detector.d.ts +46 -0
  606. package/dist/utils/project-detector.d.ts.map +1 -0
  607. package/dist/utils/project-detector.js +666 -0
  608. package/dist/utils/project-detector.js.map +1 -0
  609. package/dist/utils/proxy.d.ts +25 -0
  610. package/dist/utils/proxy.d.ts.map +1 -0
  611. package/dist/utils/proxy.js +60 -0
  612. package/dist/utils/proxy.js.map +1 -0
  613. package/dist/utils/task-json.d.ts +13 -0
  614. package/dist/utils/task-json.d.ts.map +1 -0
  615. package/dist/utils/task-json.js +12 -0
  616. package/dist/utils/task-json.js.map +1 -0
  617. package/dist/utils/template-fetcher.d.ts +150 -0
  618. package/dist/utils/template-fetcher.d.ts.map +1 -0
  619. package/dist/utils/template-fetcher.js +907 -0
  620. package/dist/utils/template-fetcher.js.map +1 -0
  621. package/dist/utils/template-hash.d.ts +123 -0
  622. package/dist/utils/template-hash.d.ts.map +1 -0
  623. package/dist/utils/template-hash.js +334 -0
  624. package/dist/utils/template-hash.js.map +1 -0
  625. package/dist/utils/uninstall-scrubbers.d.ts +66 -0
  626. package/dist/utils/uninstall-scrubbers.d.ts.map +1 -0
  627. package/dist/utils/uninstall-scrubbers.js +342 -0
  628. package/dist/utils/uninstall-scrubbers.js.map +1 -0
  629. package/package.json +90 -0
@@ -0,0 +1,1869 @@
1
+ /**
2
+ * mem.ts — search sessions across Claude Code / Codex / OpenCode.
3
+ *
4
+ * Commands:
5
+ * list list sessions (default if no command)
6
+ * search <keyword> find sessions whose contents match keyword
7
+ * context <session-id> drill-down: top-N hit turns + surrounding context
8
+ * extract <session-id> dump cleaned dialogue (use --grep KW to filter turns)
9
+ * projects list active project cwds (AI-routing entry point)
10
+ *
11
+ * Run `trellis mem help` for the full flag reference.
12
+ */
13
+ import * as fs from "node:fs";
14
+ import * as path from "node:path";
15
+ import * as os from "node:os";
16
+ import { z } from "zod";
17
+ // ---------- schemas: domain types ----------
18
+ const PlatformSchema = z.enum(["claude", "codex", "opencode"]);
19
+ const SessionInfoSchema = z.object({
20
+ platform: PlatformSchema,
21
+ id: z.string(),
22
+ title: z.string().optional(),
23
+ cwd: z.string().optional(),
24
+ created: z.string().optional(),
25
+ updated: z.string().optional(),
26
+ filePath: z.string(),
27
+ parent_id: z.string().optional(), // OpenCode only: parent session id (sub-agent chain)
28
+ });
29
+ const DialogueRoleSchema = z.enum(["user", "assistant"]);
30
+ const SearchExcerptSchema = z.object({
31
+ role: DialogueRoleSchema,
32
+ snippet: z.string(),
33
+ });
34
+ const SearchHitSchema = z.object({
35
+ count: z.number(), // total token occurrences across all matching turns
36
+ user_count: z.number(), // breakdown: user-turn occurrences
37
+ asst_count: z.number(), // breakdown: assistant-turn occurrences
38
+ total_turns: z.number(), // size of cleaned dialogue (denominator for density)
39
+ excerpts: z.array(SearchExcerptSchema),
40
+ });
41
+ /** Weighted-density relevance score:
42
+ * (3 * user_hits + asst_hits) / total_turns
43
+ * Higher = the session is more topically concentrated on the query AND the
44
+ * user themselves brought it up (user hits weighted ×3 because the user's own
45
+ * words anchor "what they actually cared about", while assistant elaboration
46
+ * is downstream noise). */
47
+ export function relevanceScore(h) {
48
+ if (h.total_turns === 0)
49
+ return 0;
50
+ return (3 * h.user_count + h.asst_count) / h.total_turns;
51
+ }
52
+ const FilterSchema = z.object({
53
+ platform: z.union([PlatformSchema, z.literal("all")]),
54
+ since: z.date().optional(),
55
+ until: z.date().optional(),
56
+ cwd: z.string().optional(),
57
+ limit: z.number(),
58
+ });
59
+ const ArgvSchema = z.object({
60
+ cmd: z.string(),
61
+ positional: z.array(z.string()),
62
+ flags: z.record(z.string(), z.union([z.string(), z.boolean()])),
63
+ });
64
+ // ---------- schemas: external file formats ----------
65
+ // Claude Code JSONL events. We only declare the fields we read; everything
66
+ // else passes through. Content of an assistant `message` is an array of
67
+ // blocks (text / thinking / tool_use); content of a user `message` is a
68
+ // string for real human input or an array of tool_result blocks (skipped).
69
+ const ClaudeBlockSchema = z
70
+ .object({
71
+ type: z.string().optional(),
72
+ text: z.string().optional(),
73
+ })
74
+ .loose();
75
+ const ClaudeMessageSchema = z
76
+ .object({
77
+ role: z.string().optional(),
78
+ content: z.union([z.string(), z.array(ClaudeBlockSchema)]).optional(),
79
+ })
80
+ .loose();
81
+ const ClaudeEventSchema = z
82
+ .object({
83
+ type: z.string().optional(),
84
+ cwd: z.string().optional(),
85
+ timestamp: z.string().optional(),
86
+ message: ClaudeMessageSchema.optional(),
87
+ isCompactSummary: z.boolean().optional(),
88
+ })
89
+ .loose();
90
+ const ClaudeIndexEntrySchema = z
91
+ .object({
92
+ id: z.string(),
93
+ cwd: z.string().optional(),
94
+ created: z.string().optional(),
95
+ title: z.string().optional(),
96
+ })
97
+ .loose();
98
+ const ClaudeIndexSchema = z
99
+ .object({ entries: z.array(ClaudeIndexEntrySchema).optional() })
100
+ .loose();
101
+ // Codex rollout JSONL events.
102
+ const CodexContentPartSchema = z
103
+ .object({
104
+ type: z.string().optional(),
105
+ text: z.string().optional(),
106
+ })
107
+ .loose();
108
+ const CodexCompactedItemSchema = z
109
+ .object({
110
+ type: z.string().optional(),
111
+ role: z.string().optional(),
112
+ content: z.array(CodexContentPartSchema).optional(),
113
+ })
114
+ .loose();
115
+ const CodexPayloadSchema = z
116
+ .object({
117
+ type: z.string().optional(),
118
+ role: z.string().optional(),
119
+ cwd: z.string().optional(),
120
+ id: z.string().optional(),
121
+ content: z.array(CodexContentPartSchema).optional(),
122
+ replacement_history: z.array(CodexCompactedItemSchema).optional(),
123
+ })
124
+ .loose();
125
+ const CodexEventSchema = z
126
+ .object({
127
+ timestamp: z.string().optional(),
128
+ type: z.string().optional(),
129
+ payload: CodexPayloadSchema.optional(),
130
+ })
131
+ .loose();
132
+ // ---------- argv ----------
133
+ export function parseArgv(argv) {
134
+ const cmd = argv[0] ?? "list";
135
+ const positional = [];
136
+ const flags = {};
137
+ for (let i = 1; i < argv.length; i++) {
138
+ const a = argv[i];
139
+ if (a === undefined)
140
+ continue;
141
+ if (a.startsWith("--")) {
142
+ const key = a.slice(2);
143
+ const next = argv[i + 1];
144
+ if (next !== undefined && !next.startsWith("--")) {
145
+ flags[key] = next;
146
+ i++;
147
+ }
148
+ else {
149
+ flags[key] = true;
150
+ }
151
+ }
152
+ else {
153
+ positional.push(a);
154
+ }
155
+ }
156
+ return ArgvSchema.parse({ cmd, positional, flags });
157
+ }
158
+ export function buildFilter(flags) {
159
+ const platformRaw = typeof flags.platform === "string" ? flags.platform : "all";
160
+ const platformParsed = z
161
+ .union([PlatformSchema, z.literal("all")])
162
+ .safeParse(platformRaw);
163
+ if (!platformParsed.success)
164
+ die(`unknown platform: ${platformRaw}`);
165
+ const sinceRaw = flags.since;
166
+ const since = typeof sinceRaw === "string" ? new Date(sinceRaw) : undefined;
167
+ if (since && Number.isNaN(+since))
168
+ die(`bad --since: ${sinceRaw}`);
169
+ const untilRaw = flags.until;
170
+ const until = typeof untilRaw === "string"
171
+ ? new Date(`${untilRaw}T23:59:59.999Z`)
172
+ : undefined;
173
+ if (until && Number.isNaN(+until))
174
+ die(`bad --until: ${untilRaw}`);
175
+ const cwd = flags.global
176
+ ? undefined
177
+ : path.resolve(typeof flags.cwd === "string" ? flags.cwd : process.cwd());
178
+ const limit = typeof flags.limit === "string" ? Number(flags.limit) : 50;
179
+ return FilterSchema.parse({
180
+ platform: platformParsed.data,
181
+ since,
182
+ until,
183
+ cwd,
184
+ limit,
185
+ });
186
+ }
187
+ function die(msg) {
188
+ console.error(`error: ${msg}`);
189
+ process.exit(2);
190
+ }
191
+ // ---------- common helpers ----------
192
+ const HOME = os.homedir();
193
+ export function inRange(iso, f) {
194
+ if (!iso)
195
+ return true;
196
+ const t = new Date(iso);
197
+ if (Number.isNaN(+t))
198
+ return true;
199
+ if (f.since && t < f.since)
200
+ return false;
201
+ if (f.until && t > f.until)
202
+ return false;
203
+ return true;
204
+ }
205
+ /**
206
+ * Interval-overlap version of `inRange` for sessions with both start and end
207
+ * timestamps. A session is kept iff its lifetime `[start, end]` overlaps the
208
+ * query window `[f.since, f.until]`.
209
+ *
210
+ * Why this exists: long / cross-day sessions (created on day N, still updated
211
+ * on day N+M) were being dropped by `inRange(created, f)` when `--since` fell
212
+ * after `created`. Switching to interval overlap keeps sessions that were
213
+ * active inside the window even when they started before it.
214
+ *
215
+ * Degenerate inputs:
216
+ * - both undefined → pass through (no timestamp = don't filter)
217
+ * - one undefined → fall back to single-point semantics on the other end
218
+ * - unparseable iso → defer to the parsable end (or pass through if both bad)
219
+ */
220
+ export function inRangeOverlap(start, end, f) {
221
+ const s = start ?? end;
222
+ const e = end ?? start;
223
+ if (!s && !e)
224
+ return true;
225
+ if (f.since && e) {
226
+ const eT = new Date(e);
227
+ if (!Number.isNaN(+eT) && eT < f.since)
228
+ return false;
229
+ }
230
+ if (f.until && s) {
231
+ const sT = new Date(s);
232
+ if (!Number.isNaN(+sT) && sT > f.until)
233
+ return false;
234
+ }
235
+ return true;
236
+ }
237
+ export function sameProject(sessionCwd, target) {
238
+ if (!target)
239
+ return true;
240
+ if (!sessionCwd)
241
+ return false;
242
+ const a = path.resolve(sessionCwd);
243
+ const b = path.resolve(target);
244
+ return a === b || a.startsWith(b + path.sep);
245
+ }
246
+ /** Walk JSONL line-by-line, calling `onLine` with each parsed object that
247
+ * matches the supplied schema. Bad JSON or schema-mismatched lines are skipped.
248
+ * Returning the literal "stop" from `onLine` halts iteration.
249
+ *
250
+ * Chunked sync streaming: 256 KB read window, leftover preserved across
251
+ * chunks for split-line reassembly. Two practical wins over the original
252
+ * `fs.readFileSync` + `data.split("\n")`:
253
+ *
254
+ * 1. **Bounded peek** — `readJsonlFirst` / `findInJsonl(maxLines<100)` only
255
+ * pull the first chunk (256 KB) and stop, instead of loading multi-MB
256
+ * rollout files in full just to read the head. 30-100× speedup on the
257
+ * listing fan-out path.
258
+ * 2. **Heap floor** — full-scan paths (`extract` / `search`) keep ~256 KB +
259
+ * one leftover line resident instead of 36 MB sessions held as one big
260
+ * UTF-8 string. Roughly 30× peak-heap drop on long sessions.
261
+ *
262
+ * Byte-prefix fast-reject: a JSONL event line virtually always begins with
263
+ * `{` (object). Lines starting with any other byte are blanks, log
264
+ * preambles, or trailing whitespace — `JSON.parse` would throw and
265
+ * `safeParse` would fail. Checking the first byte before allocating the
266
+ * parse exception path saves measurable wall time on heavy sessions. */
267
+ function readJsonl(file, schema, onLine) {
268
+ let fd;
269
+ try {
270
+ fd = fs.openSync(file, "r");
271
+ }
272
+ catch {
273
+ return;
274
+ }
275
+ const CHUNK = 256 * 1024;
276
+ const OPEN_BRACE = 0x7b; // '{'
277
+ const buf = Buffer.alloc(CHUNK);
278
+ let leftover = "";
279
+ try {
280
+ let stop = false;
281
+ while (!stop) {
282
+ const n = fs.readSync(fd, buf, 0, CHUNK, null);
283
+ if (n === 0)
284
+ break;
285
+ const chunk = leftover + buf.toString("utf8", 0, n);
286
+ let from = 0;
287
+ while (true) {
288
+ const nl = chunk.indexOf("\n", from);
289
+ if (nl === -1) {
290
+ leftover = chunk.slice(from);
291
+ break;
292
+ }
293
+ const line = chunk.slice(from, nl);
294
+ from = nl + 1;
295
+ if (!line)
296
+ continue;
297
+ // Byte-prefix fast-reject before JSON.parse / zod.
298
+ if (line.charCodeAt(0) !== OPEN_BRACE)
299
+ continue;
300
+ let raw;
301
+ try {
302
+ raw = JSON.parse(line);
303
+ }
304
+ catch {
305
+ continue;
306
+ }
307
+ const parsed = schema.safeParse(raw);
308
+ if (!parsed.success)
309
+ continue;
310
+ if (onLine(parsed.data) === "stop") {
311
+ stop = true;
312
+ break;
313
+ }
314
+ }
315
+ }
316
+ if (!stop && leftover) {
317
+ // File ended without trailing newline — process the last partial line.
318
+ const line = leftover;
319
+ if (line?.charCodeAt(0) === OPEN_BRACE) {
320
+ try {
321
+ const raw = JSON.parse(line);
322
+ const parsed = schema.safeParse(raw);
323
+ if (parsed.success)
324
+ onLine(parsed.data);
325
+ }
326
+ catch {
327
+ /* skip */
328
+ }
329
+ }
330
+ }
331
+ }
332
+ finally {
333
+ fs.closeSync(fd);
334
+ }
335
+ }
336
+ function readJsonlFirst(file, schema) {
337
+ let result;
338
+ readJsonl(file, schema, (obj) => {
339
+ result = obj;
340
+ return "stop";
341
+ });
342
+ return result;
343
+ }
344
+ function findInJsonl(file, schema, predicate, maxLines = 200) {
345
+ let count = 0;
346
+ let hit;
347
+ readJsonl(file, schema, (obj) => {
348
+ count++;
349
+ if (predicate(obj)) {
350
+ hit = obj;
351
+ return "stop";
352
+ }
353
+ if (count >= maxLines)
354
+ return "stop";
355
+ });
356
+ return hit;
357
+ }
358
+ function readJsonFile(file, schema) {
359
+ let raw;
360
+ try {
361
+ raw = JSON.parse(fs.readFileSync(file, "utf8"));
362
+ }
363
+ catch {
364
+ return undefined;
365
+ }
366
+ const parsed = schema.safeParse(raw);
367
+ return parsed.success ? parsed.data : undefined;
368
+ }
369
+ // ---------- dialogue cleaning ----------
370
+ const INJECTION_TAGS = [
371
+ "system-reminder",
372
+ "task-status",
373
+ "ready",
374
+ "current-state",
375
+ "workflow",
376
+ "workflow-state",
377
+ "guidelines",
378
+ "instructions",
379
+ "command-name",
380
+ "command-message",
381
+ "command-args",
382
+ "local-command-stdout",
383
+ "local-command-stderr",
384
+ "permissions instructions",
385
+ "collaboration_mode",
386
+ "environment_context",
387
+ "auto_compact_summary",
388
+ "user_instructions",
389
+ ];
390
+ /** True if this turn is a platform bootstrap injection (AGENTS.md, pure
391
+ * INSTRUCTIONS preamble, etc.) and should be dropped wholesale rather than
392
+ * partially cleaned. Detected after stripInjectionTags, so we look at what's
393
+ * left after tag-stripping. */
394
+ export function isBootstrapTurn(cleaned, originalLength) {
395
+ if (cleaned.startsWith("# AGENTS.md instructions for"))
396
+ return true;
397
+ // A turn that's mostly an INSTRUCTIONS block (Codex injects this as user role).
398
+ if (originalLength > 4000 && /^<INSTRUCTIONS>/i.test(cleaned))
399
+ return true;
400
+ return false;
401
+ }
402
+ export function stripInjectionTags(text) {
403
+ let out = text;
404
+ for (const tag of INJECTION_TAGS) {
405
+ const escaped = tag.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
406
+ // Case-insensitive: Codex/Trellis injection tags appear as both <INSTRUCTIONS>
407
+ // and <instructions> across platforms.
408
+ out = out.replace(new RegExp(`<${escaped}[^>]*>[\\s\\S]*?</${escaped}>`, "gi"), "");
409
+ }
410
+ out = out.replace(/^# AGENTS\.md instructions for[\s\S]*?(?=\n\n[A-Z一-龥]|$)/m, "");
411
+ return out.replace(/\n{3,}/g, "\n\n").trim();
412
+ }
413
+ /** Find the paragraph-aligned chunk surrounding a hit position. A "chunk" is
414
+ * the contiguous text bounded by the nearest blank-line breaks (`\n\n`) on
415
+ * either side. If the natural paragraph exceeds `maxChars`, fall back to a
416
+ * centered char window — and report the truncation so callers can mark it. */
417
+ export function chunkAround(text, hitIdx, maxChars) {
418
+ const startPara = text.lastIndexOf("\n\n", hitIdx);
419
+ let start = startPara === -1 ? 0 : startPara + 2;
420
+ const endPara = text.indexOf("\n\n", hitIdx);
421
+ let end = endPara === -1 ? text.length : endPara;
422
+ let truncated = false;
423
+ if (end - start > maxChars) {
424
+ start = Math.max(0, hitIdx - Math.floor(maxChars / 2));
425
+ end = Math.min(text.length, hitIdx + Math.ceil(maxChars / 2));
426
+ truncated = true;
427
+ }
428
+ return { start, end, truncated };
429
+ }
430
+ /** Multi-token AND grep over cleaned dialogue. Whitespace-split tokens; a
431
+ * turn matches if every token (case-insensitive) appears anywhere in it.
432
+ * `count` is the total occurrence count across all tokens within matching
433
+ * turns. Excerpts are paragraph-aligned chunks (drawer-style): for each
434
+ * matching turn we collect chunks around every hit position, dedupe by
435
+ * chunk start so adjacent hits inside the same paragraph collapse to one
436
+ * chunk. User-role chunks are listed first (the user's own words anchor
437
+ * topic intent more reliably than AI elaboration). */
438
+ export function searchInDialogue(turns, kw, maxExcerpts = 3, chunkChars = 400) {
439
+ const tokens = kw.toLowerCase().split(/\s+/).filter(Boolean);
440
+ const empty = SearchHitSchema.parse({
441
+ count: 0,
442
+ user_count: 0,
443
+ asst_count: 0,
444
+ total_turns: turns.length,
445
+ excerpts: [],
446
+ });
447
+ if (tokens.length === 0)
448
+ return empty;
449
+ let userCount = 0;
450
+ let asstCount = 0;
451
+ const userExcerpts = [];
452
+ const asstExcerpts = [];
453
+ for (const t of turns) {
454
+ const hay = t.text.toLowerCase();
455
+ if (!tokens.every((tok) => hay.includes(tok)))
456
+ continue;
457
+ // Collect every hit position with the token that produced it (for both
458
+ // counting and rarity-aware chunk anchor selection).
459
+ const hitPositions = [];
460
+ const tokenFreq = new Map();
461
+ let turnHits = 0;
462
+ for (const tok of tokens) {
463
+ let from = 0;
464
+ let n = 0;
465
+ while (true) {
466
+ const idx = hay.indexOf(tok, from);
467
+ if (idx === -1)
468
+ break;
469
+ n++;
470
+ turnHits++;
471
+ hitPositions.push({ idx, tok });
472
+ from = idx + tok.length;
473
+ }
474
+ tokenFreq.set(tok, n);
475
+ }
476
+ if (t.role === "user")
477
+ userCount += turnHits;
478
+ else
479
+ asstCount += turnHits;
480
+ hitPositions.sort((a, b) => a.idx - b.idx);
481
+ const candidates = [];
482
+ const seenStarts = new Set();
483
+ for (const { idx, tok } of hitPositions) {
484
+ const { start, end, truncated } = chunkAround(t.text, idx, chunkChars);
485
+ if (seenStarts.has(start))
486
+ continue;
487
+ seenStarts.add(start);
488
+ const slice = hay.slice(start, end);
489
+ const coverage = tokens.filter((tk) => slice.includes(tk)).length;
490
+ const rarity = 1 / (tokenFreq.get(tok) ?? 1);
491
+ candidates.push({ start, end, truncated, coverage, rarity });
492
+ }
493
+ candidates.sort((a, b) => {
494
+ if (b.coverage !== a.coverage)
495
+ return b.coverage - a.coverage;
496
+ if (b.rarity !== a.rarity)
497
+ return b.rarity - a.rarity;
498
+ return a.start - b.start;
499
+ });
500
+ for (const c of candidates) {
501
+ let snippet = t.text.slice(c.start, c.end).trim();
502
+ if (c.truncated) {
503
+ if (c.start > 0)
504
+ snippet = "…" + snippet;
505
+ if (c.end < t.text.length)
506
+ snippet += "…";
507
+ }
508
+ (t.role === "user" ? userExcerpts : asstExcerpts).push({
509
+ role: t.role,
510
+ snippet,
511
+ });
512
+ }
513
+ }
514
+ const excerpts = [...userExcerpts, ...asstExcerpts].slice(0, maxExcerpts);
515
+ return SearchHitSchema.parse({
516
+ count: userCount + asstCount,
517
+ user_count: userCount,
518
+ asst_count: asstCount,
519
+ total_turns: turns.length,
520
+ excerpts,
521
+ });
522
+ }
523
+ // ---------- claude adapter ----------
524
+ const CLAUDE_PROJECTS = path.join(HOME, ".claude", "projects");
525
+ function claudeProjectDirFromCwd(cwd) {
526
+ // Claude sanitizes path: every '/' and '_' becomes '-'.
527
+ return path.join(CLAUDE_PROJECTS, cwd.replace(/[/_]/g, "-"));
528
+ }
529
+ export function claudeListSessions(f) {
530
+ if (!fs.existsSync(CLAUDE_PROJECTS))
531
+ return [];
532
+ const out = [];
533
+ const projectDirs = f.cwd
534
+ ? [claudeProjectDirFromCwd(f.cwd)].filter((d) => fs.existsSync(d))
535
+ : fs.readdirSync(CLAUDE_PROJECTS).map((d) => path.join(CLAUDE_PROJECTS, d));
536
+ for (const dir of projectDirs) {
537
+ let entries;
538
+ try {
539
+ entries = fs.readdirSync(dir, { withFileTypes: true });
540
+ }
541
+ catch {
542
+ continue;
543
+ }
544
+ const indexFile = path.join(dir, "sessions-index.json");
545
+ const index = readJsonFile(indexFile, ClaudeIndexSchema);
546
+ const indexById = new Map();
547
+ for (const e of index?.entries ?? [])
548
+ indexById.set(e.id, e);
549
+ for (const e of entries) {
550
+ if (!e.isFile() || !e.name.endsWith(".jsonl"))
551
+ continue;
552
+ const filePath = path.join(dir, e.name);
553
+ const id = e.name.replace(/\.jsonl$/, "");
554
+ const idx = indexById.get(id);
555
+ let cwd = idx?.cwd;
556
+ let created = idx?.created;
557
+ const title = idx?.title;
558
+ if (!cwd || !created) {
559
+ const evt = findInJsonl(filePath, ClaudeEventSchema, (o) => typeof o.cwd === "string", 100);
560
+ cwd = cwd ?? evt?.cwd;
561
+ created =
562
+ created ??
563
+ evt?.timestamp ??
564
+ readJsonlFirst(filePath, ClaudeEventSchema)?.timestamp;
565
+ }
566
+ const stat = fs.statSync(filePath);
567
+ const updated = stat.mtime.toISOString();
568
+ // Interval overlap: keep sessions whose lifetime [created, updated]
569
+ // intersects the query window. Cross-day sessions (created before
570
+ // --since but still active inside it) must survive — see PRD
571
+ // 05-08-mem-since-cross-day-filter.
572
+ if (!inRangeOverlap(created, updated, f))
573
+ continue;
574
+ if (f.cwd && cwd && !sameProject(cwd, f.cwd))
575
+ continue;
576
+ out.push(SessionInfoSchema.parse({
577
+ platform: "claude",
578
+ id,
579
+ title,
580
+ cwd,
581
+ created,
582
+ updated,
583
+ filePath,
584
+ }));
585
+ }
586
+ }
587
+ return out;
588
+ }
589
+ export function claudeExtractDialogue(s) {
590
+ // Mirrors session-insight/extract-session.py:
591
+ // - user: type=="user" + role=="user" + content is string (list = tool_result)
592
+ // - assistant: type=="assistant" + role=="assistant", keep only `text` blocks
593
+ // - thinking and tool_use blocks dropped entirely
594
+ // - injection tags stripped
595
+ // Compaction: when we hit a `user` event with isCompactSummary=true, drop all
596
+ // pre-compact turns and replace them with a synthetic [compact summary] turn —
597
+ // the pre-compact content is now redundant with the summary.
598
+ let turns = [];
599
+ readJsonl(s.filePath, ClaudeEventSchema, (obj) => {
600
+ const t = obj.type;
601
+ const msg = obj.message;
602
+ if (!msg)
603
+ return;
604
+ const content = msg.content;
605
+ if (t === "user" && obj.isCompactSummary === true) {
606
+ let summary = "";
607
+ if (typeof content === "string") {
608
+ summary = stripInjectionTags(content);
609
+ }
610
+ else if (Array.isArray(content)) {
611
+ const parts = [];
612
+ for (const block of content) {
613
+ if (block.type === "text" && typeof block.text === "string") {
614
+ const cleaned = stripInjectionTags(block.text);
615
+ if (cleaned)
616
+ parts.push(cleaned);
617
+ }
618
+ }
619
+ summary = parts.join("\n\n");
620
+ }
621
+ turns = summary
622
+ ? [{ role: "user", text: `[compact summary]\n${summary}` }]
623
+ : [];
624
+ return;
625
+ }
626
+ if (t === "user" && msg.role === "user") {
627
+ if (typeof content === "string") {
628
+ const text = stripInjectionTags(content);
629
+ if (text && !isBootstrapTurn(text, content.length)) {
630
+ turns.push({ role: "user", text });
631
+ }
632
+ }
633
+ }
634
+ else if (t === "assistant" &&
635
+ msg.role === "assistant" &&
636
+ Array.isArray(content)) {
637
+ const parts = [];
638
+ for (const block of content) {
639
+ if (block.type === "text" && typeof block.text === "string") {
640
+ const cleaned = stripInjectionTags(block.text);
641
+ if (cleaned)
642
+ parts.push(cleaned);
643
+ }
644
+ }
645
+ if (parts.length)
646
+ turns.push({ role: "assistant", text: parts.join("\n\n") });
647
+ }
648
+ });
649
+ return turns;
650
+ }
651
+ export function claudeSearch(s, kw) {
652
+ return searchInDialogue(claudeExtractDialogue(s), kw);
653
+ }
654
+ /** Find ALL `task.py create|start` invocations in a single Bash command
655
+ * string. A real Bash invocation can contain several (e.g.
656
+ * `SMOKE=$(task.py create …); task.py start "$SMOKE"; …`); the original
657
+ * single-match `parseTaskPyCommand` only saw the first one and silently
658
+ * dropped the rest, breaking pairing in any session that used such patterns.
659
+ *
660
+ * Returned in source order. Each entry's `restRaw` is bounded to the next
661
+ * `task.py` invocation or end-of-line, whichever comes first, so multi-action
662
+ * one-liners are split safely without leaking later args into earlier ones. */
663
+ export function parseTaskPyCommandsAll(cmd) {
664
+ if (typeof cmd !== "string" || cmd.length === 0)
665
+ return [];
666
+ // Find every `task.py (create|start)` occurrence with a left boundary of
667
+ // start-of-string, whitespace, or path separator (forward or backward
668
+ // slash). This rejects flag-value embedding like `--slug=task.py-create-foo`.
669
+ const all = [];
670
+ const findRe = /(^|[\s/\\])task\.py\s+(create|start)(?:\s+|$)/g;
671
+ const matches = [];
672
+ for (const m of cmd.matchAll(findRe)) {
673
+ const action = m[2];
674
+ // bodyStart = right after the matched whitespace following the action verb
675
+ const bodyStart = m.index + m[0].length;
676
+ matches.push({ action, bodyStart });
677
+ }
678
+ for (let i = 0; i < matches.length; i++) {
679
+ const cur = matches[i];
680
+ if (!cur)
681
+ continue;
682
+ const next = matches[i + 1];
683
+ // restRaw stops at the next `task.py` invocation (so we don't claim args
684
+ // from later commands), or end-of-string otherwise. Take only up to the
685
+ // first newline — multi-line scripts have one task.py per line as the
686
+ // dominant pattern.
687
+ const slice = cmd.slice(cur.bodyStart, next?.bodyStart ?? cmd.length);
688
+ const restRaw = (slice.split("\n")[0] ?? "").trim();
689
+ // Reject prose-embedded matches. The pattern is: a bare alphanumeric word
690
+ // followed by another all-letters word with a single space gap — that's
691
+ // English prose like "task.py start exits with hint", not a real
692
+ // invocation (CLI args after the action are typically quoted titles,
693
+ // dashed flags, paths starting with `.` `/` `~` `$`, or followed by shell
694
+ // metacharacters like `2>&1` / `|` / `;`). A real `create my-task`
695
+ // (single bare positional with no trailing English) is kept.
696
+ if (/^[A-Za-z][A-Za-z0-9_-]*\s+[A-Za-z]{2,}\b/.test(restRaw))
697
+ continue;
698
+ const parsed = parseRestOfTaskPyCommand(cur.action, restRaw);
699
+ // Drop entries with no extractable info — likely prose with quote-like
700
+ // punctuation but no real arg.
701
+ if (cur.action === "create" &&
702
+ parsed.action === "create" &&
703
+ !parsed.slug &&
704
+ !parsed.titleArg)
705
+ continue;
706
+ if (cur.action === "start" && parsed.action === "start" && !parsed.taskDir)
707
+ continue;
708
+ all.push(parsed);
709
+ }
710
+ return all;
711
+ }
712
+ /** Single-result wrapper for backwards compatibility (returns the first
713
+ * occurrence, or null if none). Existing tests that assume single-match
714
+ * semantics still pass via this helper; new code should call
715
+ * `parseTaskPyCommandsAll`. */
716
+ export function parseTaskPyCommand(cmd) {
717
+ const all = parseTaskPyCommandsAll(cmd);
718
+ return all[0] ?? null;
719
+ }
720
+ function parseRestOfTaskPyCommand(action, restRaw) {
721
+ if (action === "create") {
722
+ const args = splitShellArgs(restRaw);
723
+ // First positional arg (skip any flags). For `task.py create`, the title
724
+ // is typically the first quoted positional; --slug FOO appears as a flag.
725
+ let slug;
726
+ let titleArg;
727
+ for (let i = 0; i < args.length; i++) {
728
+ const a = args[i];
729
+ if (a === undefined)
730
+ continue;
731
+ if (a === "--slug" || a === "-s") {
732
+ slug = args[i + 1];
733
+ i++;
734
+ continue;
735
+ }
736
+ if (a.startsWith("--slug=")) {
737
+ slug = a.slice("--slug=".length);
738
+ continue;
739
+ }
740
+ if (a.startsWith("-"))
741
+ continue;
742
+ titleArg ??= a;
743
+ }
744
+ return { action: "create", slug, titleArg };
745
+ }
746
+ // start
747
+ const args = splitShellArgs(restRaw);
748
+ let taskDir;
749
+ for (const a of args) {
750
+ if (a.startsWith("-"))
751
+ continue;
752
+ taskDir = a;
753
+ break;
754
+ }
755
+ return { action: "start", taskDir };
756
+ }
757
+ /** Best-effort shell-arg splitter: respects `"…"` and `'…'` quoting, splits on
758
+ * whitespace, treats shell metacharacters `;`, `|`, `&`, `(`, `)`, `>` as
759
+ * **token boundaries** (so `$(...)` substitution boundaries, command chains,
760
+ * and redirects don't leak into the next positional arg). Also strips any
761
+ * trailing shell-meta cruft from individual tokens — e.g. a `--slug` value
762
+ * captured inside `$(... --slug FOO)` gets the closing `)` lopped off.
763
+ * Sufficient for parsing slugs/paths out of `task.py create|start`
764
+ * invocations; not a full POSIX parser. */
765
+ function splitShellArgs(s) {
766
+ const out = [];
767
+ let cur = "";
768
+ let quote = null;
769
+ const flush = () => {
770
+ if (!cur)
771
+ return;
772
+ // Strip trailing shell metas that snuck in from $(...) substitution edges,
773
+ // command chains, redirects, etc. Keep leading chars (paths may start with
774
+ // `.` or `/`).
775
+ const cleaned = cur.replace(/[)};&|>]+$/, "");
776
+ if (cleaned)
777
+ out.push(cleaned);
778
+ cur = "";
779
+ };
780
+ for (const ch of s) {
781
+ if (quote) {
782
+ if (ch === quote) {
783
+ quote = null;
784
+ continue;
785
+ }
786
+ cur += ch;
787
+ continue;
788
+ }
789
+ if (ch === '"' || ch === "'") {
790
+ quote = ch;
791
+ continue;
792
+ }
793
+ if (/\s/.test(ch)) {
794
+ flush();
795
+ continue;
796
+ }
797
+ // Hard token boundaries — these never belong inside a slug or path arg.
798
+ // Drop them (don't keep as standalone token; the caller never wants them).
799
+ if (ch === ";" || ch === "|" || ch === "&" || ch === "(" || ch === ")") {
800
+ flush();
801
+ continue;
802
+ }
803
+ cur += ch;
804
+ }
805
+ flush();
806
+ return out;
807
+ }
808
+ /** Derive a slug from a `start` task-dir path like
809
+ * `.trellis/tasks/05-08-mem-phase-slice/` → `mem-phase-slice` (the
810
+ * `MM-DD-` date prefix is stripped so this matches the slug supplied via
811
+ * `--slug` on the corresponding `task.py create` invocation). */
812
+ function slugFromTaskDir(p) {
813
+ if (!p)
814
+ return undefined;
815
+ // Normalize separators and trim trailing slash + shell metas leaked from
816
+ // `$(...)` substitution / heredoc edges.
817
+ const norm = p.replace(/\\+/g, "/").replace(/\/+$/g, "");
818
+ const parts = norm.split("/").filter(Boolean);
819
+ const last = parts[parts.length - 1];
820
+ if (last === undefined)
821
+ return undefined;
822
+ // Strip leading `MM-DD-` (e.g. `05-08-`) added by task.py.
823
+ return last.replace(/^\d{2}-\d{2}-/, "");
824
+ }
825
+ /**
826
+ * Single-pass scan of a Claude JSONL file that produces both:
827
+ * 1. the cleaned dialogue turns (semantically identical to
828
+ * `claudeExtractDialogue`)
829
+ * 2. the list of `task.py create|start` Bash tool_use events with their
830
+ * `turnIndex` (= turns.length AT THE TIME the tool_use was seen).
831
+ *
832
+ * Why one pass: we need the turnIndex to align with `claudeExtractDialogue`'s
833
+ * output exactly, including compaction-reset behavior. A second pass would
834
+ * have to re-derive turn indices from timestamps, which is fragile when
835
+ * timestamps repeat or are missing.
836
+ *
837
+ * For non-Claude platforms this returns turns + an empty event list; callers
838
+ * are expected to handle Codex/OpenCode boundary detection separately (or
839
+ * gracefully degrade — see PRD MVP scope).
840
+ */
841
+ export function collectClaudeTurnsAndEvents(s) {
842
+ let turns = [];
843
+ let events = [];
844
+ readJsonl(s.filePath, ClaudeEventSchema, (obj) => {
845
+ const t = obj.type;
846
+ const msg = obj.message;
847
+ if (!msg)
848
+ return;
849
+ const content = msg.content;
850
+ if (t === "user" && obj.isCompactSummary === true) {
851
+ let summary = "";
852
+ if (typeof content === "string") {
853
+ summary = stripInjectionTags(content);
854
+ }
855
+ else if (Array.isArray(content)) {
856
+ const parts = [];
857
+ for (const block of content) {
858
+ if (block.type === "text" && typeof block.text === "string") {
859
+ const cleaned = stripInjectionTags(block.text);
860
+ if (cleaned)
861
+ parts.push(cleaned);
862
+ }
863
+ }
864
+ summary = parts.join("\n\n");
865
+ }
866
+ turns = summary
867
+ ? [{ role: "user", text: `[compact summary]\n${summary}` }]
868
+ : [];
869
+ // Reset events too: pre-compact task.py events anchor to turnIndex
870
+ // values that no longer correspond to real turns (the underlying
871
+ // dialogue is collapsed into a single synthetic [compact summary]).
872
+ // Pairing pre-compact events to post-compact turns would produce
873
+ // incoherent windows.
874
+ events = [];
875
+ return;
876
+ }
877
+ if (t === "user" && msg.role === "user") {
878
+ if (typeof content === "string") {
879
+ const text = stripInjectionTags(content);
880
+ if (text && !isBootstrapTurn(text, content.length)) {
881
+ turns.push({ role: "user", text });
882
+ }
883
+ }
884
+ return;
885
+ }
886
+ if (t === "assistant" &&
887
+ msg.role === "assistant" &&
888
+ Array.isArray(content)) {
889
+ // Walk blocks: text blocks contribute to the eventual cleaned turn;
890
+ // tool_use blocks with name="Bash" are scanned for task.py invocations.
891
+ const parts = [];
892
+ for (const block of content) {
893
+ if (block.type === "text" && typeof block.text === "string") {
894
+ const cleaned = stripInjectionTags(block.text);
895
+ if (cleaned)
896
+ parts.push(cleaned);
897
+ }
898
+ else if (block.type === "tool_use") {
899
+ // Schema is loose so we read fields off the block directly.
900
+ const b = block;
901
+ if (b.name !== "Bash")
902
+ continue;
903
+ const inp = b.input;
904
+ if (!inp || typeof inp !== "object")
905
+ continue;
906
+ const command = inp.command;
907
+ if (typeof command !== "string")
908
+ continue;
909
+ // A Bash command may invoke task.py multiple times (e.g.
910
+ // `SMOKE=$(task.py create …); task.py start "$SMOKE"`). Capture
911
+ // every occurrence — the original single-match version dropped
912
+ // the second invocation and produced unpaired windows.
913
+ const parsedAll = parseTaskPyCommandsAll(command);
914
+ for (const parsed of parsedAll) {
915
+ // turnIndex = current turns.length (the index this assistant turn
916
+ // WILL occupy if its text parts are non-empty; either way, it's
917
+ // the cut point for "everything before this Bash event"). For
918
+ // assistant messages where text comes BEFORE tool_use blocks, the
919
+ // assistant turn is appended AFTER this loop completes, so using
920
+ // turns.length here means the boundary lies just before that turn.
921
+ // We accept this small drift: brainstorm slicing is at granularity
922
+ // of full turns, not intra-turn substrings.
923
+ const ev = {
924
+ action: parsed.action,
925
+ timestamp: obj.timestamp ?? "",
926
+ turnIndex: turns.length,
927
+ ...(parsed.action === "create"
928
+ ? { slug: parsed.slug }
929
+ : { taskDir: parsed.taskDir }),
930
+ };
931
+ events.push(ev);
932
+ }
933
+ }
934
+ }
935
+ if (parts.length)
936
+ turns.push({ role: "assistant", text: parts.join("\n\n") });
937
+ }
938
+ });
939
+ return { turns, events };
940
+ }
941
+ /**
942
+ * Pair `create` → `start` events into brainstorm windows.
943
+ *
944
+ * Pairing strategy:
945
+ * 1. Walk events in order.
946
+ * 2. For each `create`, find the next unmatched `start` whose slug matches
947
+ * (slug derived from `start` taskDir's last path segment) — slug match
948
+ * wins regardless of position.
949
+ * 3. If no slug match: pair with the next unmatched `start` by position
950
+ * (FIFO).
951
+ * 4. Unmatched `create` (no following `start`): window = [create, totalTurns).
952
+ * 5. Unmatched `start` (no preceding `create`): window = [0, start).
953
+ *
954
+ * Window labels: `<slug>` if known, else `window-N`.
955
+ */
956
+ export function buildBrainstormWindows(events, totalTurns) {
957
+ const creates = events
958
+ .map((e, i) => ({ e, i }))
959
+ .filter(({ e }) => e.action === "create");
960
+ const starts = events
961
+ .map((e, i) => ({ e, i }))
962
+ .filter(({ e }) => e.action === "start");
963
+ const usedStartIdx = new Set();
964
+ const windows = [];
965
+ let windowCounter = 0;
966
+ const usedCreateIdx = new Set();
967
+ // Pass 1: pair by slug match (slug present on the `create`, matches the
968
+ // last segment of the `start` taskDir). Slug match wins over position.
969
+ for (const { e: createEv, i: ci } of creates) {
970
+ if (!createEv.slug)
971
+ continue;
972
+ const matchIdx = starts.findIndex(({ e, i }) => !usedStartIdx.has(i) && slugFromTaskDir(e.taskDir) === createEv.slug);
973
+ if (matchIdx === -1)
974
+ continue;
975
+ const startEntry = starts[matchIdx];
976
+ if (!startEntry)
977
+ continue;
978
+ usedStartIdx.add(startEntry.i);
979
+ usedCreateIdx.add(ci);
980
+ pushWindow(windows, createEv.turnIndex, startEntry.e.turnIndex, createEv.slug, ++windowCounter);
981
+ }
982
+ // Pass 2: FIFO pair remaining creates with remaining starts that appear
983
+ // AFTER the create (by event order).
984
+ for (const { e: createEv, i: ci } of creates) {
985
+ if (usedCreateIdx.has(ci))
986
+ continue;
987
+ const pairedStart = starts.find(({ i }) => !usedStartIdx.has(i) && i > ci);
988
+ if (pairedStart) {
989
+ usedStartIdx.add(pairedStart.i);
990
+ usedCreateIdx.add(ci);
991
+ const slug = createEv.slug ?? slugFromTaskDir(pairedStart.e.taskDir);
992
+ pushWindow(windows, createEv.turnIndex, pairedStart.e.turnIndex, slug, ++windowCounter);
993
+ }
994
+ else {
995
+ // Fallback A: create with no start → [create, end).
996
+ usedCreateIdx.add(ci);
997
+ pushWindow(windows, createEv.turnIndex, totalTurns, createEv.slug, ++windowCounter);
998
+ }
999
+ }
1000
+ // Pass 3: unmatched starts (start with no preceding create) → [0, start).
1001
+ // Fallback B: task was created in an earlier session.
1002
+ for (const { e: startEv, i } of starts) {
1003
+ if (usedStartIdx.has(i))
1004
+ continue;
1005
+ pushWindow(windows, 0, startEv.turnIndex, slugFromTaskDir(startEv.taskDir), ++windowCounter);
1006
+ }
1007
+ // Sort windows by startTurn for stable output ordering.
1008
+ windows.sort((a, b) => a.startTurn - b.startTurn);
1009
+ return windows;
1010
+ }
1011
+ function pushWindow(windows, startTurn, endTurn, slug, counter) {
1012
+ // Guard: if start > end (e.g., start before create due to event interleave),
1013
+ // skip the malformed window rather than emit an empty / negative slice.
1014
+ if (endTurn < startTurn)
1015
+ return;
1016
+ windows.push({
1017
+ label: slug ?? `window-${counter}`,
1018
+ startTurn,
1019
+ endTurn,
1020
+ });
1021
+ }
1022
+ // ---------- codex adapter ----------
1023
+ const CODEX_SESSIONS = path.join(HOME, ".codex", "sessions");
1024
+ function* walkDir(root) {
1025
+ if (!fs.existsSync(root))
1026
+ return;
1027
+ const stack = [root];
1028
+ while (stack.length) {
1029
+ const cur = stack.pop();
1030
+ if (cur === undefined)
1031
+ break;
1032
+ let entries;
1033
+ try {
1034
+ entries = fs.readdirSync(cur, { withFileTypes: true });
1035
+ }
1036
+ catch {
1037
+ continue;
1038
+ }
1039
+ for (const e of entries) {
1040
+ const p = path.join(cur, e.name);
1041
+ if (e.isDirectory())
1042
+ stack.push(p);
1043
+ else if (e.isFile())
1044
+ yield p;
1045
+ }
1046
+ }
1047
+ }
1048
+ export function codexListSessions(f) {
1049
+ if (!fs.existsSync(CODEX_SESSIONS))
1050
+ return [];
1051
+ const out = [];
1052
+ for (const file of walkDir(CODEX_SESSIONS)) {
1053
+ if (!file.endsWith(".jsonl"))
1054
+ continue;
1055
+ const base = path.basename(file, ".jsonl");
1056
+ const m = base.match(/^rollout-(\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2})-(.+)$/);
1057
+ const tsFromName = m?.[1]
1058
+ ? new Date(m[1].replace(/T(\d{2})-(\d{2})-(\d{2})/, "T$1:$2:$3") + "Z").toISOString()
1059
+ : undefined;
1060
+ // Note: we previously short-circuited on `!inRange(tsFromName, f)` here,
1061
+ // but the filename ts is the session's creation time — a cross-day session
1062
+ // that started before --since but was active inside it would be dropped.
1063
+ // Filter at the same place as claude/opencode using interval overlap.
1064
+ const first = readJsonlFirst(file, CodexEventSchema);
1065
+ const meta = first?.payload;
1066
+ const id = meta?.id ?? m?.[2] ?? base;
1067
+ const cwd = meta?.cwd;
1068
+ const created = first?.timestamp ?? tsFromName ?? "";
1069
+ if (f.cwd && !sameProject(cwd, f.cwd))
1070
+ continue;
1071
+ const updated = fs.statSync(file).mtime.toISOString();
1072
+ if (!inRangeOverlap(created, updated, f))
1073
+ continue;
1074
+ out.push(SessionInfoSchema.parse({
1075
+ platform: "codex",
1076
+ id,
1077
+ cwd,
1078
+ created,
1079
+ updated,
1080
+ filePath: file,
1081
+ }));
1082
+ }
1083
+ return out;
1084
+ }
1085
+ export function codexExtractDialogue(s) {
1086
+ // Codex events: payload.type=="message" with role in {user, assistant, developer, system}.
1087
+ // Keep user/assistant only. Each content part is {type: "input_text"|"output_text", text}.
1088
+ // Codex inlines a lot of system prompt as the first user message (AGENTS.md, permission
1089
+ // blocks, etc.) — stripInjectionTags removes the bulk; turns that are pure boilerplate
1090
+ // collapse to empty after strip and get dropped here.
1091
+ // Compaction: a top-level event with type=="compacted" carries a payload.replacement_history
1092
+ // array — the new authoritative history replacing everything before. We reset turns and
1093
+ // re-seed from replacement_history.
1094
+ let turns = [];
1095
+ const buildTurnFromMessage = (role, parts) => {
1096
+ const collected = [];
1097
+ let totalRaw = 0;
1098
+ for (const c of parts ?? []) {
1099
+ const txt = c.text;
1100
+ if (typeof txt !== "string")
1101
+ continue;
1102
+ if (c.type !== "input_text" && c.type !== "output_text")
1103
+ continue;
1104
+ totalRaw += txt.length;
1105
+ const cleaned = stripInjectionTags(txt);
1106
+ if (cleaned)
1107
+ collected.push(cleaned);
1108
+ }
1109
+ if (!collected.length)
1110
+ return null;
1111
+ const merged = collected.join("\n\n");
1112
+ if (isBootstrapTurn(merged, totalRaw))
1113
+ return null;
1114
+ return { role, text: merged };
1115
+ };
1116
+ readJsonl(s.filePath, CodexEventSchema, (obj) => {
1117
+ if (obj.type === "compacted") {
1118
+ const rh = obj.payload?.replacement_history;
1119
+ turns = [];
1120
+ if (!Array.isArray(rh))
1121
+ return;
1122
+ for (const item of rh) {
1123
+ if (item.type !== "message")
1124
+ continue;
1125
+ const r = DialogueRoleSchema.safeParse(item.role);
1126
+ if (!r.success)
1127
+ continue;
1128
+ const turn = buildTurnFromMessage(r.data, item.content);
1129
+ if (turn)
1130
+ turns.push({ role: turn.role, text: `[compact]\n${turn.text}` });
1131
+ }
1132
+ return;
1133
+ }
1134
+ const p = obj.payload;
1135
+ if (p?.type !== "message")
1136
+ return;
1137
+ const roleParsed = DialogueRoleSchema.safeParse(p.role);
1138
+ if (!roleParsed.success)
1139
+ return;
1140
+ const turn = buildTurnFromMessage(roleParsed.data, p.content);
1141
+ if (turn)
1142
+ turns.push(turn);
1143
+ });
1144
+ return turns;
1145
+ }
1146
+ export function codexSearch(s, kw) {
1147
+ return searchInDialogue(codexExtractDialogue(s), kw);
1148
+ }
1149
+ /** Codex twin of `collectClaudeTurnsAndEvents`. Single pass over the rollout
1150
+ * file; emits both the cleaned dialogue turns (semantically identical to
1151
+ * `codexExtractDialogue`) AND the list of `task.py create|start` invocations
1152
+ * found inside `function_call` events whose `name === "exec_command"` (Codex's
1153
+ * stable shell tool). Compaction resets both turns AND events for the same
1154
+ * reason as the Claude collector — pre-compact event indices stop pointing at
1155
+ * real turns once history is replaced. */
1156
+ export function collectCodexTurnsAndEvents(s) {
1157
+ let turns = [];
1158
+ let events = [];
1159
+ const buildTurnFromMessage = (role, parts) => {
1160
+ const collected = [];
1161
+ let totalRaw = 0;
1162
+ for (const c of parts ?? []) {
1163
+ const txt = c.text;
1164
+ if (typeof txt !== "string")
1165
+ continue;
1166
+ if (c.type !== "input_text" && c.type !== "output_text")
1167
+ continue;
1168
+ totalRaw += txt.length;
1169
+ const cleaned = stripInjectionTags(txt);
1170
+ if (cleaned)
1171
+ collected.push(cleaned);
1172
+ }
1173
+ if (!collected.length)
1174
+ return null;
1175
+ const merged = collected.join("\n\n");
1176
+ if (isBootstrapTurn(merged, totalRaw))
1177
+ return null;
1178
+ return { role, text: merged };
1179
+ };
1180
+ readJsonl(s.filePath, CodexEventSchema, (obj) => {
1181
+ if (obj.type === "compacted") {
1182
+ const rh = obj.payload?.replacement_history;
1183
+ turns = [];
1184
+ events = [];
1185
+ if (!Array.isArray(rh))
1186
+ return;
1187
+ for (const item of rh) {
1188
+ if (item.type !== "message")
1189
+ continue;
1190
+ const r = DialogueRoleSchema.safeParse(item.role);
1191
+ if (!r.success)
1192
+ continue;
1193
+ const turn = buildTurnFromMessage(r.data, item.content);
1194
+ if (turn)
1195
+ turns.push({ role: turn.role, text: `[compact]\n${turn.text}` });
1196
+ }
1197
+ return;
1198
+ }
1199
+ const p = obj.payload;
1200
+ if (!p)
1201
+ return;
1202
+ // Function-call events (Codex's shell tool dispatch). The schema is loose
1203
+ // so we read fields off the raw payload.
1204
+ if (p.type === "function_call") {
1205
+ const fnName = p.name;
1206
+ if (fnName !== "exec_command" && fnName !== "shell")
1207
+ return;
1208
+ const argsRaw = p.arguments;
1209
+ let cmd;
1210
+ if (typeof argsRaw === "string") {
1211
+ try {
1212
+ const parsed = JSON.parse(argsRaw);
1213
+ if (parsed && typeof parsed === "object") {
1214
+ const c = parsed.cmd;
1215
+ if (typeof c === "string")
1216
+ cmd = c;
1217
+ else {
1218
+ const c2 = parsed.command;
1219
+ if (typeof c2 === "string")
1220
+ cmd = c2;
1221
+ }
1222
+ }
1223
+ }
1224
+ catch {
1225
+ // arguments not JSON (some Codex versions inline a string) — try as
1226
+ // raw shell.
1227
+ cmd = argsRaw;
1228
+ }
1229
+ }
1230
+ if (!cmd)
1231
+ return;
1232
+ const parsedAll = parseTaskPyCommandsAll(cmd);
1233
+ for (const parsed of parsedAll) {
1234
+ const ev = {
1235
+ action: parsed.action,
1236
+ timestamp: obj.timestamp ?? "",
1237
+ turnIndex: turns.length,
1238
+ ...(parsed.action === "create"
1239
+ ? { slug: parsed.slug }
1240
+ : { taskDir: parsed.taskDir }),
1241
+ };
1242
+ events.push(ev);
1243
+ }
1244
+ return;
1245
+ }
1246
+ // Real conversational turn.
1247
+ if (p.type !== "message")
1248
+ return;
1249
+ const roleParsed = DialogueRoleSchema.safeParse(p.role);
1250
+ if (!roleParsed.success)
1251
+ return;
1252
+ const turn = buildTurnFromMessage(roleParsed.data, p.content);
1253
+ if (turn)
1254
+ turns.push(turn);
1255
+ });
1256
+ return { turns, events };
1257
+ }
1258
+ // ---------- opencode adapter (temporarily unavailable) ----------
1259
+ //
1260
+ // OpenCode 1.2+ migrated to a SQLite database at
1261
+ // ~/.local/share/opencode/opencode.db. The previous SQLite reader required
1262
+ // `better-sqlite3` (a native dep). In 0.6.0-beta.4 we reverted that dep
1263
+ // because its prebuilt-tarball download from GitHub Releases was unreliable
1264
+ // in some networks (notably Windows + China), and the source-build fallback
1265
+ // requires a C compiler that most users don't have — `npm install` was
1266
+ // failing for the entire CLI, not just the OpenCode reader.
1267
+ //
1268
+ // The three exported adapter functions are kept (callers in dispatch /
1269
+ // slicePhase rely on them) but degraded to no-ops with a one-shot stderr
1270
+ // warning. Re-enabled in a future release once a non-native fallback ships.
1271
+ let opencodeWarned = false;
1272
+ function warnOpencodeUnavailable() {
1273
+ if (opencodeWarned)
1274
+ return;
1275
+ opencodeWarned = true;
1276
+ process.stderr.write("⚠️ tl mem: OpenCode platform reader is temporarily unavailable in this build.\n" +
1277
+ " OpenCode 1.2+ moved to SQLite; the native dependency was reverted in\n" +
1278
+ " 0.6.0-beta.4 due to install failures. Re-enabled in a future release.\n");
1279
+ }
1280
+ export function opencodeListSessions(_f) {
1281
+ warnOpencodeUnavailable();
1282
+ return [];
1283
+ }
1284
+ export function opencodeExtractDialogue(_s) {
1285
+ warnOpencodeUnavailable();
1286
+ return [];
1287
+ }
1288
+ function opencodeSearch(_s, kw) {
1289
+ warnOpencodeUnavailable();
1290
+ return searchInDialogue([], kw);
1291
+ }
1292
+ // ---------- dispatch ----------
1293
+ function listAll(f) {
1294
+ const all = [];
1295
+ if (f.platform === "all" || f.platform === "claude")
1296
+ all.push(...claudeListSessions(f));
1297
+ if (f.platform === "all" || f.platform === "codex")
1298
+ all.push(...codexListSessions(f));
1299
+ if (f.platform === "all" || f.platform === "opencode")
1300
+ all.push(...opencodeListSessions(f));
1301
+ all.sort((a, b) => (b.updated ?? b.created ?? "").localeCompare(a.updated ?? a.created ?? ""));
1302
+ return all.slice(0, f.limit);
1303
+ }
1304
+ function extractDialogue(s) {
1305
+ switch (s.platform) {
1306
+ case "claude":
1307
+ return claudeExtractDialogue(s);
1308
+ case "codex":
1309
+ return codexExtractDialogue(s);
1310
+ case "opencode":
1311
+ return opencodeExtractDialogue(s);
1312
+ }
1313
+ }
1314
+ function searchSession(s, kw) {
1315
+ switch (s.platform) {
1316
+ case "claude":
1317
+ return claudeSearch(s, kw);
1318
+ case "codex":
1319
+ return codexSearch(s, kw);
1320
+ case "opencode":
1321
+ return opencodeSearch(s, kw);
1322
+ }
1323
+ }
1324
+ /** Build parent → descendants index for OpenCode (transitively flattened).
1325
+ * Other platforms have no native parent_id so they pass through unchanged. */
1326
+ function buildChildIndex(sessions) {
1327
+ const directChildren = new Map();
1328
+ for (const s of sessions) {
1329
+ if (!s.parent_id)
1330
+ continue;
1331
+ const list = directChildren.get(s.parent_id) ?? [];
1332
+ list.push(s);
1333
+ directChildren.set(s.parent_id, list);
1334
+ }
1335
+ // Transitive flatten: each parent maps to *all* descendants.
1336
+ const out = new Map();
1337
+ for (const [pid] of directChildren) {
1338
+ const stack = [...(directChildren.get(pid) ?? [])];
1339
+ const flat = [];
1340
+ while (stack.length) {
1341
+ const cur = stack.pop();
1342
+ if (cur === undefined)
1343
+ break;
1344
+ flat.push(cur);
1345
+ for (const c of directChildren.get(cur.id) ?? [])
1346
+ stack.push(c);
1347
+ }
1348
+ out.set(pid, flat);
1349
+ }
1350
+ return out;
1351
+ }
1352
+ function searchSessionWithChildren(s, kw, childIndex) {
1353
+ const children = childIndex.get(s.id) ?? [];
1354
+ if (children.length === 0)
1355
+ return searchSession(s, kw);
1356
+ // Concatenate parent + descendants' cleaned dialogue, then run a single
1357
+ // search over the merged turn list. This way scores reflect total topic
1358
+ // density across the sub-agent tree.
1359
+ const merged = [...extractDialogue(s)];
1360
+ for (const c of children)
1361
+ merged.push(...extractDialogue(c));
1362
+ return searchInDialogue(merged, kw);
1363
+ }
1364
+ function findSessionById(id, f) {
1365
+ const wide = { ...f, cwd: undefined, limit: 1_000_000 };
1366
+ const all = listAll(wide);
1367
+ return all.find((s) => s.id === id) ?? all.find((s) => s.id.startsWith(id));
1368
+ }
1369
+ // ---------- formatting ----------
1370
+ export function shortDate(iso) {
1371
+ if (!iso)
1372
+ return " ";
1373
+ return iso.slice(0, 16).replace("T", " ");
1374
+ }
1375
+ export function shortPath(p) {
1376
+ if (!p)
1377
+ return "(no cwd)";
1378
+ return p.replace(HOME, "~");
1379
+ }
1380
+ function printSessions(rows) {
1381
+ if (rows.length === 0) {
1382
+ console.log("(no sessions)");
1383
+ return;
1384
+ }
1385
+ for (const s of rows) {
1386
+ const id = s.id.length > 12 ? s.id.slice(0, 12) : s.id.padEnd(12);
1387
+ const parentTag = s.parent_id
1388
+ ? ` ↳ child of ${s.parent_id.slice(0, 12)}`
1389
+ : "";
1390
+ console.log(`[${s.platform.padEnd(8)}] ${shortDate(s.updated ?? s.created)} ${id} ${shortPath(s.cwd)}` +
1391
+ (s.title ? ` — ${s.title}` : "") +
1392
+ parentTag);
1393
+ }
1394
+ }
1395
+ // ---------- commands ----------
1396
+ function cmdList(argv) {
1397
+ const f = buildFilter(argv.flags);
1398
+ const rows = listAll(f);
1399
+ if (argv.flags.json) {
1400
+ console.log(JSON.stringify(rows, null, 2));
1401
+ return;
1402
+ }
1403
+ console.log(`scope: ${f.cwd ? `project=${shortPath(f.cwd)}` : "global"} platform=${f.platform}` +
1404
+ (f.since ? ` since=${f.since.toISOString().slice(0, 10)}` : "") +
1405
+ (f.until ? ` until=${f.until.toISOString().slice(0, 10)}` : ""));
1406
+ printSessions(rows);
1407
+ console.log(`\n${rows.length} session(s)`);
1408
+ }
1409
+ function cmdSearch(argv) {
1410
+ const kw = argv.positional[0];
1411
+ if (!kw)
1412
+ die("usage: search <keyword>");
1413
+ const f = buildFilter(argv.flags);
1414
+ const wide = { ...f, limit: 1_000_000 };
1415
+ const candidates = listAll(wide);
1416
+ const includeChildren = argv.flags["include-children"] === true;
1417
+ // When --include-children is set: search over the merged dialogue of each
1418
+ // session plus its descendants (only OpenCode populates parent_id natively).
1419
+ // Children whose parent is also in the candidate set are dropped from the
1420
+ // result list — they get absorbed into the parent's hit.
1421
+ const childIndex = includeChildren ? buildChildIndex(candidates) : new Map();
1422
+ const candidateIds = new Set(candidates.map((s) => s.id));
1423
+ const isAbsorbedChild = (s) => includeChildren &&
1424
+ s.parent_id !== undefined &&
1425
+ candidateIds.has(s.parent_id);
1426
+ const matches = [];
1427
+ for (const s of candidates) {
1428
+ if (isAbsorbedChild(s))
1429
+ continue;
1430
+ const hit = includeChildren
1431
+ ? searchSessionWithChildren(s, kw, childIndex)
1432
+ : searchSession(s, kw);
1433
+ if (hit.count === 0)
1434
+ continue;
1435
+ matches.push({ s, hit, descendants: childIndex.get(s.id)?.length ?? 0 });
1436
+ }
1437
+ // Rank by weighted-density relevance score: user hits matter ×3, normalized
1438
+ // by total dialogue length so a tight 18-hit short session beats a sprawling
1439
+ // 58-hit long one. Tie-break on raw count, then recency.
1440
+ matches.sort((a, b) => {
1441
+ const sa = relevanceScore(a.hit);
1442
+ const sb = relevanceScore(b.hit);
1443
+ if (sb !== sa)
1444
+ return sb - sa;
1445
+ if (b.hit.count !== a.hit.count)
1446
+ return b.hit.count - a.hit.count;
1447
+ return (b.s.updated ?? b.s.created ?? "").localeCompare(a.s.updated ?? a.s.created ?? "");
1448
+ });
1449
+ const top = matches.slice(0, f.limit);
1450
+ if (argv.flags.json) {
1451
+ console.log(JSON.stringify(top.map(({ s, hit, descendants }) => ({
1452
+ session: s,
1453
+ score: Number(relevanceScore(hit).toFixed(4)),
1454
+ hit_count: hit.count,
1455
+ user_count: hit.user_count,
1456
+ asst_count: hit.asst_count,
1457
+ total_turns: hit.total_turns,
1458
+ descendants_merged: includeChildren ? descendants : 0,
1459
+ excerpts: hit.excerpts,
1460
+ })), null, 2));
1461
+ return;
1462
+ }
1463
+ console.log(`scope: ${f.cwd ? `project=${shortPath(f.cwd)}` : "global"} keyword="${kw}" platform=${f.platform}` +
1464
+ (includeChildren ? ` include-children=on` : ""));
1465
+ if (top.length === 0) {
1466
+ console.log("(no matches)");
1467
+ return;
1468
+ }
1469
+ for (const { s, hit, descendants } of top) {
1470
+ const idShort = s.id.slice(0, 12);
1471
+ const score = relevanceScore(hit).toFixed(3);
1472
+ const childTag = includeChildren && descendants > 0 ? ` +${descendants} child` : "";
1473
+ console.log(`\n[${s.platform.padEnd(8)}] ${shortDate(s.updated ?? s.created)} ${idShort} ${shortPath(s.cwd)}` +
1474
+ ` score=${score} hits=${hit.count} (u=${hit.user_count},a=${hit.asst_count}) turns=${hit.total_turns}${childTag}` +
1475
+ (s.title ? ` — ${s.title}` : ""));
1476
+ for (const ex of hit.excerpts) {
1477
+ console.log(` [${ex.role}] ${ex.snippet}`);
1478
+ }
1479
+ }
1480
+ console.log(`\n${top.length} session(s)${matches.length > top.length ? ` (of ${matches.length})` : ""}`);
1481
+ }
1482
+ function cmdProjects(argv) {
1483
+ // List distinct cwds across all platforms with last-active timestamp + per-platform
1484
+ // session counts. Designed for AI consumption: AI calls this first to learn which
1485
+ // "门牌号" (project paths) have recent activity, then picks one for `--cwd` in
1486
+ // a follow-up `search`.
1487
+ const f = buildFilter({ ...argv.flags, global: true });
1488
+ const wide = { ...f, cwd: undefined, limit: 1_000_000 };
1489
+ const all = listAll(wide);
1490
+ const byCwd = new Map();
1491
+ for (const s of all) {
1492
+ if (!s.cwd)
1493
+ continue;
1494
+ const ts = s.updated ?? s.created ?? "";
1495
+ let agg = byCwd.get(s.cwd);
1496
+ if (!agg) {
1497
+ agg = {
1498
+ cwd: s.cwd,
1499
+ last_active: ts,
1500
+ sessions: 0,
1501
+ by_platform: { claude: 0, codex: 0, opencode: 0 },
1502
+ };
1503
+ byCwd.set(s.cwd, agg);
1504
+ }
1505
+ agg.sessions++;
1506
+ agg.by_platform[s.platform]++;
1507
+ if (ts > agg.last_active)
1508
+ agg.last_active = ts;
1509
+ }
1510
+ const rows = [...byCwd.values()].sort((a, b) => b.last_active.localeCompare(a.last_active));
1511
+ const limit = typeof argv.flags.limit === "string" ? Number(argv.flags.limit) : 30;
1512
+ const top = rows.slice(0, limit);
1513
+ if (argv.flags.json) {
1514
+ console.log(JSON.stringify(top, null, 2));
1515
+ return;
1516
+ }
1517
+ console.log(`active projects` +
1518
+ (f.since ? ` since=${f.since.toISOString().slice(0, 10)}` : "") +
1519
+ (f.until ? ` until=${f.until.toISOString().slice(0, 10)}` : ""));
1520
+ if (top.length === 0) {
1521
+ console.log("(none)");
1522
+ return;
1523
+ }
1524
+ for (const r of top) {
1525
+ const parts = Object.entries(r.by_platform)
1526
+ .filter(([, n]) => n > 0)
1527
+ .map(([p, n]) => `${p}:${n}`)
1528
+ .join(" ");
1529
+ console.log(`${shortDate(r.last_active)} sessions=${r.sessions.toString().padStart(3)} (${parts}) ${shortPath(r.cwd)}`);
1530
+ }
1531
+ console.log(`\n${top.length} project(s)${rows.length > top.length ? ` (of ${rows.length})` : ""}`);
1532
+ }
1533
+ function cmdContext(argv) {
1534
+ // Drill-down step 2 in the search workflow:
1535
+ // 1. `search <kw>` → pick a session
1536
+ // 2. `context <id> --grep <kw> --turns N --around M` → top-N hit turns with M
1537
+ // turns of context on either side, token-budgeted for AI consumption
1538
+ //
1539
+ // Without --grep: returns the first N turns (lets AI inspect session opening).
1540
+ // With --grep: ranks turns by (user-role first, then hit density), takes top-N,
1541
+ // then expands each by --around turns of surrounding context.
1542
+ const id = argv.positional[0];
1543
+ if (!id)
1544
+ die("usage: context <session-id> [--grep KW] [--turns N] [--around M]");
1545
+ const f = buildFilter(argv.flags);
1546
+ const s = findSessionById(id, f);
1547
+ if (!s)
1548
+ die(`session not found: ${id}`);
1549
+ const grepRaw = argv.flags.grep;
1550
+ const grep = typeof grepRaw === "string" ? grepRaw : undefined;
1551
+ const nTurns = typeof argv.flags.turns === "string" ? Number(argv.flags.turns) : 3;
1552
+ const around = typeof argv.flags.around === "string" ? Number(argv.flags.around) : 1;
1553
+ const maxChars = typeof argv.flags["max-chars"] === "string"
1554
+ ? Number(argv.flags["max-chars"])
1555
+ : 6000;
1556
+ let turns = extractDialogue(s);
1557
+ let mergedChildren = 0;
1558
+ if (argv.flags["include-children"] === true) {
1559
+ const all = listAll({ ...f, cwd: undefined, limit: 1_000_000 });
1560
+ const childIndex = buildChildIndex(all);
1561
+ const kids = childIndex.get(s.id) ?? [];
1562
+ mergedChildren = kids.length;
1563
+ for (const c of kids)
1564
+ turns = [...turns, ...extractDialogue(c)];
1565
+ }
1566
+ let hitIndices = [];
1567
+ let totalHitTurns = 0;
1568
+ if (grep) {
1569
+ const tokens = grep.toLowerCase().split(/\s+/).filter(Boolean);
1570
+ if (tokens.length === 0)
1571
+ die("--grep requires non-empty value");
1572
+ const matchCount = (text) => {
1573
+ const hay = text.toLowerCase();
1574
+ if (!tokens.every((tok) => hay.includes(tok)))
1575
+ return 0;
1576
+ let n = 0;
1577
+ for (const tok of tokens) {
1578
+ let from = 0;
1579
+ while (true) {
1580
+ const idx = hay.indexOf(tok, from);
1581
+ if (idx === -1)
1582
+ break;
1583
+ n++;
1584
+ from = idx + tok.length;
1585
+ }
1586
+ }
1587
+ return n;
1588
+ };
1589
+ const ranked = [];
1590
+ for (let i = 0; i < turns.length; i++) {
1591
+ const turn = turns[i];
1592
+ if (!turn)
1593
+ continue;
1594
+ const h = matchCount(turn.text);
1595
+ if (h > 0)
1596
+ ranked.push({ idx: i, role: turn.role, hits: h });
1597
+ }
1598
+ totalHitTurns = ranked.length;
1599
+ ranked.sort((a, b) => {
1600
+ if (a.role !== b.role)
1601
+ return a.role === "user" ? -1 : 1;
1602
+ if (b.hits !== a.hits)
1603
+ return b.hits - a.hits;
1604
+ return a.idx - b.idx;
1605
+ });
1606
+ hitIndices = ranked.slice(0, nTurns).map((r) => r.idx);
1607
+ }
1608
+ else {
1609
+ hitIndices = [];
1610
+ for (let i = 0; i < Math.min(nTurns, turns.length); i++)
1611
+ hitIndices.push(i);
1612
+ }
1613
+ // Expand each hit by `around` turns on either side; dedupe via Set.
1614
+ const display = new Set();
1615
+ for (const idx of hitIndices) {
1616
+ for (let j = Math.max(0, idx - around); j <= Math.min(turns.length - 1, idx + around); j++) {
1617
+ display.add(j);
1618
+ }
1619
+ }
1620
+ const ordered = [...display].sort((a, b) => a - b);
1621
+ const hitSet = new Set(hitIndices);
1622
+ const out = [];
1623
+ let used = 0;
1624
+ for (const i of ordered) {
1625
+ const t = turns[i];
1626
+ if (!t)
1627
+ continue;
1628
+ let text = t.text;
1629
+ // Per-turn cap: if a single turn exceeds half the budget, truncate it so we
1630
+ // still fit the rest of the requested context.
1631
+ const cap = Math.floor(maxChars / 2);
1632
+ if (text.length > cap)
1633
+ text = text.slice(0, cap) + `\n…[+${t.text.length - cap} chars]`;
1634
+ if (used + text.length > maxChars && out.length > 0)
1635
+ break;
1636
+ out.push({ idx: i, role: t.role, text, is_hit: hitSet.has(i) });
1637
+ used += text.length;
1638
+ }
1639
+ if (argv.flags.json) {
1640
+ console.log(JSON.stringify({
1641
+ session: s,
1642
+ query: grep,
1643
+ total_turns: turns.length,
1644
+ total_hit_turns: totalHitTurns,
1645
+ merged_children: mergedChildren,
1646
+ turns: out,
1647
+ }, null, 2));
1648
+ return;
1649
+ }
1650
+ console.log(`# context: [${s.platform}] ${s.id}`);
1651
+ if (s.title)
1652
+ console.log(`# title: ${s.title}`);
1653
+ if (s.cwd)
1654
+ console.log(`# cwd: ${shortPath(s.cwd)}`);
1655
+ if (grep)
1656
+ console.log(`# query: "${grep}" hit_turns=${totalHitTurns} showing top ${hitIndices.length}`);
1657
+ else
1658
+ console.log(`# no grep — showing first ${hitIndices.length} turns of ${turns.length}`);
1659
+ if (mergedChildren > 0)
1660
+ console.log(`# merged_children: ${mergedChildren}`);
1661
+ console.log(`# turns shown: ${out.length} budget_used: ${used}/${maxChars} chars`);
1662
+ console.log("");
1663
+ for (const t of out) {
1664
+ const marker = t.is_hit ? " ← hit" : "";
1665
+ console.log(`## turn ${t.idx} (${t.role})${marker}\n`);
1666
+ console.log(t.text);
1667
+ console.log("\n---\n");
1668
+ }
1669
+ }
1670
+ function parsePhaseFlag(raw) {
1671
+ if (raw === undefined || raw === false)
1672
+ return "all";
1673
+ if (raw === "brainstorm" || raw === "implement" || raw === "all")
1674
+ return raw;
1675
+ die(`unknown --phase: ${String(raw)} (expected brainstorm|implement|all)`);
1676
+ }
1677
+ /** Slice cleaned dialogue by phase. Claude and Codex have native boundary
1678
+ * detection (via raw JSONL `task.py create|start` invocations in tool_use /
1679
+ * function_call events). OpenCode does not — its session storage doesn't
1680
+ * expose Bash tool calls in a comparable shape, so it degrades to "all turns
1681
+ * + warning". */
1682
+ function slicePhase(s, phase) {
1683
+ const warnings = [];
1684
+ if (phase === "all" || s.platform === "opencode") {
1685
+ if (phase !== "all" && s.platform === "opencode") {
1686
+ warnings.push(`--phase ${phase} on platform=opencode is not yet supported; ` +
1687
+ `returning full dialogue.`);
1688
+ }
1689
+ const turns = extractDialogue(s);
1690
+ return {
1691
+ groups: [{ label: null, turns }],
1692
+ windows: [],
1693
+ totalTurns: turns.length,
1694
+ warnings,
1695
+ };
1696
+ }
1697
+ // Claude / Codex path: collect turns + task.py events in one raw-JSONL pass,
1698
+ // then build brainstorm windows.
1699
+ const { turns, events } = s.platform === "claude"
1700
+ ? collectClaudeTurnsAndEvents(s)
1701
+ : collectCodexTurnsAndEvents(s);
1702
+ const windows = buildBrainstormWindows(events, turns.length);
1703
+ if (phase === "brainstorm") {
1704
+ if (windows.length === 0) {
1705
+ warnings.push(`no task.py create/start boundary found in session — returning full dialogue.`);
1706
+ return {
1707
+ groups: [{ label: null, turns }],
1708
+ windows: [],
1709
+ totalTurns: turns.length,
1710
+ warnings,
1711
+ };
1712
+ }
1713
+ const groups = windows.map((w) => ({
1714
+ label: w.label,
1715
+ turns: turns.slice(w.startTurn, w.endTurn),
1716
+ }));
1717
+ return { groups, windows, totalTurns: turns.length, warnings };
1718
+ }
1719
+ // phase === "implement": all turns NOT inside any brainstorm window.
1720
+ if (windows.length === 0) {
1721
+ warnings.push(`no task.py create/start boundary found in session — implement phase is empty.`);
1722
+ return {
1723
+ groups: [{ label: null, turns: [] }],
1724
+ windows: [],
1725
+ totalTurns: turns.length,
1726
+ warnings,
1727
+ };
1728
+ }
1729
+ // Build set of indices covered by any brainstorm window.
1730
+ const covered = new Set();
1731
+ for (const w of windows) {
1732
+ for (let i = w.startTurn; i < w.endTurn; i++)
1733
+ covered.add(i);
1734
+ }
1735
+ const implementTurns = [];
1736
+ for (let i = 0; i < turns.length; i++) {
1737
+ if (!covered.has(i)) {
1738
+ const t = turns[i];
1739
+ if (t)
1740
+ implementTurns.push(t);
1741
+ }
1742
+ }
1743
+ return {
1744
+ groups: [{ label: null, turns: implementTurns }],
1745
+ windows,
1746
+ totalTurns: turns.length,
1747
+ warnings,
1748
+ };
1749
+ }
1750
+ function cmdExtract(argv) {
1751
+ const id = argv.positional[0];
1752
+ if (!id)
1753
+ die("usage: extract <session-id>");
1754
+ const f = buildFilter(argv.flags);
1755
+ const s = findSessionById(id, f);
1756
+ if (!s)
1757
+ die(`session not found: ${id}`);
1758
+ const phase = parsePhaseFlag(argv.flags.phase);
1759
+ const slice = slicePhase(s, phase);
1760
+ for (const w of slice.warnings)
1761
+ console.error(`warning: ${w}`);
1762
+ const grepRaw = argv.flags.grep;
1763
+ const grep = typeof grepRaw === "string" ? grepRaw.toLowerCase() : undefined;
1764
+ // Apply --grep AFTER phase slicing.
1765
+ const filterTurns = (turns) => grep ? turns.filter((t) => t.text.toLowerCase().includes(grep)) : turns;
1766
+ if (argv.flags.json) {
1767
+ const groups = slice.groups.map((g) => ({
1768
+ label: g.label,
1769
+ turns: filterTurns(g.turns),
1770
+ }));
1771
+ // For backwards compat when phase=all (single unlabeled group), expose
1772
+ // a flat `turns` field too. New `groups` / `windows` fields are added
1773
+ // unconditionally so AI consumers can rely on them.
1774
+ const flat = groups.flatMap((g) => g.turns);
1775
+ console.log(JSON.stringify({
1776
+ session: s,
1777
+ phase,
1778
+ windows: slice.windows,
1779
+ total_turns: slice.totalTurns,
1780
+ groups,
1781
+ turns: flat,
1782
+ }, null, 2));
1783
+ return;
1784
+ }
1785
+ console.log(`# session: [${s.platform}] ${s.id}`);
1786
+ if (s.title)
1787
+ console.log(`# title: ${s.title}`);
1788
+ if (s.cwd)
1789
+ console.log(`# cwd: ${shortPath(s.cwd)}`);
1790
+ if (s.created)
1791
+ console.log(`# date: ${shortDate(s.created)}`);
1792
+ const totalShown = slice.groups.reduce((n, g) => n + filterTurns(g.turns).length, 0);
1793
+ console.log(`# phase: ${phase} turns: ${totalShown}/${slice.totalTurns}` +
1794
+ (grep ? ` (filtered by /${grep}/)` : "") +
1795
+ (slice.windows.length > 0 ? ` windows: ${slice.windows.length}` : ""));
1796
+ console.log("");
1797
+ for (const g of slice.groups) {
1798
+ if (g.label !== null)
1799
+ console.log(`--- task: ${g.label} ---\n`);
1800
+ for (const t of filterTurns(g.turns)) {
1801
+ console.log(`## ${t.role === "user" ? "Human" : "Assistant"}\n`);
1802
+ console.log(t.text);
1803
+ console.log("\n---\n");
1804
+ }
1805
+ }
1806
+ }
1807
+ function cmdHelp() {
1808
+ console.log(`trellis mem — list/search Claude/Codex/OpenCode sessions
1809
+
1810
+ commands:
1811
+ list list sessions (default if no command)
1812
+ search <keyword> find sessions whose contents match keyword
1813
+ context <session-id> drill-down: top-N hit turns + surrounding context
1814
+ (paired with search; use --grep KW to anchor)
1815
+ extract <session-id> dump cleaned dialogue (use --grep KW to filter turns)
1816
+ projects list active projects (cwds) with session counts —
1817
+ use this to discover which --cwd to pass to search
1818
+
1819
+ flags:
1820
+ --platform claude|codex|opencode|all default all
1821
+ --since YYYY-MM-DD inclusive lower bound
1822
+ --until YYYY-MM-DD inclusive upper bound
1823
+ --global include all projects (default: cwd-scoped)
1824
+ --cwd <path> override the project cwd
1825
+ --limit N cap output (default 50)
1826
+ --grep KW extract / context: filter turns by keyword (multi-token AND)
1827
+ --phase brainstorm|implement|all extract: slice by Trellis brainstorm windows
1828
+ (default all; brainstorm = [task.py create, task.py start);
1829
+ Claude-only — Codex/OpenCode warn + return all)
1830
+ --turns N context: number of hit turns to return (default 3)
1831
+ --around N context: turns of surrounding context per hit (default 1)
1832
+ --max-chars N context: total char budget (default 6000, ~1500 tokens)
1833
+ --include-children search / context: merge OpenCode sub-agent sessions into parent
1834
+ --json emit JSON
1835
+ --help, -h show this help
1836
+
1837
+ examples:
1838
+ trellis mem list
1839
+ trellis mem list --global --platform claude --since 2026-04-01
1840
+ trellis mem search "session insight" --global
1841
+ trellis mem extract 5842592d --grep memory
1842
+ trellis mem extract 5842592d --phase brainstorm
1843
+ `);
1844
+ }
1845
+ // ---------- entry ----------
1846
+ export function runMem(args) {
1847
+ const argv = parseArgv(args);
1848
+ if (argv.flags.help ||
1849
+ argv.flags.h ||
1850
+ argv.cmd === "help" ||
1851
+ argv.cmd === "--help") {
1852
+ return cmdHelp();
1853
+ }
1854
+ switch (argv.cmd) {
1855
+ case "list":
1856
+ return cmdList(argv);
1857
+ case "search":
1858
+ return cmdSearch(argv);
1859
+ case "extract":
1860
+ return cmdExtract(argv);
1861
+ case "context":
1862
+ return cmdContext(argv);
1863
+ case "projects":
1864
+ return cmdProjects(argv);
1865
+ default:
1866
+ die(`unknown command: ${argv.cmd} (try 'help')`);
1867
+ }
1868
+ }
1869
+ //# sourceMappingURL=mem.js.map