@swarmify/agents-cli 1.11.3 → 1.13.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 (539) hide show
  1. package/CHANGELOG.md +40 -1
  2. package/LICENSE +21 -0
  3. package/README.md +209 -233
  4. package/dist/commands/__tests__/sessions.test.js +90 -20
  5. package/dist/commands/__tests__/sessions.test.js.map +1 -1
  6. package/dist/commands/cloud.d.ts +11 -0
  7. package/dist/commands/cloud.d.ts.map +1 -0
  8. package/dist/commands/cloud.js +363 -0
  9. package/dist/commands/cloud.js.map +1 -0
  10. package/dist/commands/commands.d.ts +9 -0
  11. package/dist/commands/commands.d.ts.map +1 -1
  12. package/dist/commands/commands.js +330 -139
  13. package/dist/commands/commands.js.map +1 -1
  14. package/dist/commands/daemon.d.ts +8 -0
  15. package/dist/commands/daemon.d.ts.map +1 -1
  16. package/dist/commands/daemon.js +38 -222
  17. package/dist/commands/daemon.js.map +1 -1
  18. package/dist/commands/drive.d.ts +8 -0
  19. package/dist/commands/drive.d.ts.map +1 -1
  20. package/dist/commands/drive.js +70 -7
  21. package/dist/commands/drive.js.map +1 -1
  22. package/dist/commands/exec.d.ts +9 -1
  23. package/dist/commands/exec.d.ts.map +1 -1
  24. package/dist/commands/exec.js +198 -24
  25. package/dist/commands/exec.js.map +1 -1
  26. package/dist/commands/factory.d.ts +11 -0
  27. package/dist/commands/factory.d.ts.map +1 -0
  28. package/dist/commands/factory.js +445 -0
  29. package/dist/commands/factory.js.map +1 -0
  30. package/dist/commands/fork.d.ts +8 -0
  31. package/dist/commands/fork.d.ts.map +1 -1
  32. package/dist/commands/fork.js +38 -4
  33. package/dist/commands/fork.js.map +1 -1
  34. package/dist/commands/hooks.d.ts +9 -0
  35. package/dist/commands/hooks.d.ts.map +1 -1
  36. package/dist/commands/hooks.js +252 -18
  37. package/dist/commands/hooks.js.map +1 -1
  38. package/dist/commands/init.d.ts +15 -0
  39. package/dist/commands/init.d.ts.map +1 -0
  40. package/dist/commands/init.js +137 -0
  41. package/dist/commands/init.js.map +1 -0
  42. package/dist/commands/mcp.d.ts +9 -0
  43. package/dist/commands/mcp.d.ts.map +1 -1
  44. package/dist/commands/mcp.js +250 -169
  45. package/dist/commands/mcp.js.map +1 -1
  46. package/dist/commands/models.d.ts +11 -0
  47. package/dist/commands/models.d.ts.map +1 -0
  48. package/dist/commands/models.js +170 -0
  49. package/dist/commands/models.js.map +1 -0
  50. package/dist/commands/packages.d.ts +8 -0
  51. package/dist/commands/packages.d.ts.map +1 -1
  52. package/dist/commands/packages.js +155 -14
  53. package/dist/commands/packages.js.map +1 -1
  54. package/dist/commands/permissions.d.ts +9 -0
  55. package/dist/commands/permissions.d.ts.map +1 -1
  56. package/dist/commands/permissions.js +72 -13
  57. package/dist/commands/permissions.js.map +1 -1
  58. package/dist/commands/plugins.d.ts +8 -0
  59. package/dist/commands/plugins.d.ts.map +1 -1
  60. package/dist/commands/plugins.js +265 -44
  61. package/dist/commands/plugins.js.map +1 -1
  62. package/dist/commands/profiles.d.ts +12 -0
  63. package/dist/commands/profiles.d.ts.map +1 -0
  64. package/dist/commands/profiles.js +255 -0
  65. package/dist/commands/profiles.js.map +1 -0
  66. package/dist/commands/pty.d.ts +1 -0
  67. package/dist/commands/pty.d.ts.map +1 -1
  68. package/dist/commands/pty.js +133 -22
  69. package/dist/commands/pty.js.map +1 -1
  70. package/dist/commands/pull.d.ts +8 -0
  71. package/dist/commands/pull.d.ts.map +1 -1
  72. package/dist/commands/pull.js +103 -14
  73. package/dist/commands/pull.js.map +1 -1
  74. package/dist/commands/push.d.ts +8 -0
  75. package/dist/commands/push.d.ts.map +1 -1
  76. package/dist/commands/push.js +37 -3
  77. package/dist/commands/push.js.map +1 -1
  78. package/dist/commands/refresh-memory.d.ts +16 -0
  79. package/dist/commands/refresh-memory.d.ts.map +1 -0
  80. package/dist/commands/refresh-memory.js +52 -0
  81. package/dist/commands/refresh-memory.js.map +1 -0
  82. package/dist/commands/resource-view.d.ts +39 -0
  83. package/dist/commands/resource-view.d.ts.map +1 -0
  84. package/dist/commands/resource-view.js +197 -0
  85. package/dist/commands/resource-view.js.map +1 -0
  86. package/dist/commands/routines.d.ts +8 -0
  87. package/dist/commands/routines.d.ts.map +1 -1
  88. package/dist/commands/routines.js +163 -34
  89. package/dist/commands/routines.js.map +1 -1
  90. package/dist/commands/rules.d.ts +9 -0
  91. package/dist/commands/rules.d.ts.map +1 -1
  92. package/dist/commands/rules.js +83 -12
  93. package/dist/commands/rules.js.map +1 -1
  94. package/dist/commands/secrets.d.ts +11 -0
  95. package/dist/commands/secrets.d.ts.map +1 -0
  96. package/dist/commands/secrets.js +352 -0
  97. package/dist/commands/secrets.js.map +1 -0
  98. package/dist/commands/sessions-picker.d.ts +18 -0
  99. package/dist/commands/sessions-picker.d.ts.map +1 -0
  100. package/dist/commands/sessions-picker.js +265 -0
  101. package/dist/commands/sessions-picker.js.map +1 -0
  102. package/dist/commands/sessions.d.ts +15 -0
  103. package/dist/commands/sessions.d.ts.map +1 -1
  104. package/dist/commands/sessions.js +708 -258
  105. package/dist/commands/sessions.js.map +1 -1
  106. package/dist/commands/skills.d.ts +9 -0
  107. package/dist/commands/skills.d.ts.map +1 -1
  108. package/dist/commands/skills.js +355 -233
  109. package/dist/commands/skills.js.map +1 -1
  110. package/dist/commands/status.d.ts +7 -0
  111. package/dist/commands/status.d.ts.map +1 -1
  112. package/dist/commands/status.js +7 -0
  113. package/dist/commands/status.js.map +1 -1
  114. package/dist/commands/subagents.d.ts +8 -0
  115. package/dist/commands/subagents.d.ts.map +1 -1
  116. package/dist/commands/subagents.js +214 -74
  117. package/dist/commands/subagents.js.map +1 -1
  118. package/dist/commands/sync.d.ts +8 -0
  119. package/dist/commands/sync.d.ts.map +1 -1
  120. package/dist/commands/sync.js +15 -7
  121. package/dist/commands/sync.js.map +1 -1
  122. package/dist/commands/teams-picker.d.ts +18 -0
  123. package/dist/commands/teams-picker.d.ts.map +1 -0
  124. package/dist/commands/teams-picker.js +290 -0
  125. package/dist/commands/teams-picker.js.map +1 -0
  126. package/dist/commands/teams.d.ts +18 -0
  127. package/dist/commands/teams.d.ts.map +1 -0
  128. package/dist/commands/teams.js +1098 -0
  129. package/dist/commands/teams.js.map +1 -0
  130. package/dist/commands/utils.d.ts +20 -0
  131. package/dist/commands/utils.d.ts.map +1 -1
  132. package/dist/commands/utils.js +34 -0
  133. package/dist/commands/utils.js.map +1 -1
  134. package/dist/commands/versions.d.ts +8 -0
  135. package/dist/commands/versions.d.ts.map +1 -1
  136. package/dist/commands/versions.js +71 -8
  137. package/dist/commands/versions.js.map +1 -1
  138. package/dist/commands/view.d.ts +34 -1
  139. package/dist/commands/view.d.ts.map +1 -1
  140. package/dist/commands/view.js +207 -13
  141. package/dist/commands/view.js.map +1 -1
  142. package/dist/index.d.ts +6 -0
  143. package/dist/index.d.ts.map +1 -1
  144. package/dist/index.js +136 -51
  145. package/dist/index.js.map +1 -1
  146. package/dist/lib/__tests__/bugfixes.test.js +3 -3
  147. package/dist/lib/__tests__/bugfixes.test.js.map +1 -1
  148. package/dist/lib/__tests__/exec.test.js +125 -19
  149. package/dist/lib/__tests__/exec.test.js.map +1 -1
  150. package/dist/lib/__tests__/hooks.test.d.ts +2 -0
  151. package/dist/lib/__tests__/hooks.test.d.ts.map +1 -0
  152. package/dist/lib/__tests__/hooks.test.js +203 -0
  153. package/dist/lib/__tests__/hooks.test.js.map +1 -0
  154. package/dist/lib/__tests__/memory-compile.test.d.ts +2 -0
  155. package/dist/lib/__tests__/memory-compile.test.d.ts.map +1 -0
  156. package/dist/lib/__tests__/memory-compile.test.js +95 -0
  157. package/dist/lib/__tests__/memory-compile.test.js.map +1 -0
  158. package/dist/lib/__tests__/models.test.d.ts +2 -0
  159. package/dist/lib/__tests__/models.test.d.ts.map +1 -0
  160. package/dist/lib/__tests__/models.test.js +239 -0
  161. package/dist/lib/__tests__/models.test.js.map +1 -0
  162. package/dist/lib/__tests__/rotate.test.d.ts +2 -0
  163. package/dist/lib/__tests__/rotate.test.d.ts.map +1 -0
  164. package/dist/lib/__tests__/rotate.test.js +80 -0
  165. package/dist/lib/__tests__/rotate.test.js.map +1 -0
  166. package/dist/lib/__tests__/secrets-bundles.test.d.ts +2 -0
  167. package/dist/lib/__tests__/secrets-bundles.test.d.ts.map +1 -0
  168. package/dist/lib/__tests__/secrets-bundles.test.js +104 -0
  169. package/dist/lib/__tests__/secrets-bundles.test.js.map +1 -0
  170. package/dist/lib/__tests__/secrets.test.d.ts +2 -0
  171. package/dist/lib/__tests__/secrets.test.d.ts.map +1 -0
  172. package/dist/lib/__tests__/secrets.test.js +90 -0
  173. package/dist/lib/__tests__/secrets.test.js.map +1 -0
  174. package/dist/lib/__tests__/shims.test.d.ts +2 -0
  175. package/dist/lib/__tests__/shims.test.d.ts.map +1 -0
  176. package/dist/lib/__tests__/shims.test.js +39 -0
  177. package/dist/lib/__tests__/shims.test.js.map +1 -0
  178. package/dist/lib/__tests__/usage.test.js +4 -2
  179. package/dist/lib/__tests__/usage.test.js.map +1 -1
  180. package/dist/lib/__tests__/versions.test.d.ts +2 -0
  181. package/dist/lib/__tests__/versions.test.d.ts.map +1 -0
  182. package/dist/lib/__tests__/versions.test.js +63 -0
  183. package/dist/lib/__tests__/versions.test.js.map +1 -0
  184. package/dist/lib/agents.d.ts +53 -1
  185. package/dist/lib/agents.d.ts.map +1 -1
  186. package/dist/lib/agents.js +178 -37
  187. package/dist/lib/agents.js.map +1 -1
  188. package/dist/lib/artifact-actions.d.ts +8 -3
  189. package/dist/lib/artifact-actions.d.ts.map +1 -1
  190. package/dist/lib/artifact-actions.js +8 -5
  191. package/dist/lib/artifact-actions.js.map +1 -1
  192. package/dist/lib/cloud/codex.d.ts +26 -0
  193. package/dist/lib/cloud/codex.d.ts.map +1 -0
  194. package/dist/lib/cloud/codex.js +237 -0
  195. package/dist/lib/cloud/codex.js.map +1 -0
  196. package/dist/lib/cloud/factory.d.ts +32 -0
  197. package/dist/lib/cloud/factory.d.ts.map +1 -0
  198. package/dist/lib/cloud/factory.js +43 -0
  199. package/dist/lib/cloud/factory.js.map +1 -0
  200. package/dist/lib/cloud/registry.d.ts +16 -0
  201. package/dist/lib/cloud/registry.d.ts.map +1 -0
  202. package/dist/lib/cloud/registry.js +68 -0
  203. package/dist/lib/cloud/registry.js.map +1 -0
  204. package/dist/lib/cloud/rush.d.ts +37 -0
  205. package/dist/lib/cloud/rush.d.ts.map +1 -0
  206. package/dist/lib/cloud/rush.js +230 -0
  207. package/dist/lib/cloud/rush.js.map +1 -0
  208. package/dist/lib/cloud/rush.test.d.ts +2 -0
  209. package/dist/lib/cloud/rush.test.d.ts.map +1 -0
  210. package/dist/lib/cloud/rush.test.js +63 -0
  211. package/dist/lib/cloud/rush.test.js.map +1 -0
  212. package/dist/lib/cloud/store.d.ts +23 -0
  213. package/dist/lib/cloud/store.d.ts.map +1 -0
  214. package/dist/lib/cloud/store.js +116 -0
  215. package/dist/lib/cloud/store.js.map +1 -0
  216. package/dist/lib/cloud/stream.d.ts +24 -0
  217. package/dist/lib/cloud/stream.d.ts.map +1 -0
  218. package/dist/lib/cloud/stream.js +145 -0
  219. package/dist/lib/cloud/stream.js.map +1 -0
  220. package/dist/lib/cloud/types.d.ts +109 -0
  221. package/dist/lib/cloud/types.d.ts.map +1 -0
  222. package/dist/lib/cloud/types.js +33 -0
  223. package/dist/lib/cloud/types.js.map +1 -0
  224. package/dist/lib/cloud/types.test.d.ts +2 -0
  225. package/dist/lib/cloud/types.test.d.ts.map +1 -0
  226. package/dist/lib/cloud/types.test.js +41 -0
  227. package/dist/lib/cloud/types.test.js.map +1 -0
  228. package/dist/lib/commands.d.ts +67 -0
  229. package/dist/lib/commands.d.ts.map +1 -1
  230. package/dist/lib/commands.js +161 -2
  231. package/dist/lib/commands.js.map +1 -1
  232. package/dist/lib/convert.d.ts +10 -0
  233. package/dist/lib/convert.d.ts.map +1 -1
  234. package/dist/lib/convert.js +9 -0
  235. package/dist/lib/convert.js.map +1 -1
  236. package/dist/lib/daemon.d.ts +21 -0
  237. package/dist/lib/daemon.d.ts.map +1 -1
  238. package/dist/lib/daemon.js +21 -0
  239. package/dist/lib/daemon.js.map +1 -1
  240. package/dist/lib/drive-sync.d.ts +18 -0
  241. package/dist/lib/drive-sync.d.ts.map +1 -1
  242. package/dist/lib/drive-sync.js +16 -0
  243. package/dist/lib/drive-sync.js.map +1 -1
  244. package/dist/lib/exec.d.ts +64 -3
  245. package/dist/lib/exec.d.ts.map +1 -1
  246. package/dist/lib/exec.js +218 -80
  247. package/dist/lib/exec.js.map +1 -1
  248. package/dist/lib/factory/__tests__/config.test.d.ts +2 -0
  249. package/dist/lib/factory/__tests__/config.test.d.ts.map +1 -0
  250. package/dist/lib/factory/__tests__/config.test.js +128 -0
  251. package/dist/lib/factory/__tests__/config.test.js.map +1 -0
  252. package/dist/lib/factory/config.d.ts +49 -0
  253. package/dist/lib/factory/config.d.ts.map +1 -0
  254. package/dist/lib/factory/config.js +127 -0
  255. package/dist/lib/factory/config.js.map +1 -0
  256. package/dist/lib/factory.js +1 -1
  257. package/dist/lib/factory.js.map +1 -1
  258. package/dist/lib/git.d.ts +16 -1
  259. package/dist/lib/git.d.ts.map +1 -1
  260. package/dist/lib/git.js +33 -4
  261. package/dist/lib/git.js.map +1 -1
  262. package/dist/lib/help.d.ts +7 -0
  263. package/dist/lib/help.d.ts.map +1 -1
  264. package/dist/lib/help.js +3 -0
  265. package/dist/lib/help.js.map +1 -1
  266. package/dist/lib/hooks.d.ts +59 -3
  267. package/dist/lib/hooks.d.ts.map +1 -1
  268. package/dist/lib/hooks.js +413 -33
  269. package/dist/lib/hooks.js.map +1 -1
  270. package/dist/lib/ledger/__tests__/local.test.d.ts +2 -0
  271. package/dist/lib/ledger/__tests__/local.test.d.ts.map +1 -0
  272. package/dist/lib/ledger/__tests__/local.test.js +177 -0
  273. package/dist/lib/ledger/__tests__/local.test.js.map +1 -0
  274. package/dist/lib/ledger/__tests__/sync.test.d.ts +2 -0
  275. package/dist/lib/ledger/__tests__/sync.test.d.ts.map +1 -0
  276. package/dist/lib/ledger/__tests__/sync.test.js +117 -0
  277. package/dist/lib/ledger/__tests__/sync.test.js.map +1 -0
  278. package/dist/lib/ledger/index.d.ts +18 -0
  279. package/dist/lib/ledger/index.d.ts.map +1 -0
  280. package/dist/lib/ledger/index.js +32 -0
  281. package/dist/lib/ledger/index.js.map +1 -0
  282. package/dist/lib/ledger/local.d.ts +22 -0
  283. package/dist/lib/ledger/local.d.ts.map +1 -0
  284. package/dist/lib/ledger/local.js +333 -0
  285. package/dist/lib/ledger/local.js.map +1 -0
  286. package/dist/lib/ledger/r2.d.ts +41 -0
  287. package/dist/lib/ledger/r2.d.ts.map +1 -0
  288. package/dist/lib/ledger/r2.js +335 -0
  289. package/dist/lib/ledger/r2.js.map +1 -0
  290. package/dist/lib/ledger/sync.d.ts +33 -0
  291. package/dist/lib/ledger/sync.d.ts.map +1 -0
  292. package/dist/lib/ledger/sync.js +106 -0
  293. package/dist/lib/ledger/sync.js.map +1 -0
  294. package/dist/lib/ledger/types.d.ts +100 -0
  295. package/dist/lib/ledger/types.d.ts.map +1 -0
  296. package/dist/lib/ledger/types.js +21 -0
  297. package/dist/lib/ledger/types.js.map +1 -0
  298. package/dist/lib/manifest.d.ts +6 -0
  299. package/dist/lib/manifest.d.ts.map +1 -1
  300. package/dist/lib/manifest.js +12 -0
  301. package/dist/lib/manifest.js.map +1 -1
  302. package/dist/lib/markdown.d.ts.map +1 -1
  303. package/dist/lib/markdown.js +6 -0
  304. package/dist/lib/markdown.js.map +1 -1
  305. package/dist/lib/mcp.d.ts +0 -9
  306. package/dist/lib/mcp.d.ts.map +1 -1
  307. package/dist/lib/mcp.js +0 -20
  308. package/dist/lib/mcp.js.map +1 -1
  309. package/dist/lib/memory-compile.d.ts +65 -0
  310. package/dist/lib/memory-compile.d.ts.map +1 -0
  311. package/dist/lib/memory-compile.js +174 -0
  312. package/dist/lib/memory-compile.js.map +1 -0
  313. package/dist/lib/memory.d.ts +8 -0
  314. package/dist/lib/memory.d.ts.map +1 -1
  315. package/dist/lib/memory.js +8 -0
  316. package/dist/lib/memory.js.map +1 -1
  317. package/dist/lib/models.d.ts +98 -0
  318. package/dist/lib/models.d.ts.map +1 -0
  319. package/dist/lib/models.js +722 -0
  320. package/dist/lib/models.js.map +1 -0
  321. package/dist/lib/permissions.d.ts +3 -1
  322. package/dist/lib/permissions.d.ts.map +1 -1
  323. package/dist/lib/permissions.js +13 -3
  324. package/dist/lib/permissions.js.map +1 -1
  325. package/dist/lib/picker.d.ts +27 -0
  326. package/dist/lib/picker.d.ts.map +1 -0
  327. package/dist/lib/picker.js +110 -0
  328. package/dist/lib/picker.js.map +1 -0
  329. package/dist/lib/plugins.d.ts +22 -0
  330. package/dist/lib/plugins.d.ts.map +1 -1
  331. package/dist/lib/plugins.js +114 -0
  332. package/dist/lib/plugins.js.map +1 -1
  333. package/dist/lib/profiles-keychain.d.ts +11 -0
  334. package/dist/lib/profiles-keychain.d.ts.map +1 -0
  335. package/dist/lib/profiles-keychain.js +14 -0
  336. package/dist/lib/profiles-keychain.js.map +1 -0
  337. package/dist/lib/profiles-presets.d.ts +25 -0
  338. package/dist/lib/profiles-presets.d.ts.map +1 -0
  339. package/dist/lib/profiles-presets.js +104 -0
  340. package/dist/lib/profiles-presets.js.map +1 -0
  341. package/dist/lib/profiles.d.ts +70 -0
  342. package/dist/lib/profiles.d.ts.map +1 -0
  343. package/dist/lib/profiles.js +145 -0
  344. package/dist/lib/profiles.js.map +1 -0
  345. package/dist/lib/pty-client.d.ts +1 -0
  346. package/dist/lib/pty-client.d.ts.map +1 -1
  347. package/dist/lib/pty-client.js.map +1 -1
  348. package/dist/lib/pty-server.d.ts +5 -0
  349. package/dist/lib/pty-server.d.ts.map +1 -1
  350. package/dist/lib/pty-server.js +5 -0
  351. package/dist/lib/pty-server.js.map +1 -1
  352. package/dist/lib/registry.d.ts +17 -0
  353. package/dist/lib/registry.d.ts.map +1 -1
  354. package/dist/lib/registry.js +17 -0
  355. package/dist/lib/registry.js.map +1 -1
  356. package/dist/lib/resources.d.ts +5 -0
  357. package/dist/lib/resources.d.ts.map +1 -1
  358. package/dist/lib/resources.js.map +1 -1
  359. package/dist/lib/rotate.d.ts +58 -0
  360. package/dist/lib/rotate.d.ts.map +1 -0
  361. package/dist/lib/rotate.js +93 -0
  362. package/dist/lib/rotate.js.map +1 -0
  363. package/dist/lib/routines.d.ts +30 -1
  364. package/dist/lib/routines.d.ts.map +1 -1
  365. package/dist/lib/routines.js +31 -4
  366. package/dist/lib/routines.js.map +1 -1
  367. package/dist/lib/runner.d.ts +14 -0
  368. package/dist/lib/runner.d.ts.map +1 -1
  369. package/dist/lib/runner.js +45 -11
  370. package/dist/lib/runner.js.map +1 -1
  371. package/dist/lib/sandbox.d.ts +16 -0
  372. package/dist/lib/sandbox.d.ts.map +1 -1
  373. package/dist/lib/sandbox.js +19 -2
  374. package/dist/lib/sandbox.js.map +1 -1
  375. package/dist/lib/scheduler.d.ts +8 -0
  376. package/dist/lib/scheduler.d.ts.map +1 -1
  377. package/dist/lib/scheduler.js +8 -0
  378. package/dist/lib/scheduler.js.map +1 -1
  379. package/dist/lib/secrets-bundles.d.ts +38 -0
  380. package/dist/lib/secrets-bundles.d.ts.map +1 -0
  381. package/dist/lib/secrets-bundles.js +176 -0
  382. package/dist/lib/secrets-bundles.js.map +1 -0
  383. package/dist/lib/secrets.d.ts +53 -0
  384. package/dist/lib/secrets.d.ts.map +1 -0
  385. package/dist/lib/secrets.js +140 -0
  386. package/dist/lib/secrets.js.map +1 -0
  387. package/dist/lib/session/__tests__/db.test.d.ts +2 -0
  388. package/dist/lib/session/__tests__/db.test.d.ts.map +1 -0
  389. package/dist/lib/session/__tests__/db.test.js +54 -0
  390. package/dist/lib/session/__tests__/db.test.js.map +1 -0
  391. package/dist/lib/session/__tests__/discover.test.js +54 -91
  392. package/dist/lib/session/__tests__/discover.test.js.map +1 -1
  393. package/dist/lib/session/__tests__/prompt.test.d.ts +2 -0
  394. package/dist/lib/session/__tests__/prompt.test.d.ts.map +1 -0
  395. package/dist/lib/session/__tests__/prompt.test.js +44 -0
  396. package/dist/lib/session/__tests__/prompt.test.js.map +1 -0
  397. package/dist/lib/session/__tests__/render.test.d.ts +2 -0
  398. package/dist/lib/session/__tests__/render.test.d.ts.map +1 -0
  399. package/dist/lib/session/__tests__/render.test.js +602 -0
  400. package/dist/lib/session/__tests__/render.test.js.map +1 -0
  401. package/dist/lib/session/artifacts.d.ts +15 -0
  402. package/dist/lib/session/artifacts.d.ts.map +1 -0
  403. package/dist/lib/session/artifacts.js +86 -0
  404. package/dist/lib/session/artifacts.js.map +1 -0
  405. package/dist/lib/session/db.d.ts +140 -0
  406. package/dist/lib/session/db.d.ts.map +1 -0
  407. package/dist/lib/session/db.js +599 -0
  408. package/dist/lib/session/db.js.map +1 -0
  409. package/dist/lib/session/discover.d.ts +44 -32
  410. package/dist/lib/session/discover.d.ts.map +1 -1
  411. package/dist/lib/session/discover.js +418 -464
  412. package/dist/lib/session/discover.js.map +1 -1
  413. package/dist/lib/session/parse.d.ts +11 -0
  414. package/dist/lib/session/parse.d.ts.map +1 -1
  415. package/dist/lib/session/parse.js +50 -0
  416. package/dist/lib/session/parse.js.map +1 -1
  417. package/dist/lib/session/prompt.d.ts +10 -0
  418. package/dist/lib/session/prompt.d.ts.map +1 -1
  419. package/dist/lib/session/prompt.js +38 -2
  420. package/dist/lib/session/prompt.js.map +1 -1
  421. package/dist/lib/session/prompt.test.d.ts +2 -0
  422. package/dist/lib/session/prompt.test.d.ts.map +1 -0
  423. package/dist/lib/session/prompt.test.js +57 -0
  424. package/dist/lib/session/prompt.test.js.map +1 -0
  425. package/dist/lib/session/render.d.ts +91 -10
  426. package/dist/lib/session/render.d.ts.map +1 -1
  427. package/dist/lib/session/render.js +708 -180
  428. package/dist/lib/session/render.js.map +1 -1
  429. package/dist/lib/session/team-filter.d.ts +35 -0
  430. package/dist/lib/session/team-filter.d.ts.map +1 -0
  431. package/dist/lib/session/team-filter.js +75 -0
  432. package/dist/lib/session/team-filter.js.map +1 -0
  433. package/dist/lib/session/team-filter.test.d.ts +2 -0
  434. package/dist/lib/session/team-filter.test.d.ts.map +1 -0
  435. package/dist/lib/session/team-filter.test.js +157 -0
  436. package/dist/lib/session/team-filter.test.js.map +1 -0
  437. package/dist/lib/session/types.d.ts +48 -6
  438. package/dist/lib/session/types.d.ts.map +1 -1
  439. package/dist/lib/session/types.js +9 -0
  440. package/dist/lib/session/types.js.map +1 -1
  441. package/dist/lib/shims.d.ts +73 -2
  442. package/dist/lib/shims.d.ts.map +1 -1
  443. package/dist/lib/shims.js +171 -25
  444. package/dist/lib/shims.js.map +1 -1
  445. package/dist/lib/skills.d.ts +68 -0
  446. package/dist/lib/skills.d.ts.map +1 -1
  447. package/dist/lib/skills.js +267 -1
  448. package/dist/lib/skills.js.map +1 -1
  449. package/dist/lib/state.d.ts +41 -2
  450. package/dist/lib/state.d.ts.map +1 -1
  451. package/dist/lib/state.js +63 -4
  452. package/dist/lib/state.js.map +1 -1
  453. package/dist/lib/subagents.d.ts +9 -0
  454. package/dist/lib/subagents.d.ts.map +1 -1
  455. package/dist/lib/subagents.js +9 -1
  456. package/dist/lib/subagents.js.map +1 -1
  457. package/dist/lib/teams/__tests__/oracle.test.d.ts +2 -0
  458. package/dist/lib/teams/__tests__/oracle.test.d.ts.map +1 -0
  459. package/dist/lib/teams/__tests__/oracle.test.js +89 -0
  460. package/dist/lib/teams/__tests__/oracle.test.js.map +1 -0
  461. package/dist/lib/teams/__tests__/supervisor.test.d.ts +2 -0
  462. package/dist/lib/teams/__tests__/supervisor.test.d.ts.map +1 -0
  463. package/dist/lib/teams/__tests__/supervisor.test.js +179 -0
  464. package/dist/lib/teams/__tests__/supervisor.test.js.map +1 -0
  465. package/dist/lib/teams/agents.d.ts +247 -0
  466. package/dist/lib/teams/agents.d.ts.map +1 -0
  467. package/dist/lib/teams/agents.js +1244 -0
  468. package/dist/lib/teams/agents.js.map +1 -0
  469. package/dist/lib/teams/api.d.ts +91 -0
  470. package/dist/lib/teams/api.d.ts.map +1 -0
  471. package/dist/lib/teams/api.js +239 -0
  472. package/dist/lib/teams/api.js.map +1 -0
  473. package/dist/lib/teams/cloud.d.ts +11 -0
  474. package/dist/lib/teams/cloud.d.ts.map +1 -0
  475. package/dist/lib/teams/cloud.js +169 -0
  476. package/dist/lib/teams/cloud.js.map +1 -0
  477. package/dist/lib/teams/debug.d.ts +8 -0
  478. package/dist/lib/teams/debug.d.ts.map +1 -0
  479. package/dist/lib/teams/debug.js +12 -0
  480. package/dist/lib/teams/debug.js.map +1 -0
  481. package/dist/lib/teams/file_ops.d.ts +13 -0
  482. package/dist/lib/teams/file_ops.d.ts.map +1 -0
  483. package/dist/lib/teams/file_ops.js +66 -0
  484. package/dist/lib/teams/file_ops.js.map +1 -0
  485. package/dist/lib/teams/index.d.ts +16 -0
  486. package/dist/lib/teams/index.d.ts.map +1 -0
  487. package/dist/lib/teams/index.js +15 -0
  488. package/dist/lib/teams/index.js.map +1 -0
  489. package/dist/lib/teams/oracle.d.ts +20 -0
  490. package/dist/lib/teams/oracle.d.ts.map +1 -0
  491. package/dist/lib/teams/oracle.js +59 -0
  492. package/dist/lib/teams/oracle.js.map +1 -0
  493. package/dist/lib/teams/parsers.d.ts +9 -0
  494. package/dist/lib/teams/parsers.d.ts.map +1 -0
  495. package/dist/lib/teams/parsers.js +837 -0
  496. package/dist/lib/teams/parsers.js.map +1 -0
  497. package/dist/lib/teams/persistence.d.ts +43 -0
  498. package/dist/lib/teams/persistence.d.ts.map +1 -0
  499. package/dist/lib/teams/persistence.js +299 -0
  500. package/dist/lib/teams/persistence.js.map +1 -0
  501. package/dist/lib/teams/ralph.d.ts +8 -0
  502. package/dist/lib/teams/ralph.d.ts.map +1 -0
  503. package/dist/lib/teams/ralph.js +59 -0
  504. package/dist/lib/teams/ralph.js.map +1 -0
  505. package/dist/lib/teams/registry.d.ts +18 -0
  506. package/dist/lib/teams/registry.d.ts.map +1 -0
  507. package/dist/lib/teams/registry.js +68 -0
  508. package/dist/lib/teams/registry.js.map +1 -0
  509. package/dist/lib/teams/summarizer.d.ts +73 -0
  510. package/dist/lib/teams/summarizer.d.ts.map +1 -0
  511. package/dist/lib/teams/summarizer.js +780 -0
  512. package/dist/lib/teams/summarizer.js.map +1 -0
  513. package/dist/lib/teams/supervisor.d.ts +49 -0
  514. package/dist/lib/teams/supervisor.d.ts.map +1 -0
  515. package/dist/lib/teams/supervisor.js +74 -0
  516. package/dist/lib/teams/supervisor.js.map +1 -0
  517. package/dist/lib/template.d.ts +8 -5
  518. package/dist/lib/template.d.ts.map +1 -1
  519. package/dist/lib/template.js +8 -5
  520. package/dist/lib/template.js.map +1 -1
  521. package/dist/lib/types.d.ts +58 -1
  522. package/dist/lib/types.d.ts.map +1 -1
  523. package/dist/lib/types.js +16 -1
  524. package/dist/lib/types.js.map +1 -1
  525. package/dist/lib/usage.d.ts +48 -0
  526. package/dist/lib/usage.d.ts.map +1 -1
  527. package/dist/lib/usage.js +103 -11
  528. package/dist/lib/usage.js.map +1 -1
  529. package/dist/lib/versions.d.ts +12 -1
  530. package/dist/lib/versions.d.ts.map +1 -1
  531. package/dist/lib/versions.js +91 -35
  532. package/dist/lib/versions.js.map +1 -1
  533. package/package.json +20 -6
  534. package/scripts/postinstall.js +1 -1
  535. package/scripts/rebuild-sqlite.sh +46 -0
  536. package/dist/commands/sessions.test.d.ts +0 -2
  537. package/dist/commands/sessions.test.d.ts.map +0 -1
  538. package/dist/commands/sessions.test.js +0 -53
  539. package/dist/commands/sessions.test.js.map +0 -1
@@ -1,3 +1,11 @@
1
+ /**
2
+ * Session discovery across Claude, Codex, Gemini, OpenCode, and OpenClaw.
3
+ *
4
+ * Performs incremental scans: each agent's session files are stat'd and compared
5
+ * to a scan-stamp ledger in SQLite. Only files whose mtime or size changed since
6
+ * the last run are re-parsed. All metadata is upserted into the sessions DB so
7
+ * subsequent queries are served entirely from the cache.
8
+ */
1
9
  import * as fs from 'fs';
2
10
  import * as path from 'path';
3
11
  import * as os from 'os';
@@ -8,79 +16,72 @@ import { AGENTS, getCliVersion } from '../agents.js';
8
16
  import { getConfigSymlinkVersion } from '../shims.js';
9
17
  import { SESSION_AGENTS } from './types.js';
10
18
  import { extractSessionTopic } from './prompt.js';
19
+ import { getDB, getScanStampByPath, getScanStampsForPaths, recordScans, syncLabels, upsertSessionsBatch, querySessions, countSessions, ftsSearch, } from './db.js';
11
20
  const HOME = os.homedir();
12
21
  const AGENTS_DIR = path.join(HOME, '.agents');
13
- const SESSIONS_DIR = path.join(AGENTS_DIR, 'sessions');
14
- const INDEX_PATH = path.join(SESSIONS_DIR, 'index.jsonl');
15
- const CONTENT_INDEX_PATH = path.join(SESSIONS_DIR, 'content_index.jsonl');
22
+ /** How long OpenClaw channel/cron snapshots stay valid before we re-shell-out. */
23
+ const OPENCLAW_TTL_MS = 60_000;
16
24
  let cachedOpenClawWorkspaces = null;
17
25
  const cachedAgentVersions = new Map();
18
26
  /**
19
- * Discover sessions across all installed agents, versions, and backups.
20
- * Merges with a persistent index so sessions survive version removal.
21
- * Returns SessionMeta[] sorted by timestamp descending (most recent first).
27
+ * Discover sessions. Scans only files whose (mtime, size) have changed since
28
+ * the last run; everything else is served from the SQLite cache.
22
29
  */
23
30
  export async function discoverSessions(options) {
31
+ // Touch the DB so the schema is ready and connection is cached for this run.
32
+ getDB();
24
33
  const agents = options?.agent ? [options.agent] : SESSION_AGENTS;
25
- const limit = options?.limit ?? 50;
26
- const results = await Promise.all(agents.map(agent => {
34
+ const onProgress = options?.onProgress;
35
+ // Incrementally re-scan changed files across all selected agents in parallel.
36
+ await Promise.all(agents.map(agent => {
27
37
  switch (agent) {
28
- case 'claude': return discoverClaudeSessions();
29
- case 'codex': return discoverCodexSessions();
30
- case 'gemini': return discoverGeminiSessions();
31
- case 'opencode': return discoverOpenCodeSessions();
32
- case 'openclaw': return discoverOpenClawSessions();
38
+ case 'claude': return scanClaudeIncremental(onProgress);
39
+ case 'codex': return scanCodexIncremental(onProgress);
40
+ case 'gemini': return scanGeminiIncremental(onProgress);
41
+ case 'opencode': return scanOpenCodeIncremental();
42
+ case 'openclaw': return scanOpenClawIncremental();
33
43
  }
34
44
  }));
35
- let sessions = results.flat();
36
- // Merge with persistent index (preserves sessions whose files were removed)
37
- const index = loadIndex();
38
- const liveIds = new Set(sessions.map(s => s.id));
39
- const agentFilter = new Set(agents);
40
- // Add matching index entries to display results
41
- index.forEach((entry, id) => {
42
- if (!liveIds.has(id) && agentFilter.has(entry.agent)) {
43
- sessions.push(entry);
44
- }
45
- });
46
- // Persist: merge live sessions into full index (don't drop unqueried agents)
47
- const toSave = new Map(index);
48
- for (const s of sessions) {
49
- toSave.set(s.id, s);
50
- }
51
- saveIndex([...toSave.values()]);
52
- // Build BM25 content index for all discovered sessions
53
- const bm25 = buildBM25Index(sessions);
54
- saveBM25Index(bm25);
45
+ const sessions = querySessions(buildQueryOptions(options, agents, { includeLimit: true }));
46
+ return sessions;
47
+ }
48
+ /**
49
+ * Count sessions in scope without running an incremental scan. Assumes the DB
50
+ * is already fresh (typically true because `discoverSessions` ran first this
51
+ * turn). Uses the exact same filter shape as the discover query.
52
+ */
53
+ export function countSessionsInScope(options) {
54
+ const agents = options.agent ? [options.agent] : SESSION_AGENTS;
55
+ return countSessions(buildQueryOptions(options, agents, { includeLimit: false }));
56
+ }
57
+ /** Translate DiscoverOptions into the QueryOptions shape expected by the DB layer. */
58
+ function buildQueryOptions(options, agents, opts) {
55
59
  const projectQuery = options?.project?.trim();
56
- // Filter by project (case-insensitive substring match)
57
- if (projectQuery) {
58
- const query = projectQuery.toLowerCase();
59
- sessions = sessions.filter(s => s.project?.toLowerCase().includes(query));
60
- }
61
- // Apply time range filters
62
- if (options?.since || options?.until) {
63
- const sinceMs = options.since ? parseTimeFilter(options.since) : 0;
64
- const untilMs = options.until ? new Date(options.until).getTime() : Infinity;
65
- sessions = sessions.filter(s => {
66
- const ts = new Date(s.timestamp).getTime();
67
- return ts >= sinceMs && ts <= untilMs;
68
- });
60
+ const sinceMs = options?.since ? parseTimeFilter(options.since) : undefined;
61
+ const untilMs = options?.until ? new Date(options.until).getTime() : undefined;
62
+ let cwdFilter;
63
+ let cwdPrefixFilter;
64
+ if (options?.cwdPrefix) {
65
+ cwdPrefixFilter = normalizeCwd(options.cwdPrefix);
69
66
  }
70
- // An explicit project search should scan across directories instead of
71
- // intersecting with the default cwd-only scope.
72
- if (!options?.all && !projectQuery) {
73
- const currentDir = normalizeCwd(options?.cwd || process.cwd());
74
- sessions = sessions.filter(s => normalizeCwd(s.cwd) === currentDir);
67
+ else if (!options?.all && !projectQuery) {
68
+ cwdFilter = normalizeCwd(options?.cwd || process.cwd());
75
69
  }
76
- // Sort by timestamp descending
77
- sessions.sort((a, b) => {
78
- const ta = new Date(a.timestamp).getTime() || 0;
79
- const tb = new Date(b.timestamp).getTime() || 0;
80
- return tb - ta;
81
- });
82
- return sessions.slice(0, limit);
70
+ return {
71
+ agent: options?.agent,
72
+ agents: options?.agent ? undefined : agents,
73
+ version: options?.version,
74
+ cwd: cwdFilter,
75
+ cwdPrefix: cwdPrefixFilter,
76
+ project: projectQuery,
77
+ sinceMs,
78
+ untilMs: Number.isFinite(untilMs) ? untilMs : undefined,
79
+ limit: opts.includeLimit ? (options?.limit ?? 50) : undefined,
80
+ excludeTeamOrigin: options?.excludeTeamOrigin,
81
+ onlyTeamOrigin: options?.onlyTeamOrigin,
82
+ };
83
83
  }
84
+ /** Resolve and canonicalize a working directory path (follows symlinks). */
84
85
  function normalizeCwd(cwd) {
85
86
  if (!cwd)
86
87
  return '';
@@ -88,195 +89,76 @@ function normalizeCwd(cwd) {
88
89
  return safeRealpathSync(resolved) || resolved;
89
90
  }
90
91
  /**
91
- * Resolve a session by full or short ID from the full index.
92
+ * Resolve a session by full or short ID. Accepts a pre-loaded session list
93
+ * (fast path from discoverSessions) and falls back to a DB lookup for the
94
+ * "I only know the id" case.
92
95
  */
93
96
  export function resolveSessionById(sessions, idQuery) {
94
97
  const query = idQuery.toLowerCase();
95
- // Exact match first (full id or shortId)
96
98
  const exact = sessions.filter(s => s.id.toLowerCase() === query || s.shortId.toLowerCase() === query);
97
99
  if (exact.length > 0)
98
100
  return exact;
99
- // Prefix match (against both id and shortId)
100
101
  return sessions.filter(s => s.id.toLowerCase().startsWith(query) || s.shortId.toLowerCase().startsWith(query));
101
102
  }
102
103
  // ---------------------------------------------------------------------------
103
- // Persistent session index
104
+ // Content-index search (FTS5-backed)
104
105
  // ---------------------------------------------------------------------------
105
- function loadIndex() {
106
- const map = new Map();
107
- if (!fs.existsSync(INDEX_PATH))
108
- return map;
109
- try {
110
- const content = fs.readFileSync(INDEX_PATH, 'utf-8');
111
- for (const line of content.split('\n')) {
112
- if (!line.trim())
113
- continue;
114
- try {
115
- const entry = JSON.parse(line);
116
- if (entry.id)
117
- map.set(entry.id, entry);
118
- }
119
- catch { /* malformed index entry, skip */ }
120
- }
121
- }
122
- catch (err) {
123
- console.error(`Warning: Could not load session cache (${err.message}). Rebuilding...`);
124
- }
125
- return map;
126
- }
127
- function saveIndex(sessions) {
128
- try {
129
- fs.mkdirSync(SESSIONS_DIR, { recursive: true });
130
- // Deduplicate by id, keeping the first occurrence (live sessions take priority)
131
- const seen = new Set();
132
- const lines = [];
133
- for (const s of sessions) {
134
- if (seen.has(s.id))
135
- continue;
136
- seen.add(s.id);
137
- lines.push(JSON.stringify(s));
138
- }
139
- fs.writeFileSync(INDEX_PATH, lines.join('\n') + '\n', 'utf-8');
140
- }
141
- catch (err) {
142
- console.error(`Warning: Could not save session cache: ${err.message}`);
106
+ /**
107
+ * Run an FTS5 search over the DB and intersect with the given session list,
108
+ * preserving the existing SessionMeta[] contract so sessions.ts is unchanged.
109
+ */
110
+ export function searchContentIndex(sessions, query) {
111
+ if (!query.trim())
112
+ return new Map();
113
+ const hits = ftsSearch(query);
114
+ if (hits.length === 0)
115
+ return new Map();
116
+ const byId = new Map(sessions.map(s => [s.id, s]));
117
+ const result = new Map();
118
+ for (const hit of hits) {
119
+ const session = byId.get(hit.sessionId);
120
+ if (!session)
121
+ continue;
122
+ result.set(hit.sessionId, {
123
+ ...session,
124
+ _matchedTerms: hit.matchedTerms,
125
+ _bm25Score: hit.score,
126
+ });
143
127
  }
128
+ return result;
144
129
  }
145
130
  // ---------------------------------------------------------------------------
146
- // BM25 content index
131
+ // Incremental scan orchestration
147
132
  // ---------------------------------------------------------------------------
148
- const BM25_K1 = 1.2;
149
- const BM25_B = 0.75;
150
- const BM25_INDEX_VERSION = 2;
151
- function tokenizeCounted(text) {
152
- const counts = new Map();
153
- let length = 0;
154
- const raw = text.toLowerCase().split(/[^a-z0-9]+/);
155
- for (const token of raw) {
156
- if (token.length < 2)
133
+ /**
134
+ * For a list of files, stat each, compare to the DB ledger, and return only
135
+ * the ones that need rescanning. One bulk DB query for the whole list.
136
+ */
137
+ function filterChangedFiles(filePaths) {
138
+ const ledger = getScanStampsForPaths(filePaths);
139
+ const out = [];
140
+ for (const filePath of filePaths) {
141
+ const stat = safeStatSync(filePath);
142
+ if (!stat)
157
143
  continue;
158
- length++;
159
- counts.set(token, (counts.get(token) ?? 0) + 1);
160
- }
161
- return { length, counts };
162
- }
163
- function collectSessionText(s) {
164
- const parts = [];
165
- if (s.topic)
166
- parts.push(s.topic);
167
- if (s.project)
168
- parts.push(s.project);
169
- if (s.cwd)
170
- parts.push(s.cwd);
171
- if (s.gitBranch)
172
- parts.push(s.gitBranch);
173
- if (s.account)
174
- parts.push(s.account);
175
- if (s._userTerms)
176
- parts.push(s._userTerms.join('\n'));
177
- return parts.join('\n');
178
- }
179
- export function buildBM25Index(sessions) {
180
- const docLengths = new Map();
181
- const postings = new Map();
182
- let totalLength = 0;
183
- for (const session of sessions) {
184
- const { length, counts } = tokenizeCounted(collectSessionText(session));
185
- docLengths.set(session.id, length);
186
- totalLength += length;
187
- for (const [term, tf] of counts) {
188
- let termPostings = postings.get(term);
189
- if (!termPostings) {
190
- termPostings = new Map();
191
- postings.set(term, termPostings);
192
- }
193
- termPostings.set(session.id, tf);
194
- }
195
- }
196
- const N = sessions.length;
197
- const avgdl = N > 0 ? totalLength / N : 0;
198
- return { N, avgdl, docLengths, postings };
199
- }
200
- function saveBM25Index(index) {
201
- try {
202
- fs.mkdirSync(SESSIONS_DIR, { recursive: true });
203
- const lines = [];
204
- lines.push(JSON.stringify({ v: BM25_INDEX_VERSION, N: index.N, avgdl: index.avgdl }));
205
- for (const [sid, len] of index.docLengths) {
206
- lines.push(JSON.stringify({ d: sid, l: len }));
207
- }
208
- for (const [term, termPostings] of index.postings) {
209
- const p = [];
210
- for (const [sid, tf] of termPostings)
211
- p.push([sid, tf]);
212
- lines.push(JSON.stringify({ t: term, p }));
213
- }
214
- fs.writeFileSync(CONTENT_INDEX_PATH, lines.join('\n') + '\n', 'utf-8');
215
- }
216
- catch { /* non-fatal */ }
217
- }
218
- function loadBM25Index() {
219
- if (!fs.existsSync(CONTENT_INDEX_PATH))
220
- return null;
221
- let content;
222
- try {
223
- content = fs.readFileSync(CONTENT_INDEX_PATH, 'utf-8');
224
- }
225
- catch {
226
- return null;
227
- }
228
- const lines = content.split('\n').filter(l => l.trim());
229
- if (lines.length === 0)
230
- return null;
231
- let header;
232
- try {
233
- header = JSON.parse(lines[0]);
234
- }
235
- catch {
236
- return null;
237
- }
238
- if (header?.v !== BM25_INDEX_VERSION)
239
- return null;
240
- const index = {
241
- N: Number(header.N) || 0,
242
- avgdl: Number(header.avgdl) || 0,
243
- docLengths: new Map(),
244
- postings: new Map(),
245
- };
246
- for (let i = 1; i < lines.length; i++) {
247
- let entry;
248
- try {
249
- entry = JSON.parse(lines[i]);
250
- }
251
- catch {
144
+ const scan = {
145
+ fileMtimeMs: Math.floor(stat.mtimeMs),
146
+ fileSize: stat.size,
147
+ };
148
+ const prev = ledger.get(filePath);
149
+ if (prev && prev.fileMtimeMs === scan.fileMtimeMs && prev.fileSize === scan.fileSize) {
252
150
  continue;
253
151
  }
254
- if (typeof entry.d === 'string' && typeof entry.l === 'number') {
255
- index.docLengths.set(entry.d, entry.l);
256
- }
257
- else if (typeof entry.t === 'string' && Array.isArray(entry.p)) {
258
- const termPostings = new Map();
259
- for (const pair of entry.p) {
260
- if (Array.isArray(pair) && pair.length >= 2) {
261
- termPostings.set(String(pair[0]), Number(pair[1]));
262
- }
263
- }
264
- index.postings.set(entry.t, termPostings);
265
- }
152
+ out.push({ filePath, scan });
266
153
  }
267
- return index;
154
+ return out;
268
155
  }
269
156
  // ---------------------------------------------------------------------------
270
157
  // Multi-version directory scanning
271
158
  // ---------------------------------------------------------------------------
272
159
  /**
273
- * Collect all directories to scan for an agent's sessions.
274
- * Scans: active config dir, all installed version homes, and backups.
275
- * Deduplicates by realpath to avoid double-counting the active symlink.
276
- *
277
- * @param agent - Agent name (claude, codex, gemini)
278
- * @param subdir - Subdirectory within the agent's config dir where sessions live
279
- * (e.g., 'projects' for Claude, 'sessions' for Codex, 'tmp' for Gemini)
160
+ * Collect all directories to scan for an agent's sessions. Deduplicates by
161
+ * realpath to avoid double-counting symlinked version homes.
280
162
  */
281
163
  export function getAgentSessionDirs(agent, subdir) {
282
164
  const resolved = new Set();
@@ -291,9 +173,7 @@ export function getAgentSessionDirs(agent, subdir) {
291
173
  resolved.add(key);
292
174
  dirs.push(dir);
293
175
  }
294
- // 1. Active config (may be a symlink to the current version's home)
295
176
  addDir(path.join(HOME, `.${agent}`, subdir));
296
- // 2. All installed version homes
297
177
  const versionsBase = path.join(AGENTS_DIR, 'versions', agent);
298
178
  if (fs.existsSync(versionsBase)) {
299
179
  try {
@@ -301,9 +181,8 @@ export function getAgentSessionDirs(agent, subdir) {
301
181
  addDir(path.join(versionsBase, version, 'home', `.${agent}`, subdir));
302
182
  }
303
183
  }
304
- catch { /* dir unreadable or missing */ }
184
+ catch { /* dir unreadable */ }
305
185
  }
306
- // 3. Backups (from before version management was enabled)
307
186
  const backupsBase = path.join(AGENTS_DIR, 'backups', agent);
308
187
  if (fs.existsSync(backupsBase)) {
309
188
  try {
@@ -311,7 +190,7 @@ export function getAgentSessionDirs(agent, subdir) {
311
190
  addDir(path.join(backupsBase, ts, subdir));
312
191
  }
313
192
  }
314
- catch { /* dir unreadable or missing */ }
193
+ catch { /* dir unreadable */ }
315
194
  }
316
195
  return dirs;
317
196
  }
@@ -319,18 +198,22 @@ export function getAgentSessionDirs(agent, subdir) {
319
198
  // Claude account info
320
199
  // ---------------------------------------------------------------------------
321
200
  let cachedClaudeAccount;
201
+ /** Read the Claude OAuth account email from .claude.json across all version homes. */
322
202
  function getClaudeAccount() {
323
203
  if (cachedClaudeAccount !== undefined)
324
204
  return cachedClaudeAccount || undefined;
325
- // Check all possible locations for .claude.json
205
+ // Claude's active config lives at $CLAUDE_CONFIG_DIR/.claude.json; for our shim
206
+ // that's <version>/home/.claude/.claude.json. The home-level .claude.json is a
207
+ // legacy path used when Claude runs without CLAUDE_CONFIG_DIR set.
326
208
  const candidates = [
209
+ path.join(HOME, '.claude', '.claude.json'),
327
210
  path.join(HOME, '.claude.json'),
328
211
  ];
329
- // Also check version homes (auth files are symlinked there)
330
212
  const versionsBase = path.join(AGENTS_DIR, 'versions', 'claude');
331
213
  if (fs.existsSync(versionsBase)) {
332
214
  try {
333
215
  for (const version of fs.readdirSync(versionsBase)) {
216
+ candidates.push(path.join(versionsBase, version, 'home', '.claude', '.claude.json'));
334
217
  candidates.push(path.join(versionsBase, version, 'home', '.claude.json'));
335
218
  }
336
219
  }
@@ -355,11 +238,50 @@ function getClaudeAccount() {
355
238
  // ---------------------------------------------------------------------------
356
239
  // Claude
357
240
  // ---------------------------------------------------------------------------
358
- async function discoverClaudeSessions() {
359
- const sessions = [];
360
- const seen = new Set();
241
+ /**
242
+ * Build a map of Claude sessionId -> user-given label from ~/.claude/sessions/*.json.
243
+ * Each JSON has shape { pid, sessionId, cwd, startedAt, name?, ... }. The
244
+ * `name` field only exists if the user ran /rename in that session.
245
+ * For sessionId collisions (re-resume of the same session), prefer the most
246
+ * recent startedAt.
247
+ */
248
+ function buildClaudeLabelMap() {
249
+ const map = new Map();
250
+ const dir = path.join(HOME, '.claude', 'sessions');
251
+ if (!fs.existsSync(dir))
252
+ return new Map();
253
+ let files;
254
+ try {
255
+ files = fs.readdirSync(dir).filter(f => f.endsWith('.json'));
256
+ }
257
+ catch {
258
+ return new Map();
259
+ }
260
+ for (const f of files) {
261
+ try {
262
+ const data = JSON.parse(fs.readFileSync(path.join(dir, f), 'utf-8'));
263
+ if (typeof data.sessionId !== 'string')
264
+ continue;
265
+ const name = typeof data.name === 'string' && data.name.trim() ? data.name.trim() : null;
266
+ const startedAt = typeof data.startedAt === 'number' ? data.startedAt : 0;
267
+ const existing = map.get(data.sessionId);
268
+ if (!existing || startedAt > existing.startedAt) {
269
+ map.set(data.sessionId, { label: name, startedAt });
270
+ }
271
+ }
272
+ catch { /* unreadable session metadata file */ }
273
+ }
274
+ const out = new Map();
275
+ for (const [sid, { label }] of map)
276
+ out.set(sid, label);
277
+ return out;
278
+ }
279
+ /** Incrementally re-scan changed Claude session files and upsert into the DB. */
280
+ async function scanClaudeIncremental(onProgress) {
361
281
  const account = getClaudeAccount();
362
- let skipped = 0;
282
+ const labelMap = buildClaudeLabelMap();
283
+ const filePaths = [];
284
+ const seen = new Set();
363
285
  for (const projectsDir of getAgentSessionDirs('claude', 'projects')) {
364
286
  let projectDirs;
365
287
  try {
@@ -385,28 +307,50 @@ async function discoverClaudeSessions() {
385
307
  if (seen.has(sessionId))
386
308
  continue;
387
309
  seen.add(sessionId);
388
- const filePath = path.join(dirPath, file);
389
- try {
390
- const meta = await readClaudeMeta(filePath, sessionId, account);
391
- if (meta)
392
- sessions.push(meta);
310
+ filePaths.push(path.join(dirPath, file));
311
+ }
312
+ }
313
+ }
314
+ const changed = filterChangedFiles(filePaths);
315
+ if (changed.length > 0) {
316
+ onProgress?.({ agent: 'claude', parsed: 0, total: changed.length });
317
+ const entries = [];
318
+ const touched = [];
319
+ let parsed = 0;
320
+ for (const { filePath, scan } of changed) {
321
+ try {
322
+ const sessionId = path.basename(filePath).replace('.jsonl', '');
323
+ const label = labelMap.get(sessionId) ?? undefined;
324
+ const result = await readClaudeMeta(filePath, sessionId, account, label);
325
+ if (result) {
326
+ entries.push({ meta: result.meta, content: result.content, scan });
393
327
  }
394
- catch {
395
- skipped++;
328
+ else {
329
+ touched.push({ filePath, scan });
396
330
  }
397
331
  }
332
+ catch {
333
+ touched.push({ filePath, scan });
334
+ }
335
+ parsed++;
336
+ onProgress?.({ agent: 'claude', parsed, total: changed.length });
398
337
  }
338
+ upsertSessionsBatch(entries);
339
+ recordScans(touched);
399
340
  }
400
- if (skipped > 0 && process.env.AGENTS_DEBUG) {
401
- console.error(`[debug] Skipped ${skipped} unreadable Claude session(s)`);
402
- }
403
- return sessions;
341
+ // Pick up /rename changes on sessions whose JSONL didn't change.
342
+ // Only bother for sessions we actually have a Claude row for.
343
+ if (labelMap.size > 0)
344
+ syncLabels(labelMap);
404
345
  }
405
- async function readClaudeMeta(filePath, sessionId, account) {
346
+ /** Stream-parse a single Claude JSONL file to extract session metadata. */
347
+ async function readClaudeMeta(filePath, sessionId, account, label) {
406
348
  const scan = await scanClaudeSession(filePath);
349
+ const isTeamOrigin = scan.entrypoint === 'sdk-cli';
350
+ let meta;
407
351
  if (scan.timestamp) {
408
- const cwd = scan.cwd || '';
409
- return {
352
+ const cwd = normalizeCwd(scan.cwd || '');
353
+ meta = {
410
354
  id: sessionId,
411
355
  shortId: sessionId.slice(0, 8),
412
356
  agent: 'claude',
@@ -418,37 +362,39 @@ async function readClaudeMeta(filePath, sessionId, account) {
418
362
  version: scan.version,
419
363
  account,
420
364
  topic: scan.topic,
365
+ label,
421
366
  messageCount: scan.messageCount,
422
367
  tokenCount: scan.tokenCount,
423
- _userTerms: scan.userTerms?.flatMap(splitLines),
368
+ isTeamOrigin,
424
369
  };
425
370
  }
426
- // Fallback: use file mtime
427
- const stat = safeStatSync(filePath);
428
- return {
429
- id: sessionId,
430
- shortId: sessionId.slice(0, 8),
431
- agent: 'claude',
432
- timestamp: stat ? stat.mtime.toISOString() : new Date().toISOString(),
433
- filePath,
434
- account,
435
- messageCount: scan.messageCount,
436
- tokenCount: scan.tokenCount,
437
- topic: scan.topic,
438
- _userTerms: scan.userTerms?.flatMap(splitLines),
439
- };
371
+ else {
372
+ const stat = safeStatSync(filePath);
373
+ meta = {
374
+ id: sessionId,
375
+ shortId: sessionId.slice(0, 8),
376
+ agent: 'claude',
377
+ timestamp: stat ? stat.mtime.toISOString() : new Date().toISOString(),
378
+ filePath,
379
+ account,
380
+ label,
381
+ messageCount: scan.messageCount,
382
+ tokenCount: scan.tokenCount,
383
+ topic: scan.topic,
384
+ isTeamOrigin,
385
+ };
386
+ }
387
+ return { meta, content: scan.contentText || '' };
440
388
  }
441
389
  // ---------------------------------------------------------------------------
442
390
  // Codex account info
443
391
  // ---------------------------------------------------------------------------
444
392
  let cachedCodexAccount;
393
+ /** Extract the Codex account email from the JWT id_token in auth.json. */
445
394
  function getCodexAccount() {
446
395
  if (cachedCodexAccount !== undefined)
447
396
  return cachedCodexAccount || undefined;
448
- const candidates = [
449
- path.join(HOME, '.codex', 'auth.json'),
450
- ];
451
- // Also check version homes
397
+ const candidates = [path.join(HOME, '.codex', 'auth.json')];
452
398
  const versionsBase = path.join(AGENTS_DIR, 'versions', 'codex');
453
399
  if (fs.existsSync(versionsBase)) {
454
400
  try {
@@ -463,7 +409,6 @@ function getCodexAccount() {
463
409
  if (!fs.existsSync(candidate))
464
410
  continue;
465
411
  const data = JSON.parse(fs.readFileSync(candidate, 'utf-8'));
466
- // Extract email from JWT id_token payload
467
412
  const idToken = data.tokens?.id_token;
468
413
  if (idToken) {
469
414
  const parts = idToken.split('.');
@@ -484,39 +429,53 @@ function getCodexAccount() {
484
429
  // ---------------------------------------------------------------------------
485
430
  // Codex
486
431
  // ---------------------------------------------------------------------------
487
- async function discoverCodexSessions() {
488
- const sessions = [];
489
- const seen = new Set();
432
+ /** Incrementally re-scan changed Codex session files and upsert into the DB. */
433
+ async function scanCodexIncremental(onProgress) {
490
434
  const account = getCodexAccount();
491
435
  const currentVersion = await getCurrentAgentVersion('codex');
492
- let skipped = 0;
436
+ const filePaths = [];
493
437
  for (const sessionsDir of getAgentSessionDirs('codex', 'sessions')) {
494
- const jsonlFiles = walkForFiles(sessionsDir, '.jsonl', 200);
495
- for (const filePath of jsonlFiles) {
496
- try {
497
- const meta = await readCodexMeta(filePath, account, currentVersion);
498
- if (meta && !seen.has(meta.id)) {
499
- seen.add(meta.id);
500
- sessions.push(meta);
501
- }
438
+ // High limit: we only stat files here, parsing is gated by ledger match.
439
+ for (const fp of walkForFiles(sessionsDir, '.jsonl', 100_000)) {
440
+ filePaths.push(fp);
441
+ }
442
+ }
443
+ const changed = filterChangedFiles(filePaths);
444
+ if (changed.length === 0)
445
+ return;
446
+ onProgress?.({ agent: 'codex', parsed: 0, total: changed.length });
447
+ const entries = [];
448
+ const touched = [];
449
+ const seen = new Set();
450
+ let parsed = 0;
451
+ for (const { filePath, scan } of changed) {
452
+ try {
453
+ const result = await readCodexMeta(filePath, account, currentVersion);
454
+ if (result && !seen.has(result.meta.id)) {
455
+ seen.add(result.meta.id);
456
+ entries.push({ meta: result.meta, content: result.content, scan });
502
457
  }
503
- catch {
504
- skipped++;
458
+ else {
459
+ touched.push({ filePath, scan });
505
460
  }
506
461
  }
462
+ catch {
463
+ touched.push({ filePath, scan });
464
+ }
465
+ parsed++;
466
+ onProgress?.({ agent: 'codex', parsed, total: changed.length });
507
467
  }
508
- if (skipped > 0 && process.env.AGENTS_DEBUG) {
509
- console.error(`[debug] Skipped ${skipped} unreadable Codex session(s)`);
510
- }
511
- return sessions;
468
+ upsertSessionsBatch(entries);
469
+ recordScans(touched);
512
470
  }
471
+ /** Stream-parse a single Codex JSONL file to extract session metadata. */
513
472
  async function readCodexMeta(filePath, account, currentVersion) {
514
473
  const scan = await scanCodexSession(filePath);
515
474
  const sessionId = scan.sessionId || '';
516
475
  if (!sessionId)
517
476
  return null;
518
- const cwd = scan.cwd || '';
519
- return {
477
+ const cwd = normalizeCwd(scan.cwd || '');
478
+ const meta = {
520
479
  id: sessionId,
521
480
  shortId: sessionId.slice(0, 8),
522
481
  agent: 'codex',
@@ -530,18 +489,17 @@ async function readCodexMeta(filePath, account, currentVersion) {
530
489
  messageCount: scan.messageCount,
531
490
  tokenCount: scan.tokenCount,
532
491
  account,
533
- _userTerms: scan.userTerms?.flatMap(splitLines),
534
492
  };
493
+ return { meta, content: scan.contentText || '' };
535
494
  }
536
495
  // ---------------------------------------------------------------------------
537
496
  // Gemini
538
497
  // ---------------------------------------------------------------------------
539
- async function discoverGeminiSessions() {
540
- const projectMap = buildGeminiProjectMap();
541
- const sessions = [];
542
- const seen = new Set();
498
+ /** Incrementally re-scan changed Gemini session files and upsert into the DB. */
499
+ async function scanGeminiIncremental(onProgress) {
543
500
  const currentVersion = await getCurrentAgentVersion('gemini');
544
- let skipped = 0;
501
+ const projectMap = buildGeminiProjectMap();
502
+ const filePaths = [];
545
503
  for (const tmpDir of getAgentSessionDirs('gemini', 'tmp')) {
546
504
  let hashDirs;
547
505
  try {
@@ -562,25 +520,44 @@ async function discoverGeminiSessions() {
562
520
  continue;
563
521
  }
564
522
  for (const file of chatFiles) {
565
- const filePath = path.join(chatsDir, file);
566
- try {
567
- const meta = readGeminiMeta(filePath, hashDir, projectMap, currentVersion);
568
- if (meta && !seen.has(meta.id)) {
569
- seen.add(meta.id);
570
- sessions.push(meta);
571
- }
572
- }
573
- catch {
574
- skipped++;
575
- }
523
+ filePaths.push({ filePath: path.join(chatsDir, file), hashDir });
576
524
  }
577
525
  }
578
526
  }
579
- if (skipped > 0 && process.env.AGENTS_DEBUG) {
580
- console.error(`[debug] Skipped ${skipped} unreadable Gemini session(s)`);
527
+ const changedPaths = filterChangedFiles(filePaths.map(f => f.filePath));
528
+ const changedByPath = new Map(changedPaths.map(c => [c.filePath, c.scan]));
529
+ if (changedByPath.size === 0)
530
+ return;
531
+ onProgress?.({ agent: 'gemini', parsed: 0, total: changedByPath.size });
532
+ const entries = [];
533
+ const touched = [];
534
+ const seen = new Set();
535
+ let parsed = 0;
536
+ for (const { filePath, hashDir } of filePaths) {
537
+ const scan = changedByPath.get(filePath);
538
+ if (!scan)
539
+ continue;
540
+ try {
541
+ const result = readGeminiMeta(filePath, hashDir, projectMap, currentVersion);
542
+ if (result && !seen.has(result.meta.id)) {
543
+ seen.add(result.meta.id);
544
+ entries.push({ meta: result.meta, content: result.content, scan });
545
+ }
546
+ else {
547
+ // Gemini file without a sessionId — record scan so we don't re-parse it next run.
548
+ touched.push({ filePath, scan });
549
+ }
550
+ }
551
+ catch {
552
+ touched.push({ filePath, scan });
553
+ }
554
+ parsed++;
555
+ onProgress?.({ agent: 'gemini', parsed, total: changedByPath.size });
581
556
  }
582
- return sessions;
557
+ upsertSessionsBatch(entries);
558
+ recordScans(touched);
583
559
  }
560
+ /** Parse a single Gemini JSON session file to extract session metadata. */
584
561
  function readGeminiMeta(filePath, hashDir, projectMap, currentVersion) {
585
562
  let session;
586
563
  try {
@@ -599,23 +576,22 @@ function readGeminiMeta(filePath, hashDir, projectMap, currentVersion) {
599
576
  : undefined;
600
577
  if (!sessionId)
601
578
  return null;
602
- // Resolve project name from hash
603
579
  const projectInfo = projectMap.get(projectHash || hashDir);
604
580
  const project = projectInfo?.name || hashDir.slice(0, 12);
605
- const cwd = projectInfo?.path;
581
+ const cwd = projectInfo?.path ? normalizeCwd(projectInfo.path) : undefined;
606
582
  const stat = safeStatSync(filePath);
607
583
  const messages = Array.isArray(session.messages) ? session.messages : [];
608
584
  let topic;
609
585
  let messageCount = 0;
610
586
  let tokenCount = 0;
611
587
  let sawTokenCount = false;
612
- const userTerms = [];
588
+ const userTexts = [];
613
589
  for (const message of messages) {
614
590
  if (message.type === 'user') {
615
591
  const text = extractGeminiMessageText(message.content);
616
592
  if (text) {
617
593
  messageCount++;
618
- userTerms.push(text);
594
+ userTexts.push(text);
619
595
  if (!topic)
620
596
  topic = extractSessionTopic(text);
621
597
  }
@@ -631,7 +607,7 @@ function readGeminiMeta(filePath, hashDir, projectMap, currentVersion) {
631
607
  sawTokenCount = true;
632
608
  }
633
609
  }
634
- return {
610
+ const meta = {
635
611
  id: sessionId,
636
612
  shortId: sessionId.slice(0, 8),
637
613
  agent: 'gemini',
@@ -643,40 +619,37 @@ function readGeminiMeta(filePath, hashDir, projectMap, currentVersion) {
643
619
  topic,
644
620
  messageCount,
645
621
  tokenCount: sawTokenCount ? tokenCount : undefined,
646
- _userTerms: userTerms.length > 0 ? userTerms : undefined,
647
622
  };
623
+ return { meta, content: userTexts.join('\n') };
648
624
  }
625
+ /** Build a hash-to-project mapping from Gemini's projects.json and history directories. */
649
626
  function buildGeminiProjectMap() {
650
627
  const map = new Map();
651
628
  const projectsJsonPath = path.join(HOME, '.gemini', 'projects.json');
652
- if (!fs.existsSync(projectsJsonPath))
653
- return map;
654
- try {
655
- const data = JSON.parse(fs.readFileSync(projectsJsonPath, 'utf-8'));
656
- const projects = data.projects;
657
- if (typeof projects === 'object' && projects !== null) {
658
- if (Array.isArray(projects)) {
659
- // Array format: ["path1", "path2"]
660
- for (const p of projects) {
661
- if (typeof p === 'string') {
662
- const hash = sha256(p);
663
- map.set(hash, { name: path.basename(p), path: p });
664
- // Also try the raw directory name
665
- map.set(p, { name: path.basename(p), path: p });
629
+ if (fs.existsSync(projectsJsonPath)) {
630
+ try {
631
+ const data = JSON.parse(fs.readFileSync(projectsJsonPath, 'utf-8'));
632
+ const projects = data.projects;
633
+ if (typeof projects === 'object' && projects !== null) {
634
+ if (Array.isArray(projects)) {
635
+ for (const p of projects) {
636
+ if (typeof p === 'string') {
637
+ const hash = sha256(p);
638
+ map.set(hash, { name: path.basename(p), path: p });
639
+ map.set(p, { name: path.basename(p), path: p });
640
+ }
666
641
  }
667
642
  }
668
- }
669
- else {
670
- // Object format: {path: name}
671
- for (const [p, name] of Object.entries(projects)) {
672
- const hash = sha256(p);
673
- map.set(hash, { name: String(name), path: p });
643
+ else {
644
+ for (const [p, name] of Object.entries(projects)) {
645
+ const hash = sha256(p);
646
+ map.set(hash, { name: String(name), path: p });
647
+ }
674
648
  }
675
649
  }
676
650
  }
651
+ catch { /* projects.json missing or malformed */ }
677
652
  }
678
- catch { /* projects.json missing or malformed */ }
679
- // Also check ~/.gemini/history/*/.project_root for additional mappings
680
653
  const historyDir = path.join(HOME, '.gemini', 'history');
681
654
  if (fs.existsSync(historyDir)) {
682
655
  try {
@@ -703,10 +676,10 @@ function buildGeminiProjectMap() {
703
676
  // ---------------------------------------------------------------------------
704
677
  const OPENCODE_DB = path.join(HOME, '.local', 'share', 'opencode', 'opencode.db');
705
678
  let cachedOpenCodeAccount;
679
+ /** Query the active OpenCode account email from its SQLite database. */
706
680
  function getOpenCodeAccount() {
707
681
  if (cachedOpenCodeAccount !== undefined)
708
682
  return cachedOpenCodeAccount || undefined;
709
- // Try control_account table in the DB
710
683
  try {
711
684
  if (fs.existsSync(OPENCODE_DB)) {
712
685
  const out = execSync(`sqlite3 "${OPENCODE_DB}" "SELECT email FROM control_account WHERE active=1 LIMIT 1;"`, { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
@@ -720,14 +693,26 @@ function getOpenCodeAccount() {
720
693
  cachedOpenCodeAccount = '';
721
694
  return undefined;
722
695
  }
723
- async function discoverOpenCodeSessions() {
696
+ /** Scan OpenCode sessions from its SQLite database when the DB file has changed. */
697
+ async function scanOpenCodeIncremental() {
724
698
  if (!fs.existsSync(OPENCODE_DB))
725
- return [];
699
+ return;
700
+ const stat = safeStatSync(OPENCODE_DB);
701
+ if (!stat)
702
+ return;
703
+ // OpenCode is one big DB; we use its mtime/size as the ledger for the
704
+ // entire fleet of OpenCode sessions.
705
+ const currentScan = {
706
+ fileMtimeMs: Math.floor(stat.mtimeMs),
707
+ fileSize: stat.size,
708
+ };
709
+ const prev = getScanStampByPath(OPENCODE_DB);
710
+ if (prev && prev.fileMtimeMs === currentScan.fileMtimeMs && prev.fileSize === currentScan.fileSize) {
711
+ return;
712
+ }
726
713
  const account = getOpenCodeAccount();
727
714
  const currentVersion = await getCurrentAgentVersion('opencode');
728
715
  try {
729
- // Query sessions. time_created is millisecond epoch. Limit to 200 most recent.
730
- // Use session.title as topic (OpenCode auto-generates good titles).
731
716
  const query = `
732
717
  SELECT
733
718
  s.id,
@@ -756,10 +741,10 @@ async function discoverOpenCodeSessions() {
756
741
  ) stats ON stats.session_id = s.id
757
742
  WHERE s.parent_id IS NULL
758
743
  ORDER BY time_created DESC
759
- LIMIT 200;
744
+ LIMIT 1000;
760
745
  `.replace(/\n/g, ' ');
761
746
  const out = execSync(`sqlite3 -separator '|||' "${OPENCODE_DB}"`, { encoding: 'utf-8', input: query, stdio: ['pipe', 'pipe', 'ignore'], timeout: 5000 });
762
- const sessions = [];
747
+ const entries = [];
763
748
  for (const line of out.split('\n')) {
764
749
  if (!line.trim())
765
750
  continue;
@@ -772,118 +757,128 @@ async function discoverOpenCodeSessions() {
772
757
  const hasTokenData = hasTokenDataStr === '1';
773
758
  const timestamp = isNaN(timeCreated) ? new Date().toISOString() : new Date(timeCreated).toISOString();
774
759
  const topic = title || undefined;
775
- sessions.push({
760
+ const meta = {
776
761
  id,
777
762
  shortId: id.replace(/^ses_/, '').slice(0, 8),
778
763
  agent: 'opencode',
779
764
  timestamp,
780
765
  project: directory ? path.basename(directory) : undefined,
781
- cwd: directory || undefined,
766
+ cwd: directory ? normalizeCwd(directory) : undefined,
782
767
  filePath: `${OPENCODE_DB}#${id}`,
783
768
  version: resolveSessionVersion('opencode', OPENCODE_DB, version || undefined, currentVersion),
784
769
  account,
785
770
  topic,
786
771
  messageCount: Number.isNaN(messageCount) ? undefined : messageCount,
787
772
  tokenCount: hasTokenData && !Number.isNaN(tokenCount) ? tokenCount : undefined,
788
- });
773
+ };
774
+ entries.push({ meta, content: topic || '', scan: currentScan });
789
775
  }
790
- return sessions;
776
+ upsertSessionsBatch(entries);
777
+ // Stamp the OpenCode DB itself so we can short-circuit on the next run.
778
+ recordScans([{ filePath: OPENCODE_DB, scan: currentScan }]);
791
779
  }
792
780
  catch (err) {
793
781
  if (process.stderr.isTTY) {
794
782
  console.error(`Warning: Could not query OpenCode sessions: ${err.message}`);
795
783
  }
796
- return [];
797
784
  }
798
785
  }
799
786
  // ---------------------------------------------------------------------------
800
787
  // OpenClaw
801
788
  // ---------------------------------------------------------------------------
802
- async function discoverOpenClawSessions() {
803
- const sessions = [];
804
- // Check if openclaw is installed
789
+ /** Scan active OpenClaw channels and cron jobs via the openclaw CLI. */
790
+ async function scanOpenClawIncremental() {
791
+ // Check if openclaw is installed — silently skip if not.
805
792
  try {
806
793
  execSync('which openclaw', { stdio: 'ignore' });
807
794
  }
808
795
  catch {
809
- return sessions;
796
+ return;
797
+ }
798
+ // TTL cache: skip subprocess calls if we scanned recently. Stored in the
799
+ // meta table so we skip even when no channels/cron exist to produce rows.
800
+ const db = getDB();
801
+ const row = db.prepare(`SELECT value FROM meta WHERE key = 'openclaw_last_scan_ms'`).get();
802
+ const lastScanMs = row ? parseInt(row.value, 10) : 0;
803
+ if (lastScanMs && Date.now() - lastScanMs < OPENCLAW_TTL_MS) {
804
+ return;
810
805
  }
811
806
  const currentVersion = await getCurrentAgentVersion('openclaw');
812
- // Discover active channels
813
- // Format: "- Telegram default (Jeff): enabled, configured, running, out:2h ago, mode:polling, token:config"
807
+ const now = Date.now();
808
+ const scan = { fileMtimeMs: now, fileSize: 0 };
809
+ const entries = [];
814
810
  try {
815
811
  const output = execSync('openclaw channels status', {
816
812
  encoding: 'utf-8',
817
813
  stdio: ['ignore', 'pipe', 'ignore'],
818
814
  });
819
815
  for (const line of output.split('\n')) {
820
- // Match: "- Telegram <agentId> (<Name>): ..., running, ..."
821
816
  const match = line.match(/^-\s+\w+\s+(\S+)\s+\((\w+)\):\s*(.+)/);
822
817
  if (!match)
823
818
  continue;
824
819
  const [, agentId, name, statusStr] = match;
825
- const isRunning = statusStr.includes('running');
826
- if (!isRunning)
820
+ if (!statusStr.includes('running'))
827
821
  continue;
828
- sessions.push({
829
- id: `openclaw-${agentId}`,
830
- shortId: agentId.slice(0, 8),
831
- agent: 'openclaw',
832
- timestamp: new Date().toISOString(),
833
- project: name,
834
- cwd: getOpenClawSessionCwd(agentId),
835
- version: currentVersion,
836
- filePath: '',
822
+ entries.push({
823
+ meta: {
824
+ id: `openclaw-${agentId}`,
825
+ shortId: agentId.slice(0, 8),
826
+ agent: 'openclaw',
827
+ timestamp: new Date().toISOString(),
828
+ project: name,
829
+ cwd: getOpenClawSessionCwd(agentId),
830
+ version: currentVersion,
831
+ filePath: '',
832
+ },
833
+ content: `${name} ${agentId}`,
834
+ scan,
837
835
  });
838
836
  }
839
837
  }
840
838
  catch {
841
- // Command failed or not available
839
+ /* channels command failed */
842
840
  }
843
- // Discover cron jobs
844
- // Output format (fixed-width columns, 1 space between UUID and name):
845
- // 6ec2cffe-39f8-480b-821f-0b20a2062550 paul-hourly cron */30 ... in 7h 48m ago ok isolated paul -
846
- // UUID is always 36 chars. Extract it first, then parse the rest.
847
841
  try {
848
842
  const output = execSync('openclaw cron list', {
849
843
  encoding: 'utf-8',
850
844
  stdio: ['ignore', 'pipe', 'ignore'],
851
845
  });
852
846
  const lines = output.split('\n');
853
- // Skip header row
854
847
  for (let i = 1; i < lines.length; i++) {
855
848
  const line = lines[i].trim();
856
849
  if (!line)
857
850
  continue;
858
- // Extract UUID (36 chars) and name from start of line
859
851
  const headMatch = line.match(/^([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\s+(\S+)/);
860
852
  if (!headMatch)
861
853
  continue;
862
854
  const jobId = headMatch[1];
863
855
  const jobName = headMatch[2];
864
- // Parse remaining columns (2+ whitespace separated)
865
- // Schedule+Next merge (cron expressions have internal spaces), so cols are:
866
- // [schedule+next, last, status, target, agentId, model]
867
856
  const rest = line.slice(headMatch[0].length).trim();
868
857
  const cols = rest.split(/\s{2,}/);
869
858
  const agentId = cols[4] || '';
870
- sessions.push({
871
- id: `openclaw-cron-${jobId}`,
872
- shortId: jobId.slice(0, 8),
873
- agent: 'openclaw',
874
- timestamp: new Date().toISOString(),
875
- project: `${jobName} (${agentId || 'unknown'})`,
876
- cwd: getOpenClawSessionCwd(agentId),
877
- version: currentVersion,
878
- filePath: '',
859
+ entries.push({
860
+ meta: {
861
+ id: `openclaw-cron-${jobId}`,
862
+ shortId: jobId.slice(0, 8),
863
+ agent: 'openclaw',
864
+ timestamp: new Date().toISOString(),
865
+ project: `${jobName} (${agentId || 'unknown'})`,
866
+ cwd: getOpenClawSessionCwd(agentId),
867
+ version: currentVersion,
868
+ filePath: '',
869
+ },
870
+ content: `${jobName} ${agentId}`,
871
+ scan,
879
872
  });
880
873
  }
881
874
  }
882
875
  catch {
883
- // Command failed or not available
876
+ /* cron command failed */
884
877
  }
885
- return sessions;
878
+ upsertSessionsBatch(entries);
879
+ db.prepare(`INSERT OR REPLACE INTO meta (key, value) VALUES ('openclaw_last_scan_ms', ?)`).run(String(Date.now()));
886
880
  }
881
+ /** Stream a Claude JSONL file and extract scan-level metadata (timestamp, cwd, topic, tokens). */
887
882
  async function scanClaudeSession(filePath) {
888
883
  const stream = fs.createReadStream(filePath, { encoding: 'utf-8' });
889
884
  const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
@@ -892,6 +887,7 @@ async function scanClaudeSession(filePath) {
892
887
  let gitBranch;
893
888
  let version;
894
889
  let topic;
890
+ let entrypoint;
895
891
  let messageCount = 0;
896
892
  let tokenCount = 0;
897
893
  let sawTokenCount = false;
@@ -908,6 +904,11 @@ async function scanClaudeSession(filePath) {
908
904
  catch {
909
905
  continue;
910
906
  }
907
+ // entrypoint ships on the first envelope event (attachment/user/assistant)
908
+ // and is the clean structural signal for "was this a team spawn?"
909
+ if (!entrypoint && typeof parsed.entrypoint === 'string') {
910
+ entrypoint = parsed.entrypoint;
911
+ }
911
912
  if (!timestamp && (parsed.type === 'user' || parsed.type === 'assistant') && parsed.timestamp) {
912
913
  timestamp = parsed.timestamp;
913
914
  cwd = parsed.cwd || '';
@@ -953,11 +954,13 @@ async function scanClaudeSession(filePath) {
953
954
  gitBranch,
954
955
  version,
955
956
  topic,
957
+ entrypoint,
956
958
  messageCount,
957
959
  tokenCount: sawTokenCount ? tokenCount : undefined,
958
- userTerms: userTexts.length > 0 ? userTexts : undefined,
960
+ contentText: userTexts.length > 0 ? userTexts.join('\n') : undefined,
959
961
  };
960
962
  }
963
+ /** Stream a Codex JSONL file and extract scan-level metadata (session ID, cwd, topic, tokens). */
961
964
  async function scanCodexSession(filePath) {
962
965
  const stream = fs.createReadStream(filePath, { encoding: 'utf-8' });
963
966
  const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
@@ -1025,9 +1028,10 @@ async function scanCodexSession(filePath) {
1025
1028
  topic,
1026
1029
  messageCount,
1027
1030
  tokenCount,
1028
- userTerms: userTexts.length > 0 ? userTexts : undefined,
1031
+ contentText: userTexts.length > 0 ? userTexts.join('\n') : undefined,
1029
1032
  };
1030
1033
  }
1034
+ /** Resolve the working directory for an OpenClaw agent from its workspace config. */
1031
1035
  function getOpenClawSessionCwd(agentId) {
1032
1036
  const workspace = agentId ? getOpenClawWorkspaceMap().get(agentId) : undefined;
1033
1037
  if (workspace)
@@ -1035,6 +1039,7 @@ function getOpenClawSessionCwd(agentId) {
1035
1039
  const configDir = AGENTS.openclaw.configDir;
1036
1040
  return safeRealpathSync(configDir) || configDir;
1037
1041
  }
1042
+ /** Build a cached map of OpenClaw agent ID to workspace path from openclaw.json. */
1038
1043
  function getOpenClawWorkspaceMap() {
1039
1044
  if (cachedOpenClawWorkspaces)
1040
1045
  return cachedOpenClawWorkspaces;
@@ -1061,6 +1066,7 @@ function getOpenClawWorkspaceMap() {
1061
1066
  // ---------------------------------------------------------------------------
1062
1067
  // Utilities
1063
1068
  // ---------------------------------------------------------------------------
1069
+ /** Read up to maxLines non-empty lines from the beginning of a file. */
1064
1070
  export function readFirstLines(filePath, maxLines) {
1065
1071
  return new Promise((resolve) => {
1066
1072
  const lines = [];
@@ -1081,13 +1087,12 @@ export function readFirstLines(filePath, maxLines) {
1081
1087
  }
1082
1088
  /**
1083
1089
  * Walk a directory recursively for files with a given extension.
1084
- * Returns at most `limit` files, sorted by mtime descending.
1085
1090
  */
1086
1091
  export function walkForFiles(dir, ext, limit) {
1087
1092
  const results = [];
1088
1093
  function walk(d, depth) {
1089
1094
  if (depth > 5)
1090
- return; // Prevent deep recursion
1095
+ return;
1091
1096
  let entries;
1092
1097
  try {
1093
1098
  entries = fs.readdirSync(d);
@@ -1109,13 +1114,14 @@ export function walkForFiles(dir, ext, limit) {
1109
1114
  }
1110
1115
  }
1111
1116
  walk(dir, 0);
1112
- // Sort by mtime descending and limit
1113
1117
  results.sort((a, b) => b.mtime - a.mtime);
1114
1118
  return results.slice(0, limit).map(r => r.path);
1115
1119
  }
1120
+ /** Compute the SHA-256 hex digest of a string. */
1116
1121
  function sha256(input) {
1117
1122
  return crypto.createHash('sha256').update(input).digest('hex');
1118
1123
  }
1124
+ /** Stat a path, returning null on any error. */
1119
1125
  function safeStatSync(p) {
1120
1126
  try {
1121
1127
  return fs.statSync(p);
@@ -1124,6 +1130,7 @@ function safeStatSync(p) {
1124
1130
  return null;
1125
1131
  }
1126
1132
  }
1133
+ /** Resolve a path to its real path, returning null on any error. */
1127
1134
  function safeRealpathSync(p) {
1128
1135
  try {
1129
1136
  return fs.realpathSync(p);
@@ -1132,6 +1139,7 @@ function safeRealpathSync(p) {
1132
1139
  return null;
1133
1140
  }
1134
1141
  }
1142
+ /** Extract meaningful user text from a Claude JSONL user event, skipping meta and local-command messages. */
1135
1143
  function extractClaudeUserText(parsed) {
1136
1144
  if (parsed.isMeta === true)
1137
1145
  return undefined;
@@ -1150,12 +1158,11 @@ function extractClaudeUserText(parsed) {
1150
1158
  return undefined;
1151
1159
  return text;
1152
1160
  }
1161
+ /** Check whether a message is a local-command wrapper rather than real user input. */
1153
1162
  function isLocalCommandMessage(text) {
1154
1163
  return /<local-command-caveat>|<bash-(input|stdout|stderr)>/i.test(text);
1155
1164
  }
1156
- function splitLines(text) {
1157
- return text.split('\n').map(l => l.trim()).filter(Boolean);
1158
- }
1165
+ /** Sum all token usage fields from a Claude assistant message's usage object. */
1159
1166
  function getClaudeUsageTotal(usage) {
1160
1167
  if (!usage || typeof usage !== 'object')
1161
1168
  return null;
@@ -1166,6 +1173,7 @@ function getClaudeUsageTotal(usage) {
1166
1173
  usage.cache_read_input_tokens,
1167
1174
  ]);
1168
1175
  }
1176
+ /** Extract text from Codex message content blocks, filtering out system instructions for user messages. */
1169
1177
  function extractCodexMessageText(contentBlocks, role) {
1170
1178
  if (!Array.isArray(contentBlocks))
1171
1179
  return undefined;
@@ -1184,10 +1192,12 @@ function extractCodexMessageText(contentBlocks, role) {
1184
1192
  });
1185
1193
  return text || undefined;
1186
1194
  }
1195
+ /** Trim and normalize a version string, returning undefined for empty values. */
1187
1196
  function normalizeVersion(version) {
1188
1197
  const trimmed = version?.trim();
1189
1198
  return trimmed ? trimmed : undefined;
1190
1199
  }
1200
+ /** Extract the version number from a managed ~/.agents/versions/<agent>/<version>/... path. */
1191
1201
  function extractVersionFromManagedPath(agent, sourcePath) {
1192
1202
  if (!sourcePath)
1193
1203
  return undefined;
@@ -1206,6 +1216,7 @@ function extractVersionFromManagedPath(agent, sourcePath) {
1206
1216
  }
1207
1217
  return undefined;
1208
1218
  }
1219
+ /** Resolve the current version of an agent CLI (symlink version or live CLI output, cached). */
1209
1220
  async function getCurrentAgentVersion(agent) {
1210
1221
  const cached = cachedAgentVersions.get(agent);
1211
1222
  if (cached)
@@ -1219,11 +1230,13 @@ async function getCurrentAgentVersion(agent) {
1219
1230
  cachedAgentVersions.set(agent, promise);
1220
1231
  return promise;
1221
1232
  }
1233
+ /** Resolve a session's version: embedded in file > extracted from managed path > current CLI version. */
1222
1234
  function resolveSessionVersion(agent, sourcePath, embeddedVersion, currentVersion) {
1223
1235
  return normalizeVersion(embeddedVersion)
1224
1236
  || extractVersionFromManagedPath(agent, sourcePath)
1225
1237
  || normalizeVersion(currentVersion);
1226
1238
  }
1239
+ /** Sum all token usage fields from a Codex total_token_usage object. */
1227
1240
  function getCodexTokenCount(totalTokenUsage) {
1228
1241
  if (!totalTokenUsage || typeof totalTokenUsage !== 'object')
1229
1242
  return null;
@@ -1234,6 +1247,7 @@ function getCodexTokenCount(totalTokenUsage) {
1234
1247
  totalTokenUsage.reasoning_output_tokens,
1235
1248
  ]);
1236
1249
  }
1250
+ /** Extract text from a Gemini message content field (string or array of parts). */
1237
1251
  function extractGeminiMessageText(content) {
1238
1252
  if (typeof content === 'string')
1239
1253
  return content.trim();
@@ -1251,6 +1265,7 @@ function extractGeminiMessageText(content) {
1251
1265
  }
1252
1266
  return '';
1253
1267
  }
1268
+ /** Extract the total token count from a Gemini message's tokens object. */
1254
1269
  function getGeminiTokenCount(tokens) {
1255
1270
  if (!tokens || typeof tokens !== 'object')
1256
1271
  return null;
@@ -1264,6 +1279,7 @@ function getGeminiTokenCount(tokens) {
1264
1279
  tokens.tool,
1265
1280
  ]);
1266
1281
  }
1282
+ /** Sum all numeric values in an array, returning null if none are valid numbers. */
1267
1283
  function sumKnownNumbers(values) {
1268
1284
  let total = 0;
1269
1285
  let found = false;
@@ -1278,11 +1294,16 @@ function sumKnownNumbers(values) {
1278
1294
  // ---------------------------------------------------------------------------
1279
1295
  // Time range parsing
1280
1296
  // ---------------------------------------------------------------------------
1297
+ /** Parse a time filter string (relative like '7d' or ISO timestamp) into epoch milliseconds. */
1281
1298
  export function parseTimeFilter(input) {
1282
- const relativeMatch = input.match(/^(\d+)([dw])$/i);
1299
+ const relativeMatch = input.match(/^(\d+)([mhdw])$/i);
1283
1300
  if (relativeMatch) {
1284
1301
  const value = parseInt(relativeMatch[1], 10);
1285
1302
  const unit = relativeMatch[2].toLowerCase();
1303
+ if (unit === 'm')
1304
+ return Date.now() - value * 60_000;
1305
+ if (unit === 'h')
1306
+ return Date.now() - value * 3_600_000;
1286
1307
  if (unit === 'd')
1287
1308
  return Date.now() - value * 86_400_000;
1288
1309
  if (unit === 'w')
@@ -1291,71 +1312,4 @@ export function parseTimeFilter(input) {
1291
1312
  const ts = new Date(input).getTime();
1292
1313
  return Number.isNaN(ts) ? 0 : ts;
1293
1314
  }
1294
- // ---------------------------------------------------------------------------
1295
- // BM25 content index search
1296
- // ---------------------------------------------------------------------------
1297
- /**
1298
- * Pure BM25 scorer over an in-memory index. Returns sessionId -> score+matched terms,
1299
- * sorted by score descending (Map preserves insertion order).
1300
- */
1301
- export function scoreBM25(index, query) {
1302
- const result = new Map();
1303
- if (index.N === 0)
1304
- return result;
1305
- const { counts: queryTerms } = tokenizeCounted(query);
1306
- if (queryTerms.size === 0)
1307
- return result;
1308
- const avgdl = index.avgdl || 1;
1309
- const scored = new Map();
1310
- for (const [term] of queryTerms) {
1311
- const termPostings = index.postings.get(term);
1312
- if (!termPostings)
1313
- continue;
1314
- const df = termPostings.size;
1315
- const idf = Math.log(1 + (index.N - df + 0.5) / (df + 0.5));
1316
- for (const [sessionId, tf] of termPostings) {
1317
- const dl = index.docLengths.get(sessionId) ?? avgdl;
1318
- const norm = 1 - BM25_B + BM25_B * (dl / avgdl);
1319
- const termScore = idf * (tf * (BM25_K1 + 1)) / (tf + BM25_K1 * norm);
1320
- const entry = scored.get(sessionId);
1321
- if (!entry) {
1322
- scored.set(sessionId, { score: termScore, matchedTerms: [term] });
1323
- }
1324
- else {
1325
- entry.score += termScore;
1326
- if (!entry.matchedTerms.includes(term))
1327
- entry.matchedTerms.push(term);
1328
- }
1329
- }
1330
- }
1331
- const sorted = [...scored.entries()].sort((a, b) => b[1].score - a[1].score);
1332
- for (const [sid, info] of sorted)
1333
- result.set(sid, info);
1334
- return result;
1335
- }
1336
- /**
1337
- * Score sessions using Okapi BM25 against the persisted content index.
1338
- * Returns a Map sorted by score descending, with matched terms attached.
1339
- */
1340
- export function searchContentIndex(sessions, query) {
1341
- const index = loadBM25Index();
1342
- if (!index)
1343
- return new Map();
1344
- const scored = scoreBM25(index, query);
1345
- if (scored.size === 0)
1346
- return new Map();
1347
- const sessionsById = new Map(sessions.map(s => [s.id, s]));
1348
- const result = new Map();
1349
- for (const [sessionId, info] of scored) {
1350
- const session = sessionsById.get(sessionId);
1351
- if (session) {
1352
- result.set(sessionId, {
1353
- ...session,
1354
- _matchedTerms: info.matchedTerms,
1355
- _bm25Score: info.score,
1356
- });
1357
- }
1358
- }
1359
- return result;
1360
- }
1361
1315
  //# sourceMappingURL=discover.js.map