agentbox-app 0.1.6

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 (302) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +176 -0
  3. package/bin/agentbox.mjs +103 -0
  4. package/briefs/adhd-mode.md +5 -0
  5. package/briefs/message-rules.md +95 -0
  6. package/briefs/worker.md +230 -0
  7. package/briefs/writing-rules.md +336 -0
  8. package/main/account-discovery.mjs +145 -0
  9. package/main/account-tooling.mjs +261 -0
  10. package/main/agent-activity.mjs +56 -0
  11. package/main/agent-files.mjs +422 -0
  12. package/main/agent-schedule.mjs +254 -0
  13. package/main/agent-sessions.mjs +435 -0
  14. package/main/agents.mjs +1095 -0
  15. package/main/analytics.mjs +206 -0
  16. package/main/answer-modes.mjs +206 -0
  17. package/main/approval-prompt-server.mjs +160 -0
  18. package/main/approvals.mjs +487 -0
  19. package/main/artifact-path.mjs +144 -0
  20. package/main/claude-bin.mjs +311 -0
  21. package/main/claude-input.mjs +117 -0
  22. package/main/claude-models.mjs +244 -0
  23. package/main/claude-plan.mjs +148 -0
  24. package/main/claude-usage.mjs +149 -0
  25. package/main/code-change.mjs +383 -0
  26. package/main/code-file.mjs +116 -0
  27. package/main/codex-account.mjs +146 -0
  28. package/main/codex-app-server.mjs +796 -0
  29. package/main/codex-approvals.mjs +712 -0
  30. package/main/codex-bin.mjs +261 -0
  31. package/main/codex-compaction.mjs +49 -0
  32. package/main/codex-models.mjs +194 -0
  33. package/main/codex-session.mjs +797 -0
  34. package/main/codex-threads.mjs +268 -0
  35. package/main/codex-usage-file.mjs +192 -0
  36. package/main/codex-usage.mjs +149 -0
  37. package/main/codex-watch.mjs +35 -0
  38. package/main/codex.mjs +757 -0
  39. package/main/config.mjs +345 -0
  40. package/main/copy-selection.mjs +55 -0
  41. package/main/crash-report.mjs +158 -0
  42. package/main/demo.mjs +254 -0
  43. package/main/dev-window.mjs +103 -0
  44. package/main/doc-file.mjs +99 -0
  45. package/main/doc-scheme.mjs +126 -0
  46. package/main/first-run.mjs +190 -0
  47. package/main/folders.mjs +91 -0
  48. package/main/fresh-user.mjs +120 -0
  49. package/main/git-change.mjs +323 -0
  50. package/main/img-scheme.mjs +90 -0
  51. package/main/instruction-settings.mjs +205 -0
  52. package/main/ipc.mjs +1558 -0
  53. package/main/live-replies.mjs +77 -0
  54. package/main/machine.mjs +113 -0
  55. package/main/main.mjs +972 -0
  56. package/main/menu.mjs +177 -0
  57. package/main/notify.mjs +155 -0
  58. package/main/open-source-file.mjs +8 -0
  59. package/main/project-identity.mjs +149 -0
  60. package/main/provider-commands.mjs +32 -0
  61. package/main/rail-note.mjs +74 -0
  62. package/main/repeats.mjs +261 -0
  63. package/main/row-label.mjs +185 -0
  64. package/main/screen-detail.mjs +44 -0
  65. package/main/serve.mjs +262 -0
  66. package/main/settings.mjs +912 -0
  67. package/main/staleness.mjs +76 -0
  68. package/main/store/chats.mjs +188 -0
  69. package/main/store/home.mjs +383 -0
  70. package/main/store/migrate.mjs +40 -0
  71. package/main/store/paths.mjs +82 -0
  72. package/main/store/pending-writes.mjs +122 -0
  73. package/main/store/project-lock.mjs +200 -0
  74. package/main/store/project.mjs +523 -0
  75. package/main/store/queues.mjs +31 -0
  76. package/main/store/work-items.mjs +299 -0
  77. package/main/store/write-queue.mjs +14 -0
  78. package/main/store-modules.mjs +23 -0
  79. package/main/store.mjs +1269 -0
  80. package/main/supervisor.mjs +6875 -0
  81. package/main/task-commands.mjs +161 -0
  82. package/main/task-folders-offthread.mjs +84 -0
  83. package/main/task-folders-thread.mjs +13 -0
  84. package/main/task-folders.mjs +409 -0
  85. package/main/task-remote-control.mjs +65 -0
  86. package/main/task-terminals.mjs +169 -0
  87. package/main/terminal-access.mjs +6 -0
  88. package/main/updater.mjs +240 -0
  89. package/main/zoom-keys.mjs +54 -0
  90. package/main/zoom-readout.mjs +20 -0
  91. package/mcp/agentbox-mcp.sh +34 -0
  92. package/mcp/core/account.mjs +77 -0
  93. package/mcp/core/chat.mjs +61 -0
  94. package/mcp/core/documents.mjs +238 -0
  95. package/mcp/core/page.mjs +152 -0
  96. package/mcp/core/products.mjs +110 -0
  97. package/mcp/core/skills.mjs +126 -0
  98. package/mcp/core/work.mjs +377 -0
  99. package/mcp/stdio.mjs +70 -0
  100. package/mcp/tools.mjs +257 -0
  101. package/package.json +186 -0
  102. package/renderer/dist/assets/Geist-Variable-Bj2R_7yk.woff2 +0 -0
  103. package/renderer/dist/assets/GeistMono-Variable-Dispecij.woff2 +0 -0
  104. package/renderer/dist/assets/addon-fit-YJmn1quW.js +12 -0
  105. package/renderer/dist/assets/addon-serialize-CsFQTvP_.js +13 -0
  106. package/renderer/dist/assets/cel-dusk-CS5I7kJS.webp +0 -0
  107. package/renderer/dist/assets/cel-dusk-DUslqS6B.webp +0 -0
  108. package/renderer/dist/assets/cel-dusk-Jt_xVGxp.webp +0 -0
  109. package/renderer/dist/assets/cel-dusk-sm-BBkV-tWT.webp +0 -0
  110. package/renderer/dist/assets/frost-haze-CxPSOBEg.webp +0 -0
  111. package/renderer/dist/assets/frost-haze-sm-C_w21qgd.webp +0 -0
  112. package/renderer/dist/assets/gouache-valley-ChSu14-t.webp +0 -0
  113. package/renderer/dist/assets/gouache-valley-d9qasI6k.webp +0 -0
  114. package/renderer/dist/assets/gouache-valley-sm-CdLjT5Ec.webp +0 -0
  115. package/renderer/dist/assets/ice-BC8luINc.webp +0 -0
  116. package/renderer/dist/assets/ice-CP9PrKHo.webp +0 -0
  117. package/renderer/dist/assets/ice-river-D6ziOhLY.webp +0 -0
  118. package/renderer/dist/assets/ice-river-UVM2-JY_.webp +0 -0
  119. package/renderer/dist/assets/ice-river-bErJwYWW.webp +0 -0
  120. package/renderer/dist/assets/ice-river-sm-Bg1U-SIj.webp +0 -0
  121. package/renderer/dist/assets/ice-sm-CbVcg3_D.webp +0 -0
  122. package/renderer/dist/assets/index-CwiDv5bL.css +32 -0
  123. package/renderer/dist/assets/index-D9hCAVPl.js +684 -0
  124. package/renderer/dist/assets/lake-C0WaW-6E.webp +0 -0
  125. package/renderer/dist/assets/lake-CSKjLC5K.webp +0 -0
  126. package/renderer/dist/assets/lake-ETGcOb25.webp +0 -0
  127. package/renderer/dist/assets/lake-harbour-BCQRbrHb.webp +0 -0
  128. package/renderer/dist/assets/lake-harbour-B_wfXTmE.webp +0 -0
  129. package/renderer/dist/assets/lake-harbour-Te1pEvBh.webp +0 -0
  130. package/renderer/dist/assets/lake-harbour-sm-Cme_ShEg.webp +0 -0
  131. package/renderer/dist/assets/lake-sm-CbG72Hbv.webp +0 -0
  132. package/renderer/dist/assets/lino-coast-CUZHcucI.webp +0 -0
  133. package/renderer/dist/assets/lino-coast-DP4J7cgd.webp +0 -0
  134. package/renderer/dist/assets/lino-coast-kOIAffr_.webp +0 -0
  135. package/renderer/dist/assets/lino-coast-sm-C1Mdbgit.webp +0 -0
  136. package/renderer/dist/assets/moon-pines-BskgnnXR.webp +0 -0
  137. package/renderer/dist/assets/moon-pines-CFSIIwt0.webp +0 -0
  138. package/renderer/dist/assets/moon-pines-PNhdOYww.webp +0 -0
  139. package/renderer/dist/assets/moon-pines-sm-C6K1NomK.webp +0 -0
  140. package/renderer/dist/assets/orbit-rings-2XV36Ch3.webp +0 -0
  141. package/renderer/dist/assets/orbit-rings-C4NrcYGQ.webp +0 -0
  142. package/renderer/dist/assets/orbit-rings-DGLk07dT.webp +0 -0
  143. package/renderer/dist/assets/orbit-rings-sm-Dky3YdvL.webp +0 -0
  144. package/renderer/dist/assets/peach-haze-2-C6f7xnWw.webp +0 -0
  145. package/renderer/dist/assets/peach-haze-2-sm-Dj_Ez_Sb.webp +0 -0
  146. package/renderer/dist/assets/pixel-harbour-DQjqMROg.webp +0 -0
  147. package/renderer/dist/assets/pixel-harbour-Dw71H_bc.webp +0 -0
  148. package/renderer/dist/assets/pixel-harbour-kOZfm-GM.webp +0 -0
  149. package/renderer/dist/assets/pixel-harbour-sm-Dturjuky.webp +0 -0
  150. package/renderer/dist/assets/pixel-orbit-C296JVMO.webp +0 -0
  151. package/renderer/dist/assets/pixel-orbit-C4oMRDSx.webp +0 -0
  152. package/renderer/dist/assets/pixel-orbit-CqVmQ4eh.webp +0 -0
  153. package/renderer/dist/assets/pixel-orbit-sm-CHagC_nO.webp +0 -0
  154. package/renderer/dist/assets/pixel-ridge-C6LYu6uE.webp +0 -0
  155. package/renderer/dist/assets/pixel-ridge-CIUCEESs.webp +0 -0
  156. package/renderer/dist/assets/pixel-ridge-sm-BnE0F_w-.webp +0 -0
  157. package/renderer/dist/assets/riso-hills-7heKVtry.webp +0 -0
  158. package/renderer/dist/assets/riso-hills-CIjAL3J6.webp +0 -0
  159. package/renderer/dist/assets/riso-hills-sm-DHsnbCfP.webp +0 -0
  160. package/renderer/dist/assets/slate-haze-BPWdkNsw.webp +0 -0
  161. package/renderer/dist/assets/slate-haze-sm-B0Cb5a2W.webp +0 -0
  162. package/renderer/dist/assets/source-sans-3-cyrillic-ext-wght-normal-DzyfIafT.woff2 +0 -0
  163. package/renderer/dist/assets/source-sans-3-cyrillic-wght-normal-BMDVbyM7.woff2 +0 -0
  164. package/renderer/dist/assets/source-sans-3-greek-ext-wght-normal-BWSLJLk6.woff2 +0 -0
  165. package/renderer/dist/assets/source-sans-3-greek-wght-normal-C9H9m1vD.woff2 +0 -0
  166. package/renderer/dist/assets/source-sans-3-latin-ext-wght-normal-C8iNium2.woff2 +0 -0
  167. package/renderer/dist/assets/source-sans-3-latin-wght-normal-BqRLTx4X.woff2 +0 -0
  168. package/renderer/dist/assets/source-sans-3-vietnamese-wght-normal-C1uRvKPU.woff2 +0 -0
  169. package/renderer/dist/assets/valley-haze-BDHPqoJy.webp +0 -0
  170. package/renderer/dist/assets/valley-haze-sm-CTyjmPAG.webp +0 -0
  171. package/renderer/dist/assets/vector-dunes-DzzWLdKo.webp +0 -0
  172. package/renderer/dist/assets/vector-dunes-Yr9NBQUy.webp +0 -0
  173. package/renderer/dist/assets/vector-dunes-sm-D8MYY6ms.webp +0 -0
  174. package/renderer/dist/assets/watercolour-mist-BCGKN4u4.webp +0 -0
  175. package/renderer/dist/assets/watercolour-mist-BF-jk5MC.webp +0 -0
  176. package/renderer/dist/assets/watercolour-mist-sm-Ds1xzTw9.webp +0 -0
  177. package/renderer/dist/assets/woodblock-sea-B1um67Z9.webp +0 -0
  178. package/renderer/dist/assets/woodblock-sea-DKQdYs32.webp +0 -0
  179. package/renderer/dist/assets/woodblock-sea-sm-BJzNq8xh.webp +0 -0
  180. package/renderer/dist/assets/xterm-BT7tBf8A.js +27 -0
  181. package/renderer/dist/emulator/audio-rules.mjs +74 -0
  182. package/renderer/dist/emulator/data/compression/README.md +11 -0
  183. package/renderer/dist/emulator/data/compression/extract7z.js +1 -0
  184. package/renderer/dist/emulator/data/compression/extractzip.js +1 -0
  185. package/renderer/dist/emulator/data/compression/libunrar.js +519 -0
  186. package/renderer/dist/emulator/data/compression/libunrar.wasm +0 -0
  187. package/renderer/dist/emulator/data/cores/README.md +22 -0
  188. package/renderer/dist/emulator/data/cores/mgba-legacy-wasm.data +0 -0
  189. package/renderer/dist/emulator/data/cores/mgba-thread-legacy-wasm.data +0 -0
  190. package/renderer/dist/emulator/data/cores/mgba-thread-wasm.data +0 -0
  191. package/renderer/dist/emulator/data/cores/mgba-wasm.data +0 -0
  192. package/renderer/dist/emulator/data/cores/package.json +65 -0
  193. package/renderer/dist/emulator/data/cores/reports/mgba.json +1 -0
  194. package/renderer/dist/emulator/data/emulator.css +1614 -0
  195. package/renderer/dist/emulator/data/loader.js +170 -0
  196. package/renderer/dist/emulator/data/localization/README.md +63 -0
  197. package/renderer/dist/emulator/data/localization/af-FR.json +339 -0
  198. package/renderer/dist/emulator/data/localization/ar-AR.json +301 -0
  199. package/renderer/dist/emulator/data/localization/ben-BEN.json +301 -0
  200. package/renderer/dist/emulator/data/localization/de-GER.json +301 -0
  201. package/renderer/dist/emulator/data/localization/el-GR.json +301 -0
  202. package/renderer/dist/emulator/data/localization/en-US.json +339 -0
  203. package/renderer/dist/emulator/data/localization/es-ES.json +310 -0
  204. package/renderer/dist/emulator/data/localization/fa-AF.json +310 -0
  205. package/renderer/dist/emulator/data/localization/hi-HI.json +301 -0
  206. package/renderer/dist/emulator/data/localization/it-IT.json +302 -0
  207. package/renderer/dist/emulator/data/localization/ja-JA.json +301 -0
  208. package/renderer/dist/emulator/data/localization/jv-JV.json +301 -0
  209. package/renderer/dist/emulator/data/localization/ko-KO.json +336 -0
  210. package/renderer/dist/emulator/data/localization/pt-BR.json +361 -0
  211. package/renderer/dist/emulator/data/localization/retroarch.json +617 -0
  212. package/renderer/dist/emulator/data/localization/ro-RO.json +310 -0
  213. package/renderer/dist/emulator/data/localization/ru-RU.json +301 -0
  214. package/renderer/dist/emulator/data/localization/tr-TR.json +310 -0
  215. package/renderer/dist/emulator/data/localization/vi-VN.json +302 -0
  216. package/renderer/dist/emulator/data/localization/zh-CN.json +351 -0
  217. package/renderer/dist/emulator/data/src/GameManager.js +464 -0
  218. package/renderer/dist/emulator/data/src/compression.js +142 -0
  219. package/renderer/dist/emulator/data/src/emulator.js +6158 -0
  220. package/renderer/dist/emulator/data/src/gamepad.js +167 -0
  221. package/renderer/dist/emulator/data/src/nipplejs.js +1 -0
  222. package/renderer/dist/emulator/data/src/shaders.js +236 -0
  223. package/renderer/dist/emulator/data/src/socket.io.min.js +7 -0
  224. package/renderer/dist/emulator/data/src/storage.js +125 -0
  225. package/renderer/dist/emulator/data/version.json +5 -0
  226. package/renderer/dist/emulator/host.js +299 -0
  227. package/renderer/dist/emulator/index.html +28 -0
  228. package/renderer/dist/emulator/provenance.json +6 -0
  229. package/renderer/dist/fixtures/onboard/designs/w-n13/the-first-row.html +1 -0
  230. package/renderer/dist/index.html +13 -0
  231. package/scripts/after-install.mjs +59 -0
  232. package/scripts/approval-server.sh +43 -0
  233. package/scripts/before-publish.mjs +133 -0
  234. package/shared/agent-import.d.mts +13 -0
  235. package/shared/agent-import.mjs +252 -0
  236. package/shared/agents.d.mts +190 -0
  237. package/shared/agents.mjs +937 -0
  238. package/shared/analytics-events.mjs +63 -0
  239. package/shared/answers.d.mts +12 -0
  240. package/shared/answers.mjs +80 -0
  241. package/shared/artifact-keys.d.mts +28 -0
  242. package/shared/artifact-keys.mjs +140 -0
  243. package/shared/bridge-map.mjs +128 -0
  244. package/shared/claude-commands.d.mts +20 -0
  245. package/shared/claude-commands.generated.mjs +149 -0
  246. package/shared/claude-commands.mjs +131 -0
  247. package/shared/claude-models.generated.d.mts +15 -0
  248. package/shared/claude-models.generated.mjs +26 -0
  249. package/shared/claude-usage.d.mts +16 -0
  250. package/shared/claude-usage.mjs +172 -0
  251. package/shared/codex-commands.d.mts +4 -0
  252. package/shared/codex-commands.mjs +17 -0
  253. package/shared/codex-import.d.mts +38 -0
  254. package/shared/codex-import.mjs +232 -0
  255. package/shared/codex-modes.mjs +159 -0
  256. package/shared/codex-usage.mjs +180 -0
  257. package/shared/contracts.mjs +412 -0
  258. package/shared/crash-scrub.mjs +182 -0
  259. package/shared/dashboard.mjs +481 -0
  260. package/shared/dead-run-trace.mjs +127 -0
  261. package/shared/demo-world.mjs +415 -0
  262. package/shared/effort-levels.d.mts +15 -0
  263. package/shared/effort-levels.mjs +76 -0
  264. package/shared/engines.d.mts +62 -0
  265. package/shared/engines.mjs +389 -0
  266. package/shared/first-run-examples.mjs +60 -0
  267. package/shared/first-run-line.mjs +149 -0
  268. package/shared/first-run-practice.d.mts +32 -0
  269. package/shared/first-run-practice.mjs +254 -0
  270. package/shared/first-run-shape.mjs +100 -0
  271. package/shared/fresh-user-home.mjs +189 -0
  272. package/shared/hook-failure.mjs +22 -0
  273. package/shared/install-id.mjs +39 -0
  274. package/shared/instruction-defaults.json +5 -0
  275. package/shared/library-folder.mjs +28 -0
  276. package/shared/notify-rules.mjs +120 -0
  277. package/shared/product-name.d.mts +6 -0
  278. package/shared/product-name.mjs +144 -0
  279. package/shared/project-folder-check.d.mts +22 -0
  280. package/shared/project-folder-check.mjs +152 -0
  281. package/shared/provider-commands.d.mts +8 -0
  282. package/shared/provider-commands.mjs +58 -0
  283. package/shared/rank.d.mts +19 -0
  284. package/shared/rank.mjs +146 -0
  285. package/shared/recovery.mjs +26 -0
  286. package/shared/referenced-files.d.mts +11 -0
  287. package/shared/referenced-files.mjs +122 -0
  288. package/shared/repeats.d.mts +45 -0
  289. package/shared/repeats.mjs +193 -0
  290. package/shared/schemes.mjs +33 -0
  291. package/shared/settings-terminal.d.mts +1 -0
  292. package/shared/settings-terminal.mjs +14 -0
  293. package/shared/side-build.mjs +57 -0
  294. package/shared/spawn-trouble.d.mts +8 -0
  295. package/shared/spawn-trouble.mjs +599 -0
  296. package/shared/terminal-state.d.mts +1 -0
  297. package/shared/terminal-state.mjs +58 -0
  298. package/shared/usage.d.mts +53 -0
  299. package/shared/usage.mjs +319 -0
  300. package/shared/work-items.mjs +561 -0
  301. package/shared/work-lines.mjs +360 -0
  302. package/worker-permissions.json +6 -0
@@ -0,0 +1,435 @@
1
+ // HER CLAUDE CODE THREADS, WHICH ARE THE AGENTS SHE MEANT.
2
+ //
3
+ // That sentence retires the assumption `agent-files.mjs` was built on. That
4
+ // module reads `.md` agent DEFINITIONS, which is a real thing Claude Code has
5
+ // and is not what she is looking at when she says "my agents". the four
6
+ // definition files on this Mac were last edited on 23 and 30 March, and she had
7
+ // four threads going the evening she complained.
8
+ //
9
+ // So the import gets a second source, and this is it. Nothing here replaces the
10
+ // files; a definition and a thread are different objects and the card shows
11
+ // both.
12
+ //
13
+ // WHERE A THREAD LIVES. Two places, because she named two:
14
+ //
15
+ // ~/.claude/projects/<folder>/<id>.jsonl every terminal session
16
+ // ~/Library/Application Support/Claude/
17
+ // claude-code-sessions/**/local_<id>.json the desktop app
18
+ //
19
+ // The terminal store is the transcript itself, one JSON object per line. The
20
+ // desktop store is a small record ABOUT the session, and it is the richer of
21
+ // the two because the app writes a real `title` into it. Measured 2026-08-29:
22
+ // 2,444 transcripts in 240 folders, and 38 desktop records whose newest is
23
+ // 7 July, so on her Mac today the terminal side is all of it.
24
+ //
25
+ // THE HARD PART IS NOT FINDING THEM, IT IS THROWING THEM AWAY. hundreds exist
26
+ // and a handful are relevant.
27
+ //
28
+ // The line that separates them is not a heuristic, which is the good news. Every
29
+ // transcript row carries an `entrypoint`, and it says who started the session.
30
+ // Counted over all 2,212 transcripts in her store on 2026-08-29:
31
+ //
32
+ // 2,158 sdk-cli something started it: Agentbox's own workers, Agentbox's
33
+ // chats, and our measurement scripts running twenty
34
+ // agents at a time in temp folders
35
+ // 53 cli she opened a terminal and typed
36
+ // 1 claude-desktop she used the desktop app
37
+ //
38
+ // So 54 of 2,212 were started by a person, and those 54 are the handful she
39
+ // means. Her own test is one of them: `~/Desktop/dev/zero`, 8:38 PM, "this is a
40
+ // test", `entrypoint: "cli"`.
41
+ //
42
+ // A FIRST DRAFT OF THIS GUESSED INSTEAD, and the guess is worth keeping written
43
+ // down because it looked like it worked. It dropped anything opening with the
44
+ // work item brief and anything running in a temp folder, which on that evening
45
+ // removed 126 and 171 rows and left a believable list. It would have kept every
46
+ // chat Agentbox itself runs in a product folder, because those open with her own
47
+ // question and run somewhere real. `entrypoint` was on the row the whole time.
48
+
49
+ import fs from 'node:fs';
50
+ import os from 'node:os';
51
+ import path from 'node:path';
52
+
53
+ /**
54
+ * "Recent = last few days", in days.
55
+ *
56
+ * Seven rather than three, and the number is measured rather than chosen.
57
+ * Counting only the threads a person started, her Mac on 2026-08-29 has 2 in
58
+ * the last three days and 9 in the last seven, in two folders she works in.
59
+ * Three days is a card with tonight's test on it and almost nothing else; the
60
+ * filters have already done the work that "a few" was standing in for, so the
61
+ * window can afford to be the generous reading. */
62
+ export const RECENT_DAYS = 7;
63
+
64
+ /**
65
+ * How much of a transcript is read to find its first human turn. The first
66
+ * turn is at the top of the file by construction, and the files are not small:
67
+ * the largest in her store on 2026-08-29 is 13MB, and there are hundreds. */
68
+ const HEAD_BYTES = 96 * 1024;
69
+
70
+ /**
71
+ * A stop on how many transcripts are OPENED, for the same reason the folder
72
+ * scan in agent-files.mjs has one: opening a card may not turn into a disk
73
+ * crawl.
74
+ *
75
+ * It is a cap on reading, never on looking. The walk stats every file, which
76
+ * is cheap, throws away everything older than the cutoff, and sorts what is
77
+ * left newest first before it opens anything. An earlier draft capped the walk
78
+ * itself and the bug is worth keeping written down: the cap was reached in
79
+ * directory order, so on her Mac it stopped after 1,500 of 2,444 files and
80
+ * whether tonight's thread survived was down to where its folder sorted. A
81
+ * cap that can drop the newest thread on the machine is not a safety rail. */
82
+ const MAX_READS = 1200;
83
+
84
+ /* ------------------------- WHAT IS NOT HERS TO IMPORT ---------------------- */
85
+
86
+ /**
87
+ * A FOLDER NOBODY WORKS IN.
88
+ *
89
+ * `/private/var/folders/19/.../T/speedsize-6AGMVw-run-12` is a real cwd of a
90
+ * real Claude Code session, and it is one of twenty a measurement script of
91
+ * ours started in a loop. 258 of her 448 recent threads are that. Nobody opens
92
+ * a temp directory to work, so a thread that ran in one is not a thread she
93
+ * would recognise, and offering it is how a list of hundreds happens. */
94
+ export function isScratchFolder(folder, home = os.homedir()) {
95
+ const p = String(folder ?? '');
96
+ if (!p) return true;
97
+ // A FOLDER INSIDE YOUR OWN HOME IS YOURS, whatever the disk path says. This
98
+ // is the rule the temp-path list is a proxy for, and it has to be stated
99
+ // first, because a Mac can put a home directory anywhere and the tests put
100
+ // one under `/var/folders` on purpose.
101
+ const here = path.resolve(String(home ?? ''));
102
+ if (here && (p === here || p.startsWith(`${here}/`))) return false;
103
+ return /^\/private\/tmp(\/|$)/.test(p)
104
+ || /^\/tmp(\/|$)/.test(p)
105
+ || /^\/private\/var\/folders(\/|$)/.test(p)
106
+ || /^\/var\/folders(\/|$)/.test(p);
107
+ }
108
+
109
+ /**
110
+ * THE ONES A PERSON STARTED, which is the whole filter.
111
+ *
112
+ * `cli` is a terminal she opened. `claude-desktop` is the desktop app, which
113
+ * she named. Everything else was started by software, and on this Mac that is
114
+ * overwhelmingly Agentbox itself: its workers, its chats, and the scripts we run
115
+ * to measure it. Offering those on an import card would be offering to bring
116
+ * the inbox into the inbox.
117
+ *
118
+ * An entrypoint we have never seen is treated as not hers. The cost of being
119
+ * wrong that way is one missing row on a card; the other way is the hundreds
120
+ * she is complaining about. */
121
+ export function startedByHand(entrypoint) {
122
+ return entrypoint === 'cli' || entrypoint === 'claude-desktop';
123
+ }
124
+
125
+ /**
126
+ * A SUBAGENT IS NOT A THREAD SHE OPENED. Claude Code files them under
127
+ * `<session>/subagents/`, one per fan-out, so the parent already stands for
128
+ * them and listing both says the same work twice. */
129
+ export function isSubagentPath(file) {
130
+ return /(^|\/)subagents\//.test(String(file ?? ''));
131
+ }
132
+
133
+ /* ------------------------------ WHAT IT IS CALLED -------------------------- */
134
+
135
+ /**
136
+ * Lines a person did not type, which a transcript's first turn is often made
137
+ * of: the slash command wrapper, the resume caveat, and anything the harness
138
+ * injected. A thread named `<command-name>/clear</command-name>` is a thread
139
+ * she cannot recognise. */
140
+ const NOT_TYPED = [
141
+ /^<command-name>/, /^<command-message>/, /^<command-args>/, /^<local-command/,
142
+ /^<system-reminder>/, /^<user-memory/, /^Caveat: The messages below/,
143
+ /^This session is being continued from/, /^\[Request interrupted/,
144
+ ];
145
+
146
+ /**
147
+ * THE NAME ON THE ROW, out of the first thing she actually typed.
148
+ *
149
+ * A terminal transcript has no title in it: measured 2026-08-29, ZERO of her
150
+ * 495 recent files carry a `summary` row, so there is nothing to read but the
151
+ * conversation. The first human sentence is what the desktop app titles from
152
+ * too, and it is the line she would know the thread by.
153
+ *
154
+ * SEVERAL MESSAGES ARE OFFERED, NOT ONE, and that is a fix rather than a
155
+ * flourish. A session that opens with a slash command has the wrapper as its
156
+ * first user message and her actual words in the second, so reading only the
157
+ * first left four of her own threads in an outside repo nameless and
158
+ * therefore off the card. The first message that is something a person typed
159
+ * wins; a thread with no such message has no name and is dropped, which is the
160
+ * right outcome for a session that was never spoken to.
161
+ *
162
+ * Markdown is flattened rather than rendered, because this lands in a list. */
163
+ export function threadTitle(texts, max = 72) {
164
+ for (const text of Array.isArray(texts) ? texts : [texts]) {
165
+ const got = titleFromOne(text, max);
166
+ if (got) return got;
167
+ }
168
+ return '';
169
+ }
170
+
171
+ function titleFromOne(text, max) {
172
+ const raw = String(text ?? '').replace(/\r/g, '');
173
+ for (const line of raw.split('\n')) {
174
+ const t = line.trim();
175
+ if (!t) continue;
176
+ if (NOT_TYPED.some((re) => re.test(t))) return '';
177
+ const clean = t
178
+ .replace(/^#{1,6}\s+/, '')
179
+ .replace(/^[-*+]\s+/, '')
180
+ .replace(/[*_`]/g, '')
181
+ .trim();
182
+ if (!clean) continue;
183
+ if (clean.length <= max) return clean;
184
+ const cut = clean.slice(0, max);
185
+ const sp = cut.lastIndexOf(' ');
186
+ return `${(sp > 30 ? cut.slice(0, sp) : cut).replace(/[,;:]$/, '')}...`;
187
+ }
188
+ return '';
189
+ }
190
+
191
+ /**
192
+ * `/Users/you/Desktop/dev/zero` → `~/Desktop/dev/zero`. Same reason as the
193
+ * one in agent-files.mjs: a path carrying her account name reads like a
194
+ * system string rather than a folder she knows. */
195
+ export function shortFolder(folder, home = os.homedir()) {
196
+ const here = path.resolve(home);
197
+ const full = path.resolve(String(folder ?? '') || here);
198
+ return full === here || full.startsWith(`${here}/`) ? `~${full.slice(here.length)}` : full;
199
+ }
200
+
201
+ /* ------------------------------ THE TERMINAL SIDE -------------------------- */
202
+
203
+ /**
204
+ * The head of a transcript, and out of it the three things a row needs: who
205
+ * started the session, which folder it ran in, and the first thing the person
206
+ * typed.
207
+ *
208
+ * Read as a fixed head rather than a whole file. `entrypoint` and `cwd` are on
209
+ * every message row and the first human turn is the first message, so all
210
+ * three are at the top, and the alternative is reading gigabytes to fill in a
211
+ * list. Her largest transcript is 13MB and there are hundreds. */
212
+ export function readTranscriptHead(file, bytes = HEAD_BYTES) {
213
+ let fd = null;
214
+ try {
215
+ fd = fs.openSync(file, 'r');
216
+ const buf = Buffer.alloc(bytes);
217
+ const n = fs.readSync(fd, buf, 0, bytes, 0);
218
+ const text = buf.slice(0, n).toString('utf8');
219
+ // The last line of a fixed read is usually cut in half. Dropping it costs
220
+ // nothing, because what is wanted is at the top.
221
+ const lines = text.split('\n').slice(0, -1);
222
+ let cwd = null;
223
+ let entrypoint = null;
224
+ const said = [];
225
+ for (const line of lines) {
226
+ if (!line.trim()) continue;
227
+ let row;
228
+ try { row = JSON.parse(line); } catch { continue; }
229
+ if (!cwd && typeof row.cwd === 'string' && row.cwd) cwd = row.cwd;
230
+ if (!entrypoint && typeof row.entrypoint === 'string' && row.entrypoint) entrypoint = row.entrypoint;
231
+ if (row.type === 'user' && said.length < 5) {
232
+ const t = messageText(row);
233
+ if (t.trim()) said.push(t);
234
+ }
235
+ if (cwd && entrypoint && said.length >= 5) break;
236
+ }
237
+ return { cwd, entrypoint, said };
238
+ } catch {
239
+ return { cwd: null, entrypoint: null, said: [] };
240
+ } finally {
241
+ if (fd !== null) { try { fs.closeSync(fd); } catch { /* nothing to do */ } }
242
+ }
243
+ }
244
+
245
+ /**
246
+ * The words out of one transcript row, whichever of the two shapes it is in.
247
+ * Content is a string on the plain ones and a list of blocks on the rest. */
248
+ function messageText(row) {
249
+ const c = row?.message?.content;
250
+ if (typeof c === 'string') return c;
251
+ if (Array.isArray(c)) {
252
+ return c
253
+ .filter((b) => b && b.type === 'text' && typeof b.text === 'string')
254
+ .map((b) => b.text)
255
+ .join('\n');
256
+ }
257
+ return '';
258
+ }
259
+
260
+ /**
261
+ * Every terminal thread, newest first, with the three kinds of noise gone.
262
+ *
263
+ * `skipped` comes back with it because the counts are the argument for the
264
+ * filters, and a session that changes one of them should be able to see what
265
+ * it changed rather than take this comment's word for it. */
266
+ function readTerminalThreads({ home, cutoff, max, skipIds = new Set() }) {
267
+ const root = path.join(home, '.claude', 'projects');
268
+ const skipped = { old: 0, scratch: 0, started: 0, subagent: 0, empty: 0, unread: 0, desktopDupe: 0 };
269
+ const out = [];
270
+ const all = [];
271
+ walkTranscripts(root, all);
272
+ // Stat everything, keep the recent, newest first, and only then start opening
273
+ // files. This is the order that makes `max` safe: whatever it cuts off is
274
+ // older than everything it kept.
275
+ const recent = [];
276
+ for (const file of all) {
277
+ let when = 0;
278
+ try { when = fs.statSync(file).mtimeMs; } catch { continue; }
279
+ if (when < cutoff) { skipped.old += 1; continue; }
280
+ if (isSubagentPath(file.slice(root.length))) { skipped.subagent += 1; continue; }
281
+ recent.push({ file, when });
282
+ }
283
+ recent.sort((a, b) => b.when - a.when);
284
+ if (recent.length > max) skipped.unread = recent.length - max;
285
+ for (const { file, when } of recent.slice(0, max)) {
286
+ const id = path.basename(file, '.jsonl');
287
+ // THE DESKTOP APP WRITES A THREAD TWICE, once as a transcript here and once
288
+ // as its own record, joined by `cliSessionId`. Measured 2026-08-29 on the
289
+ // one desktop thread in her store. Its own record wins, because the app
290
+ // wrote a real title into it and this side would have to guess one.
291
+ if (skipIds.has(id)) { skipped.desktopDupe += 1; continue; }
292
+ const { cwd, entrypoint, said } = readTranscriptHead(file);
293
+ if (!startedByHand(entrypoint)) { skipped.started += 1; continue; }
294
+ if (isScratchFolder(cwd, home)) { skipped.scratch += 1; continue; }
295
+ const title = threadTitle(said);
296
+ if (!title) { skipped.empty += 1; continue; }
297
+ out.push({
298
+ id,
299
+ source: entrypoint === 'claude-desktop' ? 'desktop' : 'terminal',
300
+ folder: cwd,
301
+ folderName: path.basename(cwd),
302
+ short: shortFolder(cwd, home),
303
+ title,
304
+ when,
305
+ path: file,
306
+ });
307
+ }
308
+ return { threads: out, skipped };
309
+ }
310
+
311
+ /**
312
+ * Every transcript under the projects root, subagent folders included so the
313
+ * filter above can count them rather than the walk hiding them. Unbounded on
314
+ * purpose: this only lists names, and the cap that matters is on opening. */
315
+ function walkTranscripts(dir, into, depth = 4) {
316
+ if (depth < 0) return;
317
+ let entries = [];
318
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
319
+ for (const e of entries) {
320
+ const full = path.join(dir, e.name);
321
+ if (e.isDirectory()) walkTranscripts(full, into, depth - 1);
322
+ else if (e.isFile() && e.name.endsWith('.jsonl')) into.push(full);
323
+ }
324
+ }
325
+
326
+ /* ------------------------------- THE DESKTOP SIDE -------------------------- */
327
+
328
+ /** Where the desktop app keeps what it knows about a session. */
329
+ export function desktopSessionsDir(home = os.homedir()) {
330
+ return path.join(home, 'Library', 'Application Support', 'Claude', 'claude-code-sessions');
331
+ }
332
+
333
+ /**
334
+ * THE DESKTOP APP'S THREADS, which are the easy half.
335
+ *
336
+ * Its record is small and it carries a real `title` the app wrote, plus the
337
+ * folder, the branch and the moment of the last turn. So there is no
338
+ * transcript to read and no name to guess: measured on her Mac 2026-08-29, 38
339
+ * records, every one of them titled, for example "Fix duplicate cowork task
340
+ * display issue".
341
+ *
342
+ * Archived ones are left out. She archived them, which is the desktop app's
343
+ * word for done with. */
344
+ function readDesktopThreads({ home, cutoff, max }) {
345
+ const root = desktopSessionsDir(home);
346
+ const skipped = { old: 0, scratch: 0, archived: 0, empty: 0 };
347
+ const out = [];
348
+ // Every id this side owns, recent or not, so the transcript walk can leave
349
+ // them alone rather than draw the same thread under a worse name.
350
+ const owned = new Set();
351
+ const files = [];
352
+ walkDesktop(root, files, max);
353
+ for (const file of files) {
354
+ let rec = null;
355
+ try { rec = JSON.parse(fs.readFileSync(file, 'utf8')); } catch { continue; }
356
+ if (typeof rec?.cliSessionId === 'string' && rec.cliSessionId) owned.add(rec.cliSessionId);
357
+ const when = Number(rec?.lastActivityAt || rec?.createdAt || 0);
358
+ if (!when || when < cutoff) { skipped.old += 1; continue; }
359
+ if (rec?.isArchived) { skipped.archived += 1; continue; }
360
+ const folder = String(rec?.originCwd || rec?.cwd || '');
361
+ if (isScratchFolder(folder, home)) { skipped.scratch += 1; continue; }
362
+ const title = threadTitle(rec?.title || '');
363
+ if (!title) { skipped.empty += 1; continue; }
364
+ out.push({
365
+ id: String(rec?.cliSessionId || rec?.sessionId || path.basename(file, '.json')),
366
+ source: 'desktop',
367
+ folder,
368
+ folderName: path.basename(folder),
369
+ short: shortFolder(folder, home),
370
+ title,
371
+ when,
372
+ path: file,
373
+ });
374
+ }
375
+ return { threads: out, skipped, owned };
376
+ }
377
+
378
+ function walkDesktop(dir, into, max, depth = 3) {
379
+ if (into.length >= max || depth < 0) return;
380
+ let entries = [];
381
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
382
+ for (const e of entries) {
383
+ if (into.length >= max) return;
384
+ const full = path.join(dir, e.name);
385
+ if (e.isDirectory()) walkDesktop(full, into, max, depth - 1);
386
+ else if (e.isFile() && e.name.endsWith('.json')) into.push(full);
387
+ }
388
+ }
389
+
390
+ /* ---------------------------------- BOTH ----------------------------------- */
391
+
392
+ /**
393
+ * BOTH SOURCES, NEWEST FIRST, THE HANDFUL SHE MEANT.
394
+ *
395
+ * `days` is her "last few days" and it is a parameter rather than a constant
396
+ * because the number is the one thing on this card somebody may want to argue
397
+ * about. Everything else here is a fact about the file.
398
+ *
399
+ * One row per thread and no folding by folder: the folder is on the row and
400
+ * the card groups by it, the same way it already groups agent files. */
401
+ export function readSessionThreads({
402
+ home = os.homedir(),
403
+ days = RECENT_DAYS,
404
+ now = Date.now(),
405
+ max = MAX_READS,
406
+ } = {}) {
407
+ const cutoff = now - Math.max(0, Number(days) || 0) * 86400 * 1000;
408
+ // The desktop side goes first so its ids are known before the transcripts are
409
+ // walked, which is what stops one thread appearing twice.
410
+ const desk = readDesktopThreads({ home, cutoff, max });
411
+ const term = readTerminalThreads({ home, cutoff, max, skipIds: desk.owned });
412
+ const threads = [...term.threads, ...desk.threads].sort((a, b) => b.when - a.when);
413
+ return {
414
+ threads,
415
+ skipped: { ...term.skipped, desktopOld: desk.skipped.old, archived: desk.skipped.archived },
416
+ };
417
+ }
418
+
419
+ /**
420
+ * THE FOLDERS THOSE THREADS RAN IN, each with its own threads under it.
421
+ *
422
+ * The import card is already a section per place the rows can land, so this is
423
+ * the shape it wants. Sorted by the newest thread in each, because the folder
424
+ * she touched an hour ago is the one she is looking for. */
425
+ export function threadsByFolder(threads = []) {
426
+ const byFolder = new Map();
427
+ for (const t of threads) {
428
+ const key = t.folder || '';
429
+ if (!byFolder.has(key)) {
430
+ byFolder.set(key, { folder: key, name: t.folderName, short: t.short, threads: [] });
431
+ }
432
+ byFolder.get(key).threads.push(t);
433
+ }
434
+ return [...byFolder.values()].sort((a, b) => (b.threads[0]?.when ?? 0) - (a.threads[0]?.when ?? 0));
435
+ }