@mindfoldhq/trellis 0.6.0-beta.2 → 0.6.0-beta.20

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 (330) hide show
  1. package/README.md +1 -1
  2. package/dist/cli/index.d.ts +1 -1
  3. package/dist/cli/index.d.ts.map +1 -1
  4. package/dist/cli/index.js +58 -2
  5. package/dist/cli/index.js.map +1 -1
  6. package/dist/commands/channel/adapters/claude.d.ts +29 -0
  7. package/dist/commands/channel/adapters/claude.d.ts.map +1 -0
  8. package/dist/commands/channel/adapters/claude.js +203 -0
  9. package/dist/commands/channel/adapters/claude.js.map +1 -0
  10. package/dist/commands/channel/adapters/codex.d.ts +85 -0
  11. package/dist/commands/channel/adapters/codex.d.ts.map +1 -0
  12. package/dist/commands/channel/adapters/codex.js +505 -0
  13. package/dist/commands/channel/adapters/codex.js.map +1 -0
  14. package/dist/commands/channel/adapters/index.d.ts +84 -0
  15. package/dist/commands/channel/adapters/index.d.ts.map +1 -0
  16. package/dist/commands/channel/adapters/index.js +115 -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/guard.d.ts +150 -0
  43. package/dist/commands/channel/guard.d.ts.map +1 -0
  44. package/dist/commands/channel/guard.js +474 -0
  45. package/dist/commands/channel/guard.js.map +1 -0
  46. package/dist/commands/channel/index.d.ts +3 -0
  47. package/dist/commands/channel/index.d.ts.map +1 -0
  48. package/dist/commands/channel/index.js +531 -0
  49. package/dist/commands/channel/index.js.map +1 -0
  50. package/dist/commands/channel/interrupt.d.ts +10 -0
  51. package/dist/commands/channel/interrupt.d.ts.map +1 -0
  52. package/dist/commands/channel/interrupt.js +22 -0
  53. package/dist/commands/channel/interrupt.js.map +1 -0
  54. package/dist/commands/channel/kill.d.ts +7 -0
  55. package/dist/commands/channel/kill.d.ts.map +1 -0
  56. package/dist/commands/channel/kill.js +121 -0
  57. package/dist/commands/channel/kill.js.map +1 -0
  58. package/dist/commands/channel/list.d.ts +17 -0
  59. package/dist/commands/channel/list.d.ts.map +1 -0
  60. package/dist/commands/channel/list.js +233 -0
  61. package/dist/commands/channel/list.js.map +1 -0
  62. package/dist/commands/channel/messages.d.ts +15 -0
  63. package/dist/commands/channel/messages.d.ts.map +1 -0
  64. package/dist/commands/channel/messages.js +245 -0
  65. package/dist/commands/channel/messages.js.map +1 -0
  66. package/dist/commands/channel/rm.d.ts +27 -0
  67. package/dist/commands/channel/rm.d.ts.map +1 -0
  68. package/dist/commands/channel/rm.js +216 -0
  69. package/dist/commands/channel/rm.js.map +1 -0
  70. package/dist/commands/channel/run.d.ts +30 -0
  71. package/dist/commands/channel/run.d.ts.map +1 -0
  72. package/dist/commands/channel/run.js +130 -0
  73. package/dist/commands/channel/run.js.map +1 -0
  74. package/dist/commands/channel/send.d.ts +11 -0
  75. package/dist/commands/channel/send.d.ts.map +1 -0
  76. package/dist/commands/channel/send.js +24 -0
  77. package/dist/commands/channel/send.js.map +1 -0
  78. package/dist/commands/channel/spawn.d.ts +40 -0
  79. package/dist/commands/channel/spawn.d.ts.map +1 -0
  80. package/dist/commands/channel/spawn.js +244 -0
  81. package/dist/commands/channel/spawn.js.map +1 -0
  82. package/dist/commands/channel/store/events.d.ts +39 -0
  83. package/dist/commands/channel/store/events.d.ts.map +1 -0
  84. package/dist/commands/channel/store/events.js +87 -0
  85. package/dist/commands/channel/store/events.js.map +1 -0
  86. package/dist/commands/channel/store/filter.d.ts +3 -0
  87. package/dist/commands/channel/store/filter.d.ts.map +1 -0
  88. package/dist/commands/channel/store/filter.js +2 -0
  89. package/dist/commands/channel/store/filter.js.map +1 -0
  90. package/dist/commands/channel/store/lock.d.ts +23 -0
  91. package/dist/commands/channel/store/lock.d.ts.map +1 -0
  92. package/dist/commands/channel/store/lock.js +99 -0
  93. package/dist/commands/channel/store/lock.js.map +1 -0
  94. package/dist/commands/channel/store/paths.d.ts +63 -0
  95. package/dist/commands/channel/store/paths.d.ts.map +1 -0
  96. package/dist/commands/channel/store/paths.js +246 -0
  97. package/dist/commands/channel/store/paths.js.map +1 -0
  98. package/dist/commands/channel/store/schema.d.ts +27 -0
  99. package/dist/commands/channel/store/schema.d.ts.map +1 -0
  100. package/dist/commands/channel/store/schema.js +34 -0
  101. package/dist/commands/channel/store/schema.js.map +1 -0
  102. package/dist/commands/channel/store/thread-state.d.ts +5 -0
  103. package/dist/commands/channel/store/thread-state.d.ts.map +1 -0
  104. package/dist/commands/channel/store/thread-state.js +16 -0
  105. package/dist/commands/channel/store/thread-state.js.map +1 -0
  106. package/dist/commands/channel/store/watch.d.ts +19 -0
  107. package/dist/commands/channel/store/watch.d.ts.map +1 -0
  108. package/dist/commands/channel/store/watch.js +146 -0
  109. package/dist/commands/channel/store/watch.js.map +1 -0
  110. package/dist/commands/channel/supervisor/idle.d.ts +46 -0
  111. package/dist/commands/channel/supervisor/idle.d.ts.map +1 -0
  112. package/dist/commands/channel/supervisor/idle.js +72 -0
  113. package/dist/commands/channel/supervisor/idle.js.map +1 -0
  114. package/dist/commands/channel/supervisor/inbox.d.ts +30 -0
  115. package/dist/commands/channel/supervisor/inbox.d.ts.map +1 -0
  116. package/dist/commands/channel/supervisor/inbox.js +160 -0
  117. package/dist/commands/channel/supervisor/inbox.js.map +1 -0
  118. package/dist/commands/channel/supervisor/shutdown.d.ts +68 -0
  119. package/dist/commands/channel/supervisor/shutdown.d.ts.map +1 -0
  120. package/dist/commands/channel/supervisor/shutdown.js +146 -0
  121. package/dist/commands/channel/supervisor/shutdown.js.map +1 -0
  122. package/dist/commands/channel/supervisor/stdout.d.ts +51 -0
  123. package/dist/commands/channel/supervisor/stdout.d.ts.map +1 -0
  124. package/dist/commands/channel/supervisor/stdout.js +121 -0
  125. package/dist/commands/channel/supervisor/stdout.js.map +1 -0
  126. package/dist/commands/channel/supervisor/turns.d.ts +31 -0
  127. package/dist/commands/channel/supervisor/turns.d.ts.map +1 -0
  128. package/dist/commands/channel/supervisor/turns.js +45 -0
  129. package/dist/commands/channel/supervisor/turns.js.map +1 -0
  130. package/dist/commands/channel/supervisor/warning.d.ts +48 -0
  131. package/dist/commands/channel/supervisor/warning.d.ts.map +1 -0
  132. package/dist/commands/channel/supervisor/warning.js +77 -0
  133. package/dist/commands/channel/supervisor/warning.js.map +1 -0
  134. package/dist/commands/channel/supervisor.d.ts +59 -0
  135. package/dist/commands/channel/supervisor.d.ts.map +1 -0
  136. package/dist/commands/channel/supervisor.js +344 -0
  137. package/dist/commands/channel/supervisor.js.map +1 -0
  138. package/dist/commands/channel/text-body.d.ts +13 -0
  139. package/dist/commands/channel/text-body.d.ts.map +1 -0
  140. package/dist/commands/channel/text-body.js +47 -0
  141. package/dist/commands/channel/text-body.js.map +1 -0
  142. package/dist/commands/channel/threads.d.ts +39 -0
  143. package/dist/commands/channel/threads.d.ts.map +1 -0
  144. package/dist/commands/channel/threads.js +106 -0
  145. package/dist/commands/channel/threads.js.map +1 -0
  146. package/dist/commands/channel/title.d.ts +12 -0
  147. package/dist/commands/channel/title.d.ts.map +1 -0
  148. package/dist/commands/channel/title.js +24 -0
  149. package/dist/commands/channel/title.js.map +1 -0
  150. package/dist/commands/channel/wait.d.ts +17 -0
  151. package/dist/commands/channel/wait.d.ts.map +1 -0
  152. package/dist/commands/channel/wait.js +75 -0
  153. package/dist/commands/channel/wait.js.map +1 -0
  154. package/dist/commands/init.d.ts +2 -0
  155. package/dist/commands/init.d.ts.map +1 -1
  156. package/dist/commands/init.js +97 -42
  157. package/dist/commands/init.js.map +1 -1
  158. package/dist/commands/mem.d.ts +13 -117
  159. package/dist/commands/mem.d.ts.map +1 -1
  160. package/dist/commands/mem.js +168 -1074
  161. package/dist/commands/mem.js.map +1 -1
  162. package/dist/commands/uninstall.d.ts.map +1 -1
  163. package/dist/commands/uninstall.js +28 -2
  164. package/dist/commands/uninstall.js.map +1 -1
  165. package/dist/commands/update.d.ts.map +1 -1
  166. package/dist/commands/update.js +31 -111
  167. package/dist/commands/update.js.map +1 -1
  168. package/dist/commands/upgrade.d.ts +28 -0
  169. package/dist/commands/upgrade.d.ts.map +1 -0
  170. package/dist/commands/upgrade.js +84 -0
  171. package/dist/commands/upgrade.js.map +1 -0
  172. package/dist/commands/workflow.d.ts +35 -0
  173. package/dist/commands/workflow.d.ts.map +1 -0
  174. package/dist/commands/workflow.js +219 -0
  175. package/dist/commands/workflow.js.map +1 -0
  176. package/dist/configurators/claude.d.ts.map +1 -1
  177. package/dist/configurators/claude.js +1 -0
  178. package/dist/configurators/claude.js.map +1 -1
  179. package/dist/configurators/codex.d.ts.map +1 -1
  180. package/dist/configurators/codex.js +5 -3
  181. package/dist/configurators/codex.js.map +1 -1
  182. package/dist/configurators/shared.js +4 -4
  183. package/dist/configurators/shared.js.map +1 -1
  184. package/dist/configurators/workflow.d.ts +8 -0
  185. package/dist/configurators/workflow.d.ts.map +1 -1
  186. package/dist/configurators/workflow.js +3 -2
  187. package/dist/configurators/workflow.js.map +1 -1
  188. package/dist/migrations/manifests/0.5.10.json +9 -0
  189. package/dist/migrations/manifests/0.5.11.json +16 -0
  190. package/dist/migrations/manifests/0.5.12.json +9 -0
  191. package/dist/migrations/manifests/0.5.13.json +9 -0
  192. package/dist/migrations/manifests/0.5.14.json +9 -0
  193. package/dist/migrations/manifests/0.5.15.json +9 -0
  194. package/dist/migrations/manifests/0.5.16.json +9 -0
  195. package/dist/migrations/manifests/0.5.17.json +9 -0
  196. package/dist/migrations/manifests/0.5.18.json +9 -0
  197. package/dist/migrations/manifests/0.6.0-beta.10.json +9 -0
  198. package/dist/migrations/manifests/0.6.0-beta.11.json +9 -0
  199. package/dist/migrations/manifests/0.6.0-beta.12.json +9 -0
  200. package/dist/migrations/manifests/0.6.0-beta.13.json +9 -0
  201. package/dist/migrations/manifests/0.6.0-beta.14.json +9 -0
  202. package/dist/migrations/manifests/0.6.0-beta.15.json +9 -0
  203. package/dist/migrations/manifests/0.6.0-beta.16.json +9 -0
  204. package/dist/migrations/manifests/0.6.0-beta.17.json +9 -0
  205. package/dist/migrations/manifests/0.6.0-beta.18.json +16 -0
  206. package/dist/migrations/manifests/0.6.0-beta.19.json +9 -0
  207. package/dist/migrations/manifests/0.6.0-beta.20.json +9 -0
  208. package/dist/migrations/manifests/0.6.0-beta.3.json +9 -0
  209. package/dist/migrations/manifests/0.6.0-beta.4.json +9 -0
  210. package/dist/migrations/manifests/0.6.0-beta.5.json +9 -0
  211. package/dist/migrations/manifests/0.6.0-beta.6.json +16 -0
  212. package/dist/migrations/manifests/0.6.0-beta.7.json +9 -0
  213. package/dist/migrations/manifests/0.6.0-beta.8.json +9 -0
  214. package/dist/migrations/manifests/0.6.0-beta.9.json +9 -0
  215. package/dist/templates/claude/agents/trellis-check.md +13 -7
  216. package/dist/templates/claude/agents/trellis-implement.md +8 -7
  217. package/dist/templates/claude/settings.json +4 -4
  218. package/dist/templates/codebuddy/agents/trellis-check.md +13 -7
  219. package/dist/templates/codebuddy/agents/trellis-implement.md +8 -7
  220. package/dist/templates/codebuddy/settings.json +4 -4
  221. package/dist/templates/codex/agents/trellis-check.toml +4 -4
  222. package/dist/templates/codex/agents/trellis-implement.toml +4 -4
  223. package/dist/templates/codex/config.toml +5 -3
  224. package/dist/templates/codex/hooks/session-start.py +205 -119
  225. package/dist/templates/codex/hooks.json +2 -2
  226. package/dist/templates/codex/skills/before-dev/SKILL.md +12 -6
  227. package/dist/templates/codex/skills/brainstorm/SKILL.md +69 -457
  228. package/dist/templates/codex/skills/check/SKILL.md +86 -18
  229. package/dist/templates/codex/skills/start/SKILL.md +33 -323
  230. package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-context-loading.md +7 -4
  231. package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-spec-structure.md +1 -1
  232. package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-workflow.md +3 -2
  233. package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/context-injection.md +5 -5
  234. package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/spec-system.md +1 -1
  235. package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/task-system.md +35 -6
  236. package/dist/templates/common/bundled-skills/trellis-meta/references/platform-files/agents.md +5 -4
  237. package/dist/templates/common/bundled-skills/trellis-spec-bootstarp/SKILL.md +41 -0
  238. package/dist/templates/common/bundled-skills/trellis-spec-bootstarp/references/mcp-setup.md +90 -0
  239. package/dist/templates/common/bundled-skills/trellis-spec-bootstarp/references/repository-analysis.md +59 -0
  240. package/dist/templates/common/bundled-skills/trellis-spec-bootstarp/references/spec-task-planning.md +61 -0
  241. package/dist/templates/common/bundled-skills/trellis-spec-bootstarp/references/spec-writing.md +70 -0
  242. package/dist/templates/common/commands/continue.md +6 -5
  243. package/dist/templates/common/commands/start.md +9 -6
  244. package/dist/templates/common/skills/before-dev.md +12 -6
  245. package/dist/templates/common/skills/brainstorm.md +68 -504
  246. package/dist/templates/common/skills/check.md +7 -1
  247. package/dist/templates/copilot/hooks/session-start.py +219 -101
  248. package/dist/templates/copilot/hooks.json +2 -2
  249. package/dist/templates/copilot/prompts/before-dev.prompt.md +12 -6
  250. package/dist/templates/copilot/prompts/brainstorm.prompt.md +69 -457
  251. package/dist/templates/copilot/prompts/check.prompt.md +86 -18
  252. package/dist/templates/copilot/prompts/parallel.prompt.md +16 -8
  253. package/dist/templates/copilot/prompts/start.prompt.md +33 -367
  254. package/dist/templates/cursor/agents/trellis-check.md +13 -7
  255. package/dist/templates/cursor/agents/trellis-implement.md +8 -7
  256. package/dist/templates/cursor/hooks.json +1 -7
  257. package/dist/templates/droid/droids/trellis-check.md +13 -7
  258. package/dist/templates/droid/droids/trellis-implement.md +8 -7
  259. package/dist/templates/droid/settings.json +4 -4
  260. package/dist/templates/gemini/agents/trellis-check.md +11 -5
  261. package/dist/templates/gemini/agents/trellis-implement.md +7 -6
  262. package/dist/templates/gemini/settings.json +2 -2
  263. package/dist/templates/kiro/agents/trellis-check.json +1 -1
  264. package/dist/templates/kiro/agents/trellis-implement.json +1 -1
  265. package/dist/templates/markdown/spec/guides/code-reuse-thinking-guide.md.txt +127 -9
  266. package/dist/templates/markdown/spec/guides/cross-layer-thinking-guide.md.txt +171 -6
  267. package/dist/templates/markdown/spec/guides/cross-platform-thinking-guide.md.txt +333 -43
  268. package/dist/templates/markdown/spec/guides/index.md.txt +18 -0
  269. package/dist/templates/opencode/agents/trellis-check.md +13 -7
  270. package/dist/templates/opencode/agents/trellis-implement.md +9 -8
  271. package/dist/templates/opencode/lib/session-utils.js +212 -123
  272. package/dist/templates/opencode/lib/trellis-context.js +73 -11
  273. package/dist/templates/opencode/plugins/inject-subagent-context.js +131 -29
  274. package/dist/templates/opencode/plugins/inject-workflow-state.js +9 -5
  275. package/dist/templates/opencode/plugins/session-start.js +9 -1
  276. package/dist/templates/pi/agents/trellis-check.md +5 -4
  277. package/dist/templates/pi/agents/trellis-implement.md +5 -4
  278. package/dist/templates/pi/extensions/trellis/index.ts.txt +1357 -754
  279. package/dist/templates/qoder/agents/trellis-check.md +11 -5
  280. package/dist/templates/qoder/agents/trellis-implement.md +7 -6
  281. package/dist/templates/qoder/settings.json +4 -4
  282. package/dist/templates/shared-hooks/index.d.ts.map +1 -1
  283. package/dist/templates/shared-hooks/index.js +0 -1
  284. package/dist/templates/shared-hooks/index.js.map +1 -1
  285. package/dist/templates/shared-hooks/inject-subagent-context.py +36 -14
  286. package/dist/templates/shared-hooks/inject-workflow-state.py +40 -42
  287. package/dist/templates/shared-hooks/session-start.py +222 -171
  288. package/dist/templates/trellis/config.yaml +38 -0
  289. package/dist/templates/trellis/index.d.ts +1 -0
  290. package/dist/templates/trellis/index.d.ts.map +1 -1
  291. package/dist/templates/trellis/index.js +2 -0
  292. package/dist/templates/trellis/index.js.map +1 -1
  293. package/dist/templates/trellis/scripts/add_session.py +50 -24
  294. package/dist/templates/trellis/scripts/common/config.py +57 -1
  295. package/dist/templates/trellis/scripts/common/safe_commit.py +285 -0
  296. package/dist/templates/trellis/scripts/common/session_context.py +384 -137
  297. package/dist/templates/trellis/scripts/common/task_context.py +3 -3
  298. package/dist/templates/trellis/scripts/common/task_store.py +161 -15
  299. package/dist/templates/trellis/scripts/common/workflow_phase.py +7 -10
  300. package/dist/templates/trellis/scripts/task.py +3 -3
  301. package/dist/templates/trellis/workflow.md +119 -98
  302. package/dist/utils/cwd-guard.d.ts +38 -0
  303. package/dist/utils/cwd-guard.d.ts.map +1 -0
  304. package/dist/utils/cwd-guard.js +62 -0
  305. package/dist/utils/cwd-guard.js.map +1 -0
  306. package/dist/utils/file-writer.d.ts +13 -0
  307. package/dist/utils/file-writer.d.ts.map +1 -1
  308. package/dist/utils/file-writer.js +59 -1
  309. package/dist/utils/file-writer.js.map +1 -1
  310. package/dist/utils/manifest-prune.d.ts +61 -0
  311. package/dist/utils/manifest-prune.d.ts.map +1 -0
  312. package/dist/utils/manifest-prune.js +136 -0
  313. package/dist/utils/manifest-prune.js.map +1 -0
  314. package/dist/utils/task-json.d.ts +9 -42
  315. package/dist/utils/task-json.d.ts.map +1 -1
  316. package/dist/utils/task-json.js +8 -45
  317. package/dist/utils/task-json.js.map +1 -1
  318. package/dist/utils/template-hash.d.ts +32 -6
  319. package/dist/utils/template-hash.d.ts.map +1 -1
  320. package/dist/utils/template-hash.js +53 -31
  321. package/dist/utils/template-hash.js.map +1 -1
  322. package/dist/utils/uninstall-scrubbers.d.ts +1 -0
  323. package/dist/utils/uninstall-scrubbers.d.ts.map +1 -1
  324. package/dist/utils/uninstall-scrubbers.js +21 -0
  325. package/dist/utils/uninstall-scrubbers.js.map +1 -1
  326. package/dist/utils/workflow-resolver.d.ts +86 -0
  327. package/dist/utils/workflow-resolver.d.ts.map +1 -0
  328. package/dist/utils/workflow-resolver.js +265 -0
  329. package/dist/utils/workflow-resolver.js.map +1 -0
  330. package/package.json +9 -8
@@ -1,5 +1,9 @@
1
1
  /**
2
- * mem.ts — search sessions across Claude Code / Codex / OpenCode.
2
+ * mem.ts — CLI wrapper over `@mindfoldhq/trellis-core/mem`.
3
+ *
4
+ * The reusable retrieval / context-extraction logic lives in core; this file
5
+ * owns only CLI concerns: argument parsing, terminal rendering, the OpenCode
6
+ * "reader unavailable" notice, and process exit behavior.
3
7
  *
4
8
  * Commands:
5
9
  * list list sessions (default if no command)
@@ -10,157 +14,9 @@
10
14
  *
11
15
  * Run `trellis mem help` for the full flag reference.
12
16
  */
13
- import * as fs from "node:fs";
14
- import * as path from "node:path";
15
17
  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
- messageDir: z.string().optional(),
28
- parent_id: z.string().optional(), // OpenCode only: parent session id (sub-agent chain)
29
- });
30
- const DialogueRoleSchema = z.enum(["user", "assistant"]);
31
- const SearchExcerptSchema = z.object({
32
- role: DialogueRoleSchema,
33
- snippet: z.string(),
34
- });
35
- const SearchHitSchema = z.object({
36
- count: z.number(), // total token occurrences across all matching turns
37
- user_count: z.number(), // breakdown: user-turn occurrences
38
- asst_count: z.number(), // breakdown: assistant-turn occurrences
39
- total_turns: z.number(), // size of cleaned dialogue (denominator for density)
40
- excerpts: z.array(SearchExcerptSchema),
41
- });
42
- /** Weighted-density relevance score:
43
- * (3 * user_hits + asst_hits) / total_turns
44
- * Higher = the session is more topically concentrated on the query AND the
45
- * user themselves brought it up (user hits weighted ×3 because the user's own
46
- * words anchor "what they actually cared about", while assistant elaboration
47
- * is downstream noise). */
48
- export function relevanceScore(h) {
49
- if (h.total_turns === 0)
50
- return 0;
51
- return (3 * h.user_count + h.asst_count) / h.total_turns;
52
- }
53
- const FilterSchema = z.object({
54
- platform: z.union([PlatformSchema, z.literal("all")]),
55
- since: z.date().optional(),
56
- until: z.date().optional(),
57
- cwd: z.string().optional(),
58
- limit: z.number(),
59
- });
60
- const ArgvSchema = z.object({
61
- cmd: z.string(),
62
- positional: z.array(z.string()),
63
- flags: z.record(z.string(), z.union([z.string(), z.boolean()])),
64
- });
65
- // ---------- schemas: external file formats ----------
66
- // Claude Code JSONL events. We only declare the fields we read; everything
67
- // else passes through. Content of an assistant `message` is an array of
68
- // blocks (text / thinking / tool_use); content of a user `message` is a
69
- // string for real human input or an array of tool_result blocks (skipped).
70
- const ClaudeBlockSchema = z
71
- .object({
72
- type: z.string().optional(),
73
- text: z.string().optional(),
74
- })
75
- .loose();
76
- const ClaudeMessageSchema = z
77
- .object({
78
- role: z.string().optional(),
79
- content: z.union([z.string(), z.array(ClaudeBlockSchema)]).optional(),
80
- })
81
- .loose();
82
- const ClaudeEventSchema = z
83
- .object({
84
- type: z.string().optional(),
85
- cwd: z.string().optional(),
86
- timestamp: z.string().optional(),
87
- message: ClaudeMessageSchema.optional(),
88
- isCompactSummary: z.boolean().optional(),
89
- })
90
- .loose();
91
- const ClaudeIndexEntrySchema = z
92
- .object({
93
- id: z.string(),
94
- cwd: z.string().optional(),
95
- created: z.string().optional(),
96
- title: z.string().optional(),
97
- })
98
- .loose();
99
- const ClaudeIndexSchema = z
100
- .object({ entries: z.array(ClaudeIndexEntrySchema).optional() })
101
- .loose();
102
- // Codex rollout JSONL events.
103
- const CodexContentPartSchema = z
104
- .object({
105
- type: z.string().optional(),
106
- text: z.string().optional(),
107
- })
108
- .loose();
109
- const CodexCompactedItemSchema = z
110
- .object({
111
- type: z.string().optional(),
112
- role: z.string().optional(),
113
- content: z.array(CodexContentPartSchema).optional(),
114
- })
115
- .loose();
116
- const CodexPayloadSchema = z
117
- .object({
118
- type: z.string().optional(),
119
- role: z.string().optional(),
120
- cwd: z.string().optional(),
121
- id: z.string().optional(),
122
- content: z.array(CodexContentPartSchema).optional(),
123
- replacement_history: z.array(CodexCompactedItemSchema).optional(),
124
- })
125
- .loose();
126
- const CodexEventSchema = z
127
- .object({
128
- timestamp: z.string().optional(),
129
- type: z.string().optional(),
130
- payload: CodexPayloadSchema.optional(),
131
- })
132
- .loose();
133
- // OpenCode session/message/part files.
134
- const OpenCodeSessionSchema = z
135
- .object({
136
- id: z.string(),
137
- title: z.string().optional(),
138
- directory: z.string().optional(),
139
- parentID: z.string().optional(),
140
- time: z
141
- .object({
142
- created: z.number().optional(),
143
- updated: z.number().optional(),
144
- })
145
- .loose()
146
- .optional(),
147
- })
148
- .loose();
149
- const OpenCodeMessageSchema = z
150
- .object({
151
- id: z.string(),
152
- role: z.string().optional(),
153
- time: z.object({ created: z.number().optional() }).loose().optional(),
154
- })
155
- .loose();
156
- const OpenCodePartSchema = z
157
- .object({
158
- type: z.string().optional(),
159
- text: z.string().optional(),
160
- synthetic: z.boolean().optional(),
161
- })
162
- .loose();
163
- // ---------- argv ----------
18
+ import * as path from "node:path";
19
+ import { extractMemDialogue, listMemProjects, listMemSessions, MemSessionNotFoundError, readMemContext, searchMemSessions, } from "@mindfoldhq/trellis-core/mem";
164
20
  export function parseArgv(argv) {
165
21
  const cmd = argv[0] ?? "list";
166
22
  const positional = [];
@@ -184,754 +40,72 @@ export function parseArgv(argv) {
184
40
  positional.push(a);
185
41
  }
186
42
  }
187
- return ArgvSchema.parse({ cmd, positional, flags });
43
+ return { cmd, positional, flags };
188
44
  }
45
+ const VALID_PLATFORMS = [
46
+ "claude",
47
+ "codex",
48
+ "opencode",
49
+ "all",
50
+ ];
51
+ /** Translate parsed CLI flags into a core `MemFilter`. Validation failures
52
+ * exit the process — core never sees raw CLI flags. */
189
53
  export function buildFilter(flags) {
190
54
  const platformRaw = typeof flags.platform === "string" ? flags.platform : "all";
191
- const platformParsed = z
192
- .union([PlatformSchema, z.literal("all")])
193
- .safeParse(platformRaw);
194
- if (!platformParsed.success)
55
+ if (!VALID_PLATFORMS.includes(platformRaw))
195
56
  die(`unknown platform: ${platformRaw}`);
57
+ const platform = platformRaw;
196
58
  const sinceRaw = flags.since;
197
59
  const since = typeof sinceRaw === "string" ? new Date(sinceRaw) : undefined;
198
60
  if (since && Number.isNaN(+since))
199
- die(`bad --since: ${sinceRaw}`);
61
+ die(`bad --since: ${String(sinceRaw)}`);
200
62
  const untilRaw = flags.until;
201
63
  const until = typeof untilRaw === "string"
202
64
  ? new Date(`${untilRaw}T23:59:59.999Z`)
203
65
  : undefined;
204
66
  if (until && Number.isNaN(+until))
205
- die(`bad --until: ${untilRaw}`);
67
+ die(`bad --until: ${String(untilRaw)}`);
206
68
  const cwd = flags.global
207
69
  ? undefined
208
70
  : path.resolve(typeof flags.cwd === "string" ? flags.cwd : process.cwd());
209
- const limit = typeof flags.limit === "string" ? Number(flags.limit) : 50;
210
- return FilterSchema.parse({
211
- platform: platformParsed.data,
212
- since,
213
- until,
214
- cwd,
215
- limit,
216
- });
71
+ const limit = parseOptionalNumberFlag(flags.limit, "--limit", 50);
72
+ return { platform, since, until, cwd, limit };
73
+ }
74
+ function parseOptionalNumberFlag(raw, name, fallback) {
75
+ if (raw === undefined || raw === false)
76
+ return fallback;
77
+ if (typeof raw !== "string")
78
+ die(`${name} requires a number`);
79
+ const value = Number(raw);
80
+ if (!Number.isFinite(value))
81
+ die(`bad ${name}: ${raw}`);
82
+ return value;
217
83
  }
218
84
  function die(msg) {
219
85
  console.error(`error: ${msg}`);
220
86
  process.exit(2);
221
87
  }
222
- // ---------- common helpers ----------
223
- const HOME = os.homedir();
224
- export function inRange(iso, f) {
225
- if (!iso)
226
- return true;
227
- const t = new Date(iso);
228
- if (Number.isNaN(+t))
229
- return true;
230
- if (f.since && t < f.since)
231
- return false;
232
- if (f.until && t > f.until)
233
- return false;
234
- return true;
235
- }
236
- /**
237
- * Interval-overlap version of `inRange` for sessions with both start and end
238
- * timestamps. A session is kept iff its lifetime `[start, end]` overlaps the
239
- * query window `[f.since, f.until]`.
240
- *
241
- * Why this exists: long / cross-day sessions (created on day N, still updated
242
- * on day N+M) were being dropped by `inRange(created, f)` when `--since` fell
243
- * after `created`. Switching to interval overlap keeps sessions that were
244
- * active inside the window even when they started before it.
245
- *
246
- * Degenerate inputs:
247
- * - both undefined → pass through (no timestamp = don't filter)
248
- * - one undefined → fall back to single-point semantics on the other end
249
- * - unparseable iso → defer to the parsable end (or pass through if both bad)
250
- */
251
- export function inRangeOverlap(start, end, f) {
252
- const s = start ?? end;
253
- const e = end ?? start;
254
- if (!s && !e)
255
- return true;
256
- if (f.since && e) {
257
- const eT = new Date(e);
258
- if (!Number.isNaN(+eT) && eT < f.since)
259
- return false;
260
- }
261
- if (f.until && s) {
262
- const sT = new Date(s);
263
- if (!Number.isNaN(+sT) && sT > f.until)
264
- return false;
265
- }
266
- return true;
267
- }
268
- export function sameProject(sessionCwd, target) {
269
- if (!target)
270
- return true;
271
- if (!sessionCwd)
272
- return false;
273
- const a = path.resolve(sessionCwd);
274
- const b = path.resolve(target);
275
- return a === b || a.startsWith(b + path.sep);
276
- }
277
- /** Walk JSONL line-by-line, calling `onLine` with each parsed object that
278
- * matches the supplied schema. Bad JSON or schema-mismatched lines are skipped.
279
- * Returning the literal "stop" from `onLine` halts iteration. */
280
- function readJsonl(file, schema, onLine) {
281
- let data;
282
- try {
283
- data = fs.readFileSync(file, "utf8");
284
- }
285
- catch {
88
+ // ---------- OpenCode reader notice ----------
89
+ //
90
+ // OpenCode 1.2+ moved to a SQLite store; the native dependency was reverted in
91
+ // 0.6.0-beta.4 due to install failures. Core's OpenCode adapter is a silent
92
+ // no-op — surfacing the degraded state is a CLI presentation concern, emitted
93
+ // once per process whenever the OpenCode source is in scope.
94
+ let opencodeWarned = false;
95
+ function warnOpencodeUnavailable() {
96
+ if (opencodeWarned)
286
97
  return;
287
- }
288
- for (const line of data.split("\n")) {
289
- if (!line.trim())
290
- continue;
291
- let raw;
292
- try {
293
- raw = JSON.parse(line);
294
- }
295
- catch {
296
- continue;
297
- }
298
- const parsed = schema.safeParse(raw);
299
- if (!parsed.success)
300
- continue;
301
- if (onLine(parsed.data) === "stop")
302
- return;
303
- }
98
+ opencodeWarned = true;
99
+ process.stderr.write("⚠️ tl mem: OpenCode platform reader is temporarily unavailable in this build.\n" +
100
+ " OpenCode 1.2+ moved to SQLite; the native dependency was reverted in\n" +
101
+ " 0.6.0-beta.4 due to install failures. Re-enabled in a future release.\n");
304
102
  }
305
- function readJsonlFirst(file, schema) {
306
- let result;
307
- readJsonl(file, schema, (obj) => {
308
- result = obj;
309
- return "stop";
310
- });
311
- return result;
312
- }
313
- function findInJsonl(file, schema, predicate, maxLines = 200) {
314
- let count = 0;
315
- let hit;
316
- readJsonl(file, schema, (obj) => {
317
- count++;
318
- if (predicate(obj)) {
319
- hit = obj;
320
- return "stop";
321
- }
322
- if (count >= maxLines)
323
- return "stop";
324
- });
325
- return hit;
326
- }
327
- function readJsonFile(file, schema) {
328
- let raw;
329
- try {
330
- raw = JSON.parse(fs.readFileSync(file, "utf8"));
331
- }
332
- catch {
333
- return undefined;
334
- }
335
- const parsed = schema.safeParse(raw);
336
- return parsed.success ? parsed.data : undefined;
337
- }
338
- // ---------- dialogue cleaning ----------
339
- const INJECTION_TAGS = [
340
- "system-reminder",
341
- "task-status",
342
- "ready",
343
- "current-state",
344
- "workflow",
345
- "workflow-state",
346
- "guidelines",
347
- "instructions",
348
- "command-name",
349
- "command-message",
350
- "command-args",
351
- "local-command-stdout",
352
- "local-command-stderr",
353
- "permissions instructions",
354
- "collaboration_mode",
355
- "environment_context",
356
- "auto_compact_summary",
357
- "user_instructions",
358
- ];
359
- /** True if this turn is a platform bootstrap injection (AGENTS.md, pure
360
- * INSTRUCTIONS preamble, etc.) and should be dropped wholesale rather than
361
- * partially cleaned. Detected after stripInjectionTags, so we look at what's
362
- * left after tag-stripping. */
363
- export function isBootstrapTurn(cleaned, originalLength) {
364
- if (cleaned.startsWith("# AGENTS.md instructions for"))
365
- return true;
366
- // A turn that's mostly an INSTRUCTIONS block (Codex injects this as user role).
367
- if (originalLength > 4000 && /^<INSTRUCTIONS>/i.test(cleaned))
368
- return true;
369
- return false;
370
- }
371
- export function stripInjectionTags(text) {
372
- let out = text;
373
- for (const tag of INJECTION_TAGS) {
374
- const escaped = tag.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
375
- // Case-insensitive: Codex/Trellis injection tags appear as both <INSTRUCTIONS>
376
- // and <instructions> across platforms.
377
- out = out.replace(new RegExp(`<${escaped}[^>]*>[\\s\\S]*?</${escaped}>`, "gi"), "");
378
- }
379
- out = out.replace(/^# AGENTS\.md instructions for[\s\S]*?(?=\n\n[A-Z一-龥]|$)/m, "");
380
- return out.replace(/\n{3,}/g, "\n\n").trim();
381
- }
382
- /** Find the paragraph-aligned chunk surrounding a hit position. A "chunk" is
383
- * the contiguous text bounded by the nearest blank-line breaks (`\n\n`) on
384
- * either side. If the natural paragraph exceeds `maxChars`, fall back to a
385
- * centered char window — and report the truncation so callers can mark it. */
386
- export function chunkAround(text, hitIdx, maxChars) {
387
- const startPara = text.lastIndexOf("\n\n", hitIdx);
388
- let start = startPara === -1 ? 0 : startPara + 2;
389
- const endPara = text.indexOf("\n\n", hitIdx);
390
- let end = endPara === -1 ? text.length : endPara;
391
- let truncated = false;
392
- if (end - start > maxChars) {
393
- start = Math.max(0, hitIdx - Math.floor(maxChars / 2));
394
- end = Math.min(text.length, hitIdx + Math.ceil(maxChars / 2));
395
- truncated = true;
396
- }
397
- return { start, end, truncated };
398
- }
399
- /** Multi-token AND grep over cleaned dialogue. Whitespace-split tokens; a
400
- * turn matches if every token (case-insensitive) appears anywhere in it.
401
- * `count` is the total occurrence count across all tokens within matching
402
- * turns. Excerpts are paragraph-aligned chunks (drawer-style): for each
403
- * matching turn we collect chunks around every hit position, dedupe by
404
- * chunk start so adjacent hits inside the same paragraph collapse to one
405
- * chunk. User-role chunks are listed first (the user's own words anchor
406
- * topic intent more reliably than AI elaboration). */
407
- export function searchInDialogue(turns, kw, maxExcerpts = 3, chunkChars = 400) {
408
- const tokens = kw.toLowerCase().split(/\s+/).filter(Boolean);
409
- const empty = SearchHitSchema.parse({
410
- count: 0,
411
- user_count: 0,
412
- asst_count: 0,
413
- total_turns: turns.length,
414
- excerpts: [],
415
- });
416
- if (tokens.length === 0)
417
- return empty;
418
- let userCount = 0;
419
- let asstCount = 0;
420
- const userExcerpts = [];
421
- const asstExcerpts = [];
422
- for (const t of turns) {
423
- const hay = t.text.toLowerCase();
424
- if (!tokens.every((tok) => hay.includes(tok)))
425
- continue;
426
- // Collect every hit position with the token that produced it (for both
427
- // counting and rarity-aware chunk anchor selection).
428
- const hitPositions = [];
429
- const tokenFreq = new Map();
430
- let turnHits = 0;
431
- for (const tok of tokens) {
432
- let from = 0;
433
- let n = 0;
434
- while (true) {
435
- const idx = hay.indexOf(tok, from);
436
- if (idx === -1)
437
- break;
438
- n++;
439
- turnHits++;
440
- hitPositions.push({ idx, tok });
441
- from = idx + tok.length;
442
- }
443
- tokenFreq.set(tok, n);
444
- }
445
- if (t.role === "user")
446
- userCount += turnHits;
447
- else
448
- asstCount += turnHits;
449
- hitPositions.sort((a, b) => a.idx - b.idx);
450
- const candidates = [];
451
- const seenStarts = new Set();
452
- for (const { idx, tok } of hitPositions) {
453
- const { start, end, truncated } = chunkAround(t.text, idx, chunkChars);
454
- if (seenStarts.has(start))
455
- continue;
456
- seenStarts.add(start);
457
- const slice = hay.slice(start, end);
458
- const coverage = tokens.filter((tk) => slice.includes(tk)).length;
459
- const rarity = 1 / (tokenFreq.get(tok) ?? 1);
460
- candidates.push({ start, end, truncated, coverage, rarity });
461
- }
462
- candidates.sort((a, b) => {
463
- if (b.coverage !== a.coverage)
464
- return b.coverage - a.coverage;
465
- if (b.rarity !== a.rarity)
466
- return b.rarity - a.rarity;
467
- return a.start - b.start;
468
- });
469
- for (const c of candidates) {
470
- let snippet = t.text.slice(c.start, c.end).trim();
471
- if (c.truncated) {
472
- if (c.start > 0)
473
- snippet = "…" + snippet;
474
- if (c.end < t.text.length)
475
- snippet += "…";
476
- }
477
- (t.role === "user" ? userExcerpts : asstExcerpts).push({
478
- role: t.role,
479
- snippet,
480
- });
481
- }
482
- }
483
- const excerpts = [...userExcerpts, ...asstExcerpts].slice(0, maxExcerpts);
484
- return SearchHitSchema.parse({
485
- count: userCount + asstCount,
486
- user_count: userCount,
487
- asst_count: asstCount,
488
- total_turns: turns.length,
489
- excerpts,
490
- });
491
- }
492
- // ---------- claude adapter ----------
493
- const CLAUDE_PROJECTS = path.join(HOME, ".claude", "projects");
494
- function claudeProjectDirFromCwd(cwd) {
495
- // Claude sanitizes path: every '/' and '_' becomes '-'.
496
- return path.join(CLAUDE_PROJECTS, cwd.replace(/[/_]/g, "-"));
497
- }
498
- export function claudeListSessions(f) {
499
- if (!fs.existsSync(CLAUDE_PROJECTS))
500
- return [];
501
- const out = [];
502
- const projectDirs = f.cwd
503
- ? [claudeProjectDirFromCwd(f.cwd)].filter((d) => fs.existsSync(d))
504
- : fs.readdirSync(CLAUDE_PROJECTS).map((d) => path.join(CLAUDE_PROJECTS, d));
505
- for (const dir of projectDirs) {
506
- let entries;
507
- try {
508
- entries = fs.readdirSync(dir, { withFileTypes: true });
509
- }
510
- catch {
511
- continue;
512
- }
513
- const indexFile = path.join(dir, "sessions-index.json");
514
- const index = readJsonFile(indexFile, ClaudeIndexSchema);
515
- const indexById = new Map();
516
- for (const e of index?.entries ?? [])
517
- indexById.set(e.id, e);
518
- for (const e of entries) {
519
- if (!e.isFile() || !e.name.endsWith(".jsonl"))
520
- continue;
521
- const filePath = path.join(dir, e.name);
522
- const id = e.name.replace(/\.jsonl$/, "");
523
- const idx = indexById.get(id);
524
- let cwd = idx?.cwd;
525
- let created = idx?.created;
526
- const title = idx?.title;
527
- if (!cwd || !created) {
528
- const evt = findInJsonl(filePath, ClaudeEventSchema, (o) => typeof o.cwd === "string", 100);
529
- cwd = cwd ?? evt?.cwd;
530
- created =
531
- created ??
532
- evt?.timestamp ??
533
- readJsonlFirst(filePath, ClaudeEventSchema)?.timestamp;
534
- }
535
- const stat = fs.statSync(filePath);
536
- const updated = stat.mtime.toISOString();
537
- // Interval overlap: keep sessions whose lifetime [created, updated]
538
- // intersects the query window. Cross-day sessions (created before
539
- // --since but still active inside it) must survive — see PRD
540
- // 05-08-mem-since-cross-day-filter.
541
- if (!inRangeOverlap(created, updated, f))
542
- continue;
543
- if (f.cwd && cwd && !sameProject(cwd, f.cwd))
544
- continue;
545
- out.push(SessionInfoSchema.parse({
546
- platform: "claude",
547
- id,
548
- title,
549
- cwd,
550
- created,
551
- updated,
552
- filePath,
553
- }));
554
- }
555
- }
556
- return out;
557
- }
558
- export function claudeExtractDialogue(s) {
559
- // Mirrors session-insight/extract-session.py:
560
- // - user: type=="user" + role=="user" + content is string (list = tool_result)
561
- // - assistant: type=="assistant" + role=="assistant", keep only `text` blocks
562
- // - thinking and tool_use blocks dropped entirely
563
- // - injection tags stripped
564
- // Compaction: when we hit a `user` event with isCompactSummary=true, drop all
565
- // pre-compact turns and replace them with a synthetic [compact summary] turn —
566
- // the pre-compact content is now redundant with the summary.
567
- let turns = [];
568
- readJsonl(s.filePath, ClaudeEventSchema, (obj) => {
569
- const t = obj.type;
570
- const msg = obj.message;
571
- if (!msg)
572
- return;
573
- const content = msg.content;
574
- if (t === "user" && obj.isCompactSummary === true) {
575
- let summary = "";
576
- if (typeof content === "string") {
577
- summary = stripInjectionTags(content);
578
- }
579
- else if (Array.isArray(content)) {
580
- const parts = [];
581
- for (const block of content) {
582
- if (block.type === "text" && typeof block.text === "string") {
583
- const cleaned = stripInjectionTags(block.text);
584
- if (cleaned)
585
- parts.push(cleaned);
586
- }
587
- }
588
- summary = parts.join("\n\n");
589
- }
590
- turns = summary
591
- ? [{ role: "user", text: `[compact summary]\n${summary}` }]
592
- : [];
593
- return;
594
- }
595
- if (t === "user" && msg.role === "user") {
596
- if (typeof content === "string") {
597
- const text = stripInjectionTags(content);
598
- if (text && !isBootstrapTurn(text, content.length)) {
599
- turns.push({ role: "user", text });
600
- }
601
- }
602
- }
603
- else if (t === "assistant" &&
604
- msg.role === "assistant" &&
605
- Array.isArray(content)) {
606
- const parts = [];
607
- for (const block of content) {
608
- if (block.type === "text" && typeof block.text === "string") {
609
- const cleaned = stripInjectionTags(block.text);
610
- if (cleaned)
611
- parts.push(cleaned);
612
- }
613
- }
614
- if (parts.length)
615
- turns.push({ role: "assistant", text: parts.join("\n\n") });
616
- }
617
- });
618
- return turns;
619
- }
620
- export function claudeSearch(s, kw) {
621
- return searchInDialogue(claudeExtractDialogue(s), kw);
622
- }
623
- // ---------- codex adapter ----------
624
- const CODEX_SESSIONS = path.join(HOME, ".codex", "sessions");
625
- function* walkDir(root) {
626
- if (!fs.existsSync(root))
627
- return;
628
- const stack = [root];
629
- while (stack.length) {
630
- const cur = stack.pop();
631
- if (cur === undefined)
632
- break;
633
- let entries;
634
- try {
635
- entries = fs.readdirSync(cur, { withFileTypes: true });
636
- }
637
- catch {
638
- continue;
639
- }
640
- for (const e of entries) {
641
- const p = path.join(cur, e.name);
642
- if (e.isDirectory())
643
- stack.push(p);
644
- else if (e.isFile())
645
- yield p;
646
- }
647
- }
648
- }
649
- export function codexListSessions(f) {
650
- if (!fs.existsSync(CODEX_SESSIONS))
651
- return [];
652
- const out = [];
653
- for (const file of walkDir(CODEX_SESSIONS)) {
654
- if (!file.endsWith(".jsonl"))
655
- continue;
656
- const base = path.basename(file, ".jsonl");
657
- const m = base.match(/^rollout-(\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2})-(.+)$/);
658
- const tsFromName = m?.[1]
659
- ? new Date(m[1].replace(/T(\d{2})-(\d{2})-(\d{2})/, "T$1:$2:$3") + "Z").toISOString()
660
- : undefined;
661
- // Note: we previously short-circuited on `!inRange(tsFromName, f)` here,
662
- // but the filename ts is the session's creation time — a cross-day session
663
- // that started before --since but was active inside it would be dropped.
664
- // Filter at the same place as claude/opencode using interval overlap.
665
- const first = readJsonlFirst(file, CodexEventSchema);
666
- const meta = first?.payload;
667
- const id = meta?.id ?? m?.[2] ?? base;
668
- const cwd = meta?.cwd;
669
- const created = first?.timestamp ?? tsFromName ?? "";
670
- if (f.cwd && !sameProject(cwd, f.cwd))
671
- continue;
672
- const updated = fs.statSync(file).mtime.toISOString();
673
- if (!inRangeOverlap(created, updated, f))
674
- continue;
675
- out.push(SessionInfoSchema.parse({
676
- platform: "codex",
677
- id,
678
- cwd,
679
- created,
680
- updated,
681
- filePath: file,
682
- }));
683
- }
684
- return out;
685
- }
686
- export function codexExtractDialogue(s) {
687
- // Codex events: payload.type=="message" with role in {user, assistant, developer, system}.
688
- // Keep user/assistant only. Each content part is {type: "input_text"|"output_text", text}.
689
- // Codex inlines a lot of system prompt as the first user message (AGENTS.md, permission
690
- // blocks, etc.) — stripInjectionTags removes the bulk; turns that are pure boilerplate
691
- // collapse to empty after strip and get dropped here.
692
- // Compaction: a top-level event with type=="compacted" carries a payload.replacement_history
693
- // array — the new authoritative history replacing everything before. We reset turns and
694
- // re-seed from replacement_history.
695
- let turns = [];
696
- const buildTurnFromMessage = (role, parts) => {
697
- const collected = [];
698
- let totalRaw = 0;
699
- for (const c of parts ?? []) {
700
- const txt = c.text;
701
- if (typeof txt !== "string")
702
- continue;
703
- if (c.type !== "input_text" && c.type !== "output_text")
704
- continue;
705
- totalRaw += txt.length;
706
- const cleaned = stripInjectionTags(txt);
707
- if (cleaned)
708
- collected.push(cleaned);
709
- }
710
- if (!collected.length)
711
- return null;
712
- const merged = collected.join("\n\n");
713
- if (isBootstrapTurn(merged, totalRaw))
714
- return null;
715
- return { role, text: merged };
716
- };
717
- readJsonl(s.filePath, CodexEventSchema, (obj) => {
718
- if (obj.type === "compacted") {
719
- const rh = obj.payload?.replacement_history;
720
- turns = [];
721
- if (!Array.isArray(rh))
722
- return;
723
- for (const item of rh) {
724
- if (item.type !== "message")
725
- continue;
726
- const r = DialogueRoleSchema.safeParse(item.role);
727
- if (!r.success)
728
- continue;
729
- const turn = buildTurnFromMessage(r.data, item.content);
730
- if (turn)
731
- turns.push({ role: turn.role, text: `[compact]\n${turn.text}` });
732
- }
733
- return;
734
- }
735
- const p = obj.payload;
736
- if (p?.type !== "message")
737
- return;
738
- const roleParsed = DialogueRoleSchema.safeParse(p.role);
739
- if (!roleParsed.success)
740
- return;
741
- const turn = buildTurnFromMessage(roleParsed.data, p.content);
742
- if (turn)
743
- turns.push(turn);
744
- });
745
- return turns;
746
- }
747
- export function codexSearch(s, kw) {
748
- return searchInDialogue(codexExtractDialogue(s), kw);
749
- }
750
- // ---------- opencode adapter ----------
751
- const OC_ROOT = path.join(HOME, ".local", "share", "opencode", "storage");
752
- const OC_SESSION_DIR = path.join(OC_ROOT, "session");
753
- const OC_MESSAGE_DIR = path.join(OC_ROOT, "message");
754
- const OC_PART_DIR = path.join(OC_ROOT, "part");
755
- export function opencodeListSessions(f) {
756
- if (!fs.existsSync(OC_SESSION_DIR))
757
- return [];
758
- const out = [];
759
- for (const file of walkDir(OC_SESSION_DIR)) {
760
- if (!file.endsWith(".json"))
761
- continue;
762
- const info = readJsonFile(file, OpenCodeSessionSchema);
763
- if (!info)
764
- continue;
765
- const created = info.time?.created !== undefined
766
- ? new Date(info.time.created).toISOString()
767
- : undefined;
768
- const updated = info.time?.updated !== undefined
769
- ? new Date(info.time.updated).toISOString()
770
- : undefined;
771
- const cwd = info.directory;
772
- if (f.cwd && !sameProject(cwd, f.cwd))
773
- continue;
774
- if (!inRangeOverlap(created, updated, f))
775
- continue;
776
- out.push(SessionInfoSchema.parse({
777
- platform: "opencode",
778
- id: info.id,
779
- title: info.title,
780
- cwd,
781
- created,
782
- updated,
783
- filePath: file,
784
- messageDir: path.join(OC_MESSAGE_DIR, info.id),
785
- parent_id: info.parentID,
786
- }));
787
- }
788
- return out;
789
- }
790
- function opencodeListMessageFiles(messageDir) {
791
- try {
792
- return fs.readdirSync(messageDir).filter((n) => n.endsWith(".json"));
793
- }
794
- catch {
795
- return [];
796
- }
797
- }
798
- export function opencodeExtractDialogue(s) {
799
- // OpenCode: messages live at message/<sid>/msg_*.json, part bodies at part/<msgId>/prt_*.json.
800
- // Keep parts with type=="text" && synthetic !== true; group by message; dialogue role
801
- // comes from the message file's `role` field. Synthetic parts are platform-injected
802
- // preamble (mode prompts, agent boilerplate) and are dropped as noise.
803
- const turns = [];
804
- if (!s.messageDir || !fs.existsSync(s.messageDir))
805
- return turns;
806
- const ordered = [];
807
- for (const mf of opencodeListMessageFiles(s.messageDir)) {
808
- const msg = readJsonFile(path.join(s.messageDir, mf), OpenCodeMessageSchema);
809
- if (msg)
810
- ordered.push({ msg, created: msg.time?.created ?? 0 });
811
- }
812
- ordered.sort((a, b) => a.created - b.created);
813
- for (const { msg } of ordered) {
814
- const roleParsed = DialogueRoleSchema.safeParse(msg.role);
815
- if (!roleParsed.success)
816
- continue;
817
- const partDir = path.join(OC_PART_DIR, msg.id);
818
- if (!fs.existsSync(partDir))
819
- continue;
820
- let parts;
821
- try {
822
- parts = fs.readdirSync(partDir).filter((n) => n.endsWith(".json"));
823
- }
824
- catch {
825
- continue;
826
- }
827
- const collected = [];
828
- let totalRaw = 0;
829
- for (const pf of parts) {
830
- const part = readJsonFile(path.join(partDir, pf), OpenCodePartSchema);
831
- if (!part)
832
- continue;
833
- if (part.type !== "text" || part.synthetic)
834
- continue;
835
- if (typeof part.text !== "string")
836
- continue;
837
- totalRaw += part.text.length;
838
- const cleaned = stripInjectionTags(part.text);
839
- if (cleaned)
840
- collected.push(cleaned);
841
- }
842
- if (!collected.length)
843
- continue;
844
- const merged = collected.join("\n\n");
845
- if (isBootstrapTurn(merged, totalRaw))
846
- continue;
847
- turns.push({ role: roleParsed.data, text: merged });
848
- }
849
- return turns;
850
- }
851
- function opencodeSearch(s, kw) {
852
- const turns = opencodeExtractDialogue(s);
853
- if (s.title)
854
- turns.unshift({ role: "user", text: s.title });
855
- return searchInDialogue(turns, kw);
856
- }
857
- // ---------- dispatch ----------
858
- function listAll(f) {
859
- const all = [];
860
- if (f.platform === "all" || f.platform === "claude")
861
- all.push(...claudeListSessions(f));
862
- if (f.platform === "all" || f.platform === "codex")
863
- all.push(...codexListSessions(f));
103
+ function maybeWarnOpencode(f) {
864
104
  if (f.platform === "all" || f.platform === "opencode")
865
- all.push(...opencodeListSessions(f));
866
- all.sort((a, b) => (b.updated ?? b.created ?? "").localeCompare(a.updated ?? a.created ?? ""));
867
- return all.slice(0, f.limit);
868
- }
869
- function extractDialogue(s) {
870
- switch (s.platform) {
871
- case "claude":
872
- return claudeExtractDialogue(s);
873
- case "codex":
874
- return codexExtractDialogue(s);
875
- case "opencode":
876
- return opencodeExtractDialogue(s);
877
- }
878
- }
879
- function searchSession(s, kw) {
880
- switch (s.platform) {
881
- case "claude":
882
- return claudeSearch(s, kw);
883
- case "codex":
884
- return codexSearch(s, kw);
885
- case "opencode":
886
- return opencodeSearch(s, kw);
887
- }
888
- }
889
- /** Build parent → descendants index for OpenCode (transitively flattened).
890
- * Other platforms have no native parent_id so they pass through unchanged. */
891
- function buildChildIndex(sessions) {
892
- const directChildren = new Map();
893
- for (const s of sessions) {
894
- if (!s.parent_id)
895
- continue;
896
- const list = directChildren.get(s.parent_id) ?? [];
897
- list.push(s);
898
- directChildren.set(s.parent_id, list);
899
- }
900
- // Transitive flatten: each parent maps to *all* descendants.
901
- const out = new Map();
902
- for (const [pid] of directChildren) {
903
- const stack = [...(directChildren.get(pid) ?? [])];
904
- const flat = [];
905
- while (stack.length) {
906
- const cur = stack.pop();
907
- if (cur === undefined)
908
- break;
909
- flat.push(cur);
910
- for (const c of directChildren.get(cur.id) ?? [])
911
- stack.push(c);
912
- }
913
- out.set(pid, flat);
914
- }
915
- return out;
916
- }
917
- function searchSessionWithChildren(s, kw, childIndex) {
918
- const children = childIndex.get(s.id) ?? [];
919
- if (children.length === 0)
920
- return searchSession(s, kw);
921
- // Concatenate parent + descendants' cleaned dialogue, then run a single
922
- // search over the merged turn list. This way scores reflect total topic
923
- // density across the sub-agent tree.
924
- const merged = [...extractDialogue(s)];
925
- for (const c of children)
926
- merged.push(...extractDialogue(c));
927
- return searchInDialogue(merged, kw);
928
- }
929
- function findSessionById(id, f) {
930
- const wide = { ...f, cwd: undefined, limit: 1_000_000 };
931
- const all = listAll(wide);
932
- return all.find((s) => s.id === id) ?? all.find((s) => s.id.startsWith(id));
105
+ warnOpencodeUnavailable();
933
106
  }
934
107
  // ---------- formatting ----------
108
+ const HOME = os.homedir();
935
109
  export function shortDate(iso) {
936
110
  if (!iso)
937
111
  return " ";
@@ -960,7 +134,8 @@ function printSessions(rows) {
960
134
  // ---------- commands ----------
961
135
  function cmdList(argv) {
962
136
  const f = buildFilter(argv.flags);
963
- const rows = listAll(f);
137
+ maybeWarnOpencode(f);
138
+ const rows = listMemSessions({ filter: f });
964
139
  if (argv.flags.json) {
965
140
  console.log(JSON.stringify(rows, null, 2));
966
141
  return;
@@ -976,52 +151,24 @@ function cmdSearch(argv) {
976
151
  if (!kw)
977
152
  die("usage: search <keyword>");
978
153
  const f = buildFilter(argv.flags);
979
- const wide = { ...f, limit: 1_000_000 };
980
- const candidates = listAll(wide);
154
+ maybeWarnOpencode(f);
981
155
  const includeChildren = argv.flags["include-children"] === true;
982
- // When --include-children is set: search over the merged dialogue of each
983
- // session plus its descendants (only OpenCode populates parent_id natively).
984
- // Children whose parent is also in the candidate set are dropped from the
985
- // result list — they get absorbed into the parent's hit.
986
- const childIndex = includeChildren ? buildChildIndex(candidates) : new Map();
987
- const candidateIds = new Set(candidates.map((s) => s.id));
988
- const isAbsorbedChild = (s) => includeChildren &&
989
- s.parent_id !== undefined &&
990
- candidateIds.has(s.parent_id);
991
- const matches = [];
992
- for (const s of candidates) {
993
- if (isAbsorbedChild(s))
994
- continue;
995
- const hit = includeChildren
996
- ? searchSessionWithChildren(s, kw, childIndex)
997
- : searchSession(s, kw);
998
- if (hit.count === 0)
999
- continue;
1000
- matches.push({ s, hit, descendants: childIndex.get(s.id)?.length ?? 0 });
1001
- }
1002
- // Rank by weighted-density relevance score: user hits matter ×3, normalized
1003
- // by total dialogue length so a tight 18-hit short session beats a sprawling
1004
- // 58-hit long one. Tie-break on raw count, then recency.
1005
- matches.sort((a, b) => {
1006
- const sa = relevanceScore(a.hit);
1007
- const sb = relevanceScore(b.hit);
1008
- if (sb !== sa)
1009
- return sb - sa;
1010
- if (b.hit.count !== a.hit.count)
1011
- return b.hit.count - a.hit.count;
1012
- return (b.s.updated ?? b.s.created ?? "").localeCompare(a.s.updated ?? a.s.created ?? "");
156
+ const result = searchMemSessions({
157
+ keyword: kw,
158
+ filter: f,
159
+ includeChildren,
1013
160
  });
1014
- const top = matches.slice(0, f.limit);
161
+ const top = result.matches;
1015
162
  if (argv.flags.json) {
1016
- console.log(JSON.stringify(top.map(({ s, hit, descendants }) => ({
1017
- session: s,
1018
- score: Number(relevanceScore(hit).toFixed(4)),
1019
- hit_count: hit.count,
1020
- user_count: hit.user_count,
1021
- asst_count: hit.asst_count,
1022
- total_turns: hit.total_turns,
1023
- descendants_merged: includeChildren ? descendants : 0,
1024
- excerpts: hit.excerpts,
163
+ console.log(JSON.stringify(top.map((m) => ({
164
+ session: m.session,
165
+ score: Number(m.score.toFixed(4)),
166
+ hit_count: m.hit.count,
167
+ user_count: m.hit.userCount,
168
+ asst_count: m.hit.asstCount,
169
+ total_turns: m.hit.totalTurns,
170
+ descendants_merged: includeChildren ? m.descendantsMerged : 0,
171
+ excerpts: m.hit.excerpts,
1025
172
  })), null, 2));
1026
173
  return;
1027
174
  }
@@ -1031,49 +178,30 @@ function cmdSearch(argv) {
1031
178
  console.log("(no matches)");
1032
179
  return;
1033
180
  }
1034
- for (const { s, hit, descendants } of top) {
181
+ for (const m of top) {
182
+ const s = m.session;
1035
183
  const idShort = s.id.slice(0, 12);
1036
- const score = relevanceScore(hit).toFixed(3);
1037
- const childTag = includeChildren && descendants > 0 ? ` +${descendants} child` : "";
184
+ const score = m.score.toFixed(3);
185
+ const childTag = includeChildren && m.descendantsMerged > 0
186
+ ? ` +${m.descendantsMerged} child`
187
+ : "";
1038
188
  console.log(`\n[${s.platform.padEnd(8)}] ${shortDate(s.updated ?? s.created)} ${idShort} ${shortPath(s.cwd)}` +
1039
- ` score=${score} hits=${hit.count} (u=${hit.user_count},a=${hit.asst_count}) turns=${hit.total_turns}${childTag}` +
189
+ ` score=${score} hits=${m.hit.count} (u=${m.hit.userCount},a=${m.hit.asstCount}) turns=${m.hit.totalTurns}${childTag}` +
1040
190
  (s.title ? ` — ${s.title}` : ""));
1041
- for (const ex of hit.excerpts) {
191
+ for (const ex of m.hit.excerpts) {
1042
192
  console.log(` [${ex.role}] ${ex.snippet}`);
1043
193
  }
1044
194
  }
1045
- console.log(`\n${top.length} session(s)${matches.length > top.length ? ` (of ${matches.length})` : ""}`);
195
+ console.log(`\n${top.length} session(s)${result.totalMatches > top.length ? ` (of ${result.totalMatches})` : ""}`);
1046
196
  }
1047
197
  function cmdProjects(argv) {
1048
- // List distinct cwds across all platforms with last-active timestamp + per-platform
1049
- // session counts. Designed for AI consumption: AI calls this first to learn which
1050
- // "门牌号" (project paths) have recent activity, then picks one for `--cwd` in
1051
- // a follow-up `search`.
198
+ // Distinct cwds across all platforms with last-active timestamp + per-platform
199
+ // session counts. AI calls this first to learn which project paths have
200
+ // recent activity, then picks one for `--cwd` in a follow-up `search`.
1052
201
  const f = buildFilter({ ...argv.flags, global: true });
1053
- const wide = { ...f, cwd: undefined, limit: 1_000_000 };
1054
- const all = listAll(wide);
1055
- const byCwd = new Map();
1056
- for (const s of all) {
1057
- if (!s.cwd)
1058
- continue;
1059
- const ts = s.updated ?? s.created ?? "";
1060
- let agg = byCwd.get(s.cwd);
1061
- if (!agg) {
1062
- agg = {
1063
- cwd: s.cwd,
1064
- last_active: ts,
1065
- sessions: 0,
1066
- by_platform: { claude: 0, codex: 0, opencode: 0 },
1067
- };
1068
- byCwd.set(s.cwd, agg);
1069
- }
1070
- agg.sessions++;
1071
- agg.by_platform[s.platform]++;
1072
- if (ts > agg.last_active)
1073
- agg.last_active = ts;
1074
- }
1075
- const rows = [...byCwd.values()].sort((a, b) => b.last_active.localeCompare(a.last_active));
1076
- const limit = typeof argv.flags.limit === "string" ? Number(argv.flags.limit) : 30;
202
+ maybeWarnOpencode(f);
203
+ const rows = listMemProjects({ filter: f });
204
+ const limit = parseOptionalNumberFlag(argv.flags.limit, "--limit", 30);
1077
205
  const top = rows.slice(0, limit);
1078
206
  if (argv.flags.json) {
1079
207
  console.log(JSON.stringify(top, null, 2));
@@ -1096,159 +224,117 @@ function cmdProjects(argv) {
1096
224
  console.log(`\n${top.length} project(s)${rows.length > top.length ? ` (of ${rows.length})` : ""}`);
1097
225
  }
1098
226
  function cmdContext(argv) {
1099
- // Drill-down step 2 in the search workflow:
1100
- // 1. `search <kw>` pick a session
1101
- // 2. `context <id> --grep <kw> --turns N --around M` → top-N hit turns with M
1102
- // turns of context on either side, token-budgeted for AI consumption
1103
- //
1104
- // Without --grep: returns the first N turns (lets AI inspect session opening).
1105
- // With --grep: ranks turns by (user-role first, then hit density), takes top-N,
1106
- // then expands each by --around turns of surrounding context.
227
+ // Drill-down step 2 in the search workflow: `search <kw>` picks a session,
228
+ // then `context <id> --grep <kw>` returns top-N hit turns with surrounding
229
+ // context, token-budgeted for AI consumption. Without --grep: first N turns.
1107
230
  const id = argv.positional[0];
1108
231
  if (!id)
1109
232
  die("usage: context <session-id> [--grep KW] [--turns N] [--around M]");
1110
233
  const f = buildFilter(argv.flags);
1111
- const s = findSessionById(id, f);
1112
- if (!s)
1113
- die(`session not found: ${id}`);
234
+ maybeWarnOpencode(f);
1114
235
  const grepRaw = argv.flags.grep;
1115
236
  const grep = typeof grepRaw === "string" ? grepRaw : undefined;
1116
- const nTurns = typeof argv.flags.turns === "string" ? Number(argv.flags.turns) : 3;
1117
- const around = typeof argv.flags.around === "string" ? Number(argv.flags.around) : 1;
1118
- const maxChars = typeof argv.flags["max-chars"] === "string"
1119
- ? Number(argv.flags["max-chars"])
1120
- : 6000;
1121
- let turns = extractDialogue(s);
1122
- let mergedChildren = 0;
1123
- if (argv.flags["include-children"] === true) {
1124
- const all = listAll({ ...f, cwd: undefined, limit: 1_000_000 });
1125
- const childIndex = buildChildIndex(all);
1126
- const kids = childIndex.get(s.id) ?? [];
1127
- mergedChildren = kids.length;
1128
- for (const c of kids)
1129
- turns = [...turns, ...extractDialogue(c)];
1130
- }
1131
- let hitIndices = [];
1132
- let totalHitTurns = 0;
1133
- if (grep) {
1134
- const tokens = grep.toLowerCase().split(/\s+/).filter(Boolean);
1135
- if (tokens.length === 0)
1136
- die("--grep requires non-empty value");
1137
- const matchCount = (text) => {
1138
- const hay = text.toLowerCase();
1139
- if (!tokens.every((tok) => hay.includes(tok)))
1140
- return 0;
1141
- let n = 0;
1142
- for (const tok of tokens) {
1143
- let from = 0;
1144
- while (true) {
1145
- const idx = hay.indexOf(tok, from);
1146
- if (idx === -1)
1147
- break;
1148
- n++;
1149
- from = idx + tok.length;
1150
- }
1151
- }
1152
- return n;
1153
- };
1154
- const ranked = [];
1155
- for (let i = 0; i < turns.length; i++) {
1156
- const turn = turns[i];
1157
- if (!turn)
1158
- continue;
1159
- const h = matchCount(turn.text);
1160
- if (h > 0)
1161
- ranked.push({ idx: i, role: turn.role, hits: h });
1162
- }
1163
- totalHitTurns = ranked.length;
1164
- ranked.sort((a, b) => {
1165
- if (a.role !== b.role)
1166
- return a.role === "user" ? -1 : 1;
1167
- if (b.hits !== a.hits)
1168
- return b.hits - a.hits;
1169
- return a.idx - b.idx;
237
+ if (grep?.split(/\s+/).filter(Boolean).length === 0)
238
+ die("--grep requires non-empty value");
239
+ const nTurns = parseOptionalNumberFlag(argv.flags.turns, "--turns", 3);
240
+ const around = parseOptionalNumberFlag(argv.flags.around, "--around", 1);
241
+ const maxChars = parseOptionalNumberFlag(argv.flags["max-chars"], "--max-chars", 6000);
242
+ const includeChildren = argv.flags["include-children"] === true;
243
+ let result;
244
+ try {
245
+ result = readMemContext({
246
+ sessionId: id,
247
+ filter: f,
248
+ grep,
249
+ turns: nTurns,
250
+ around,
251
+ maxChars,
252
+ includeChildren,
1170
253
  });
1171
- hitIndices = ranked.slice(0, nTurns).map((r) => r.idx);
1172
254
  }
1173
- else {
1174
- hitIndices = [];
1175
- for (let i = 0; i < Math.min(nTurns, turns.length); i++)
1176
- hitIndices.push(i);
1177
- }
1178
- // Expand each hit by `around` turns on either side; dedupe via Set.
1179
- const display = new Set();
1180
- for (const idx of hitIndices) {
1181
- for (let j = Math.max(0, idx - around); j <= Math.min(turns.length - 1, idx + around); j++) {
1182
- display.add(j);
1183
- }
1184
- }
1185
- const ordered = [...display].sort((a, b) => a - b);
1186
- const hitSet = new Set(hitIndices);
1187
- const out = [];
1188
- let used = 0;
1189
- for (const i of ordered) {
1190
- const t = turns[i];
1191
- if (!t)
1192
- continue;
1193
- let text = t.text;
1194
- // Per-turn cap: if a single turn exceeds half the budget, truncate it so we
1195
- // still fit the rest of the requested context.
1196
- const cap = Math.floor(maxChars / 2);
1197
- if (text.length > cap)
1198
- text = text.slice(0, cap) + `\n…[+${t.text.length - cap} chars]`;
1199
- if (used + text.length > maxChars && out.length > 0)
1200
- break;
1201
- out.push({ idx: i, role: t.role, text, is_hit: hitSet.has(i) });
1202
- used += text.length;
255
+ catch (error) {
256
+ if (error instanceof MemSessionNotFoundError)
257
+ die(`session not found: ${id}`);
258
+ throw error;
1203
259
  }
260
+ const s = result.session;
1204
261
  if (argv.flags.json) {
1205
262
  console.log(JSON.stringify({
1206
263
  session: s,
1207
- query: grep,
1208
- total_turns: turns.length,
1209
- total_hit_turns: totalHitTurns,
1210
- merged_children: mergedChildren,
1211
- turns: out,
264
+ query: result.query,
265
+ total_turns: result.totalTurns,
266
+ total_hit_turns: result.totalHitTurns,
267
+ merged_children: result.mergedChildren,
268
+ turns: result.turns.map((t) => ({
269
+ idx: t.idx,
270
+ role: t.role,
271
+ text: t.text,
272
+ is_hit: t.isHit,
273
+ })),
1212
274
  }, null, 2));
1213
275
  return;
1214
276
  }
277
+ // `hitIndices.length` from the legacy implementation — recomputed here for
278
+ // the human-readable header only.
279
+ const shown = grep
280
+ ? Math.min(result.totalHitTurns, nTurns)
281
+ : Math.min(nTurns, result.totalTurns);
1215
282
  console.log(`# context: [${s.platform}] ${s.id}`);
1216
283
  if (s.title)
1217
284
  console.log(`# title: ${s.title}`);
1218
285
  if (s.cwd)
1219
286
  console.log(`# cwd: ${shortPath(s.cwd)}`);
1220
287
  if (grep)
1221
- console.log(`# query: "${grep}" hit_turns=${totalHitTurns} showing top ${hitIndices.length}`);
288
+ console.log(`# query: "${grep}" hit_turns=${result.totalHitTurns} showing top ${shown}`);
1222
289
  else
1223
- console.log(`# no grep — showing first ${hitIndices.length} turns of ${turns.length}`);
1224
- if (mergedChildren > 0)
1225
- console.log(`# merged_children: ${mergedChildren}`);
1226
- console.log(`# turns shown: ${out.length} budget_used: ${used}/${maxChars} chars`);
290
+ console.log(`# no grep — showing first ${shown} turns of ${result.totalTurns}`);
291
+ if (result.mergedChildren > 0)
292
+ console.log(`# merged_children: ${result.mergedChildren}`);
293
+ console.log(`# turns shown: ${result.turns.length} budget_used: ${result.budgetUsed}/${result.maxChars} chars`);
1227
294
  console.log("");
1228
- for (const t of out) {
1229
- const marker = t.is_hit ? " ← hit" : "";
295
+ for (const t of result.turns) {
296
+ const marker = t.isHit ? " ← hit" : "";
1230
297
  console.log(`## turn ${t.idx} (${t.role})${marker}\n`);
1231
298
  console.log(t.text);
1232
299
  console.log("\n---\n");
1233
300
  }
1234
301
  }
302
+ function parsePhaseFlag(raw) {
303
+ if (raw === undefined || raw === false)
304
+ return "all";
305
+ if (raw === "brainstorm" || raw === "implement" || raw === "all")
306
+ return raw;
307
+ die(`unknown --phase: ${String(raw)} (expected brainstorm|implement|all)`);
308
+ }
1235
309
  function cmdExtract(argv) {
1236
310
  const id = argv.positional[0];
1237
311
  if (!id)
1238
312
  die("usage: extract <session-id>");
1239
313
  const f = buildFilter(argv.flags);
1240
- const s = findSessionById(id, f);
1241
- if (!s)
1242
- die(`session not found: ${id}`);
1243
- const turns = extractDialogue(s);
314
+ maybeWarnOpencode(f);
315
+ const phase = parsePhaseFlag(argv.flags.phase);
1244
316
  const grepRaw = argv.flags.grep;
1245
317
  const grep = typeof grepRaw === "string" ? grepRaw.toLowerCase() : undefined;
318
+ let result;
319
+ try {
320
+ result = extractMemDialogue({ sessionId: id, filter: f, phase, grep });
321
+ }
322
+ catch (error) {
323
+ if (error instanceof MemSessionNotFoundError)
324
+ die(`session not found: ${id}`);
325
+ throw error;
326
+ }
327
+ for (const w of result.warnings)
328
+ console.error(`warning: ${w.message}`);
329
+ const s = result.session;
1246
330
  if (argv.flags.json) {
1247
331
  console.log(JSON.stringify({
1248
332
  session: s,
1249
- turns: grep
1250
- ? turns.filter((t) => t.text.toLowerCase().includes(grep))
1251
- : turns,
333
+ phase: result.phase,
334
+ windows: result.windows,
335
+ total_turns: result.totalTurns,
336
+ groups: result.groups,
337
+ turns: result.turns,
1252
338
  }, null, 2));
1253
339
  return;
1254
340
  }
@@ -1259,14 +345,18 @@ function cmdExtract(argv) {
1259
345
  console.log(`# cwd: ${shortPath(s.cwd)}`);
1260
346
  if (s.created)
1261
347
  console.log(`# date: ${shortDate(s.created)}`);
1262
- console.log(`# turns: ${turns.length}${grep ? ` (filtered by /${grep}/)` : ""}`);
348
+ console.log(`# phase: ${result.phase} turns: ${result.turns.length}/${result.totalTurns}` +
349
+ (grep ? ` (filtered by /${grep}/)` : "") +
350
+ (result.windows.length > 0 ? ` windows: ${result.windows.length}` : ""));
1263
351
  console.log("");
1264
- for (const t of turns) {
1265
- if (grep && !t.text.toLowerCase().includes(grep))
1266
- continue;
1267
- console.log(`## ${t.role === "user" ? "Human" : "Assistant"}\n`);
1268
- console.log(t.text);
1269
- console.log("\n---\n");
352
+ for (const g of result.groups) {
353
+ if (g.label !== null)
354
+ console.log(`--- task: ${g.label} ---\n`);
355
+ for (const t of g.turns) {
356
+ console.log(`## ${t.role === "user" ? "Human" : "Assistant"}\n`);
357
+ console.log(t.text);
358
+ console.log("\n---\n");
359
+ }
1270
360
  }
1271
361
  }
1272
362
  function cmdHelp() {
@@ -1289,6 +379,9 @@ flags:
1289
379
  --cwd <path> override the project cwd
1290
380
  --limit N cap output (default 50)
1291
381
  --grep KW extract / context: filter turns by keyword (multi-token AND)
382
+ --phase brainstorm|implement|all extract: slice by Trellis brainstorm windows
383
+ (default all; brainstorm = [task.py create, task.py start);
384
+ Claude/Codex supported; OpenCode warns + returns all)
1292
385
  --turns N context: number of hit turns to return (default 3)
1293
386
  --around N context: turns of surrounding context per hit (default 1)
1294
387
  --max-chars N context: total char budget (default 6000, ~1500 tokens)
@@ -1301,6 +394,7 @@ examples:
1301
394
  trellis mem list --global --platform claude --since 2026-04-01
1302
395
  trellis mem search "session insight" --global
1303
396
  trellis mem extract 5842592d --grep memory
397
+ trellis mem extract 5842592d --phase brainstorm
1304
398
  `);
1305
399
  }
1306
400
  // ---------- entry ----------