agentbox-app 0.1.12 → 0.1.16
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.
- package/README.md +24 -9
- package/bin/agentbox.mjs +23 -1
- package/briefs/message-rules.md +36 -8
- package/briefs/worker.md +46 -23
- package/main/account-discovery.mjs +122 -30
- package/main/account-tooling.mjs +29 -1
- package/main/account-usage.mjs +46 -0
- package/main/acp.mjs +296 -0
- package/main/agent-homes.mjs +158 -0
- package/main/agent-sessions.mjs +311 -260
- package/main/agent-updates.mjs +9 -22
- package/main/agents.mjs +179 -466
- package/main/analytics.mjs +42 -2
- package/main/answer-modes.mjs +103 -37
- package/main/approval-prompt-core.mjs +197 -0
- package/main/approval-prompt-server.mjs +4 -138
- package/main/approvals.mjs +101 -297
- package/main/artifact-path.mjs +47 -18
- package/main/claude-bin.mjs +45 -311
- package/main/claude-input.mjs +57 -7
- package/main/claude-launch-env.mjs +54 -0
- package/main/claude-models.mjs +39 -7
- package/main/claude-plan.mjs +64 -16
- package/main/claude-session-open.mjs +28 -0
- package/main/claude-usage-process.mjs +40 -0
- package/main/claude-usage.mjs +94 -59
- package/main/cloud/account.mjs +179 -0
- package/main/cloud/aws.mjs +127 -0
- package/main/cloud/browser-return.mjs +58 -0
- package/main/cloud/clients.mjs +15 -0
- package/main/cloud/codex-cloud.mjs +53 -0
- package/main/cloud/config.mjs +107 -0
- package/main/cloud/docs.mjs +219 -0
- package/main/cloud/exec.mjs +27 -0
- package/main/cloud/host.mjs +165 -0
- package/main/cloud/kit.mjs +80 -0
- package/main/cloud/logins.mjs +543 -0
- package/main/cloud/managed.mjs +115 -0
- package/main/cloud/marker.mjs +104 -0
- package/main/cloud/org.mjs +53 -0
- package/main/cloud/reuse.mjs +208 -0
- package/main/cloud/run-cli.mjs +177 -0
- package/main/cloud/run.mjs +1934 -0
- package/main/cloud/usage.mjs +76 -0
- package/main/cloud-permission.mjs +51 -0
- package/main/codex-account.mjs +78 -0
- package/main/codex-app-server.mjs +276 -409
- package/main/codex-approvals.mjs +148 -433
- package/main/codex-bin.mjs +51 -249
- package/main/codex-models.mjs +20 -2
- package/main/codex-session.mjs +246 -506
- package/main/codex-threads-worker.mjs +12 -0
- package/main/codex-threads.mjs +112 -11
- package/main/codex-usage-file.mjs +5 -3
- package/main/codex-usage.mjs +56 -7
- package/main/codex-watch.mjs +83 -5
- package/main/codex.mjs +95 -363
- package/main/config.mjs +37 -5
- package/main/corner-tag.mjs +810 -0
- package/main/crash-report.mjs +26 -3
- package/main/dev-window.mjs +1 -1
- package/main/engine-bin.mjs +545 -0
- package/main/engine-setup.mjs +25 -24
- package/main/feedback.mjs +14 -31
- package/main/folder-pictures.mjs +2 -2
- package/main/freeze-recorder-thread.mjs +86 -0
- package/main/freeze-recorder.mjs +146 -0
- package/main/fresh-user.mjs +20 -9
- package/main/git-change-offthread.mjs +17 -3
- package/main/git-change-thread.mjs +5 -2
- package/main/harnesses/claude-stream.mjs +208 -0
- package/main/harnesses/claude.mjs +105 -0
- package/main/harnesses/codex.mjs +72 -0
- package/main/harnesses/common.mjs +78 -0
- package/main/harnesses/instructions-file.mjs +116 -0
- package/main/harnesses/opencode.mjs +234 -0
- package/main/harnesses.mjs +65 -0
- package/main/importable-threads.mjs +96 -0
- package/main/ipc.mjs +520 -135
- package/main/leftovers.mjs +117 -25
- package/main/liquid-glass.mjs +26 -0
- package/main/live-replies.mjs +10 -4
- package/main/local-app-frames.mjs +82 -0
- package/main/main-went-red.mjs +330 -0
- package/main/main.mjs +248 -37
- package/main/memory-gate-history.mjs +85 -20
- package/main/memory-gate-server.mjs +129 -11
- package/main/memory-gate.mjs +62 -8
- package/main/memory-log.mjs +124 -0
- package/main/menu.mjs +4 -2
- package/main/message-priority.mjs +2 -2
- package/main/mic.mjs +38 -0
- package/main/native/liquid-glass/build/liquid-glass.node +0 -0
- package/main/new-chats-watch.mjs +110 -0
- package/main/new-user-window.mjs +25 -0
- package/main/notify.mjs +64 -5
- package/main/opencode-server.mjs +271 -0
- package/main/opencode.mjs +287 -0
- package/main/phone-alerts.mjs +117 -0
- package/main/phone-inbox.mjs +625 -0
- package/main/phone-link.mjs +571 -0
- package/main/phone-uploads.mjs +476 -0
- package/main/picture-shape.mjs +114 -0
- package/main/quit-sequence.mjs +143 -0
- package/main/refresh-agent-models.mjs +15 -2
- package/main/relay-address.mjs +28 -0
- package/main/relay-client.mjs +314 -0
- package/main/repeats.mjs +10 -4
- package/main/row-label.mjs +18 -47
- package/main/run-change.mjs +13 -0
- package/main/run-ledger.mjs +135 -0
- package/main/runs-on.mjs +32 -35
- package/main/save-attachments.mjs +41 -0
- package/main/serve.mjs +155 -9
- package/main/settings.mjs +364 -50
- package/main/shell-path.mjs +234 -0
- package/main/ship-conflict-brief.mjs +119 -0
- package/main/ship-queue.mjs +134 -20
- package/main/sign-in-files.mjs +3 -7
- package/main/source-updater.mjs +45 -11
- package/main/stage-attachment.mjs +68 -0
- package/main/stall-watch.mjs +50 -0
- package/main/start-hold.mjs +73 -0
- package/main/store/home.mjs +87 -232
- package/main/store/paths.mjs +11 -0
- package/main/store/teammates-files.mjs +99 -0
- package/main/store/work-items.mjs +231 -13
- package/main/store.mjs +227 -53
- package/main/supervisor.mjs +3865 -699
- package/main/task-commands.mjs +13 -17
- package/main/task-folders-offthread.mjs +6 -1
- package/main/task-folders.mjs +897 -105
- package/main/task-terminals.mjs +147 -41
- package/main/team/index.mjs +114 -10
- package/main/team/memory-cloud.mjs +183 -1
- package/main/team/room-hours.mjs +48 -0
- package/main/team/room-setup.mjs +31 -0
- package/main/team/rooms-memory.mjs +454 -0
- package/main/team/rooms-shape.mjs +42 -0
- package/main/team/rooms-supabase.mjs +324 -0
- package/main/team/rooms.mjs +575 -0
- package/main/team/session.mjs +96 -26
- package/main/team/supabase-backend.mjs +180 -0
- package/main/team/sync.mjs +109 -1
- package/main/teammates.mjs +580 -0
- package/main/terminal-host.mjs +143 -0
- package/main/terminal-hosts.mjs +261 -0
- package/main/transcript-search.mjs +80 -0
- package/main/updater.mjs +71 -9
- package/main/webview-guard.mjs +45 -0
- package/main/worktree-include.mjs +365 -0
- package/mcp/core/documents.mjs +3 -1
- package/mcp/core/repeats.mjs +88 -0
- package/mcp/core/teammates.mjs +222 -0
- package/mcp/core/work.mjs +76 -4
- package/mcp/stdio.mjs +24 -11
- package/mcp/tools.mjs +116 -8
- package/package.json +30 -5
- package/renderer/dist/assets/abnfDiagram-YUKMZFIV-CoWB6YUK.js +1 -0
- package/renderer/dist/assets/arc-Ch0NrCul.js +1 -0
- package/renderer/dist/assets/architectureDiagram-47P4ROYG-CXz9M5tQ.js +36 -0
- package/renderer/dist/assets/blockDiagram-E7TT5TSB-DchGUuHH.js +140 -0
- package/renderer/dist/assets/c4Diagram-CRA5TL53-Cip-zNyr.js +38 -0
- package/renderer/dist/assets/channel-BQuQSL--.js +1 -0
- package/renderer/dist/assets/chunk-2Q5K7J3B-BoXid_2r.js +1 -0
- package/renderer/dist/assets/chunk-5VM5RSS4-Cpez1rRz.js +15 -0
- package/renderer/dist/assets/chunk-7TKQ45FW-DKA84YkE.js +1 -0
- package/renderer/dist/assets/chunk-AW2ZBBNX-Dg-SumPG.js +88 -0
- package/renderer/dist/assets/chunk-FVRAUYC3-06TCvPm1.js +2 -0
- package/renderer/dist/assets/chunk-HTAEGDNF-CMEBy1Td.js +1 -0
- package/renderer/dist/assets/chunk-JWPE2WC7-BJ2ObmV-.js +1 -0
- package/renderer/dist/assets/chunk-KQW6MTUR-CGWOYXUE.js +1 -0
- package/renderer/dist/assets/chunk-XXDRQBXY-CiZp07XC.js +1 -0
- package/renderer/dist/assets/classDiagram-v2-K4WV4PDN-DJNIX719.js +217 -0
- package/renderer/dist/assets/cornerTag-CPgvu-q3.js +1 -0
- package/renderer/dist/assets/cornerTag-oMfEjzCO.css +1 -0
- package/renderer/dist/assets/cose-bilkent-JH36ORCC-CbkyZOUH.js +1 -0
- package/renderer/dist/assets/cynefin-EF2NZ3EQ-78iOroZO.js +141 -0
- package/renderer/dist/assets/cynefinDiagram-3GCD6N5R-GcCopMs7.js +62 -0
- package/renderer/dist/assets/cytoscape.esm-C5JX3QLV.js +331 -0
- package/renderer/dist/assets/dagre-W4DXFKR2-DHLlXLkz.js +4 -0
- package/renderer/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
- package/renderer/dist/assets/diagram-2UJZ2QOL-DuZSUHcH.js +200 -0
- package/renderer/dist/assets/diagram-OVF4WLC6-Ckxu3cqH.js +24 -0
- package/renderer/dist/assets/diagram-PFPMY2P6-CsTw9Ycn.js +30 -0
- package/renderer/dist/assets/diagram-UMYRVEAY-CTXd8qVg.js +24 -0
- package/renderer/dist/assets/diagram-YEKJPTXX-CJs1T_9m.js +41 -0
- package/renderer/dist/assets/diagram-ZIFT7M5P-CvjI23mf.js +3 -0
- package/renderer/dist/assets/ebnfDiagram-VR2GEFS7-d_IeAzOJ.js +1 -0
- package/renderer/dist/assets/elk-IJKZMXRS-zo9KFP3N.js +27 -0
- package/renderer/dist/assets/erDiagram-O2IAPWRE-DqfYf5Dt.js +99 -0
- package/renderer/dist/assets/flowDiagram-OXPTDLAJ-DAWsMLmo.js +213 -0
- package/renderer/dist/assets/ganttDiagram-R7TSEDQI-4ml_bhwR.js +292 -0
- package/renderer/dist/assets/gitGraphDiagram-XJZIOB7I-B_WcqSId.js +106 -0
- package/renderer/dist/assets/graph-DGbsTEsI.js +1 -0
- package/renderer/dist/assets/infoDiagram-5W2HQ5XZ-CI-jYqzc.js +2 -0
- package/renderer/dist/assets/init-Gi6I4Gst.js +1 -0
- package/renderer/dist/assets/ishikawaDiagram-K3B6WC7H-B56VWRlV.js +70 -0
- package/renderer/dist/assets/journeyDiagram-COXZFDF6-D1-p09Lr.js +139 -0
- package/renderer/dist/assets/kanban-definition-P3RFRI5V-Cv3w7JoN.js +89 -0
- package/renderer/dist/assets/katex-C5jXJg4s.js +257 -0
- package/renderer/dist/assets/linear-C35V2Spi.js +1 -0
- package/renderer/dist/assets/main-BcoBiGwv.css +32 -0
- package/renderer/dist/assets/main-CLnIOulD.js +1191 -0
- package/renderer/dist/assets/mermaid.core-BzGT5Vo9.js +314 -0
- package/renderer/dist/assets/mindmap-definition-LZFPQGKD-BZUfuymC.js +96 -0
- package/renderer/dist/assets/ordinal-Cboi1Yqb.js +1 -0
- package/renderer/dist/assets/pegDiagram-BGZESJAR-DFh9XNGe.js +1 -0
- package/renderer/dist/assets/pieDiagram-CAPJLFHJ-C65tPVsy.js +39 -0
- package/renderer/dist/assets/product-name-CSNW6d7s.js +40 -0
- package/renderer/dist/assets/quadrantDiagram-GDMTTRAM-Cushj_nk.js +7 -0
- package/renderer/dist/assets/railroadDiagram-SM67HX2B-DZGebGx5.js +1 -0
- package/renderer/dist/assets/requirementDiagram-X7JNWC4A-DpAB0MNo.js +84 -0
- package/renderer/dist/assets/sankeyDiagram-UM26HJRW-D-29rHra.js +40 -0
- package/renderer/dist/assets/sequenceDiagram-ZO4K6R2Y-FfOQAZol.js +169 -0
- package/renderer/dist/assets/sizeCapture-INFHLROL-C_n6fDBY.js +1 -0
- package/renderer/dist/assets/stateDiagram-v2-TBUQTH76-BZjenzJG.js +291 -0
- package/renderer/dist/assets/swimlanes-N4OXWK64-DxaMU9vX.js +1 -0
- package/renderer/dist/assets/swimlanesDiagram-K3J5GTZL-Cb2kM6rr.js +8 -0
- package/renderer/dist/assets/timeline-definition-YOQAKGHF-DTg5HJKz.js +120 -0
- package/renderer/dist/assets/usecaseDiagram-VIAY4XPW-Bc3MZDsl.js +370 -0
- package/renderer/dist/assets/vennDiagram-BLWOH2XV-GIVGk9Id.js +34 -0
- package/renderer/dist/assets/wardleyDiagram-YMQ3BMBF-NmJnbQOg.js +78 -0
- package/renderer/dist/assets/xychartDiagram-TAQBALBS-p-kfGfnW.js +7 -0
- package/renderer/dist/corner-tag.html +13 -0
- package/renderer/dist/emulator/audio-rules.mjs +74 -0
- package/renderer/dist/emulator/data/compression/README.md +11 -0
- package/renderer/dist/emulator/data/compression/extract7z.js +1 -0
- package/renderer/dist/emulator/data/compression/extractzip.js +1 -0
- package/renderer/dist/emulator/data/compression/libunrar.js +519 -0
- package/renderer/dist/emulator/data/compression/libunrar.wasm +0 -0
- package/renderer/dist/emulator/data/cores/README.md +22 -0
- package/renderer/dist/emulator/data/cores/mgba-legacy-wasm.data +0 -0
- package/renderer/dist/emulator/data/cores/mgba-thread-legacy-wasm.data +0 -0
- package/renderer/dist/emulator/data/cores/mgba-thread-wasm.data +0 -0
- package/renderer/dist/emulator/data/cores/mgba-wasm.data +0 -0
- package/renderer/dist/emulator/data/cores/package.json +65 -0
- package/renderer/dist/emulator/data/cores/reports/mgba.json +1 -0
- package/renderer/dist/emulator/data/emulator.css +1614 -0
- package/renderer/dist/emulator/data/loader.js +170 -0
- package/renderer/dist/emulator/data/localization/README.md +63 -0
- package/renderer/dist/emulator/data/localization/af-FR.json +339 -0
- package/renderer/dist/emulator/data/localization/ar-AR.json +301 -0
- package/renderer/dist/emulator/data/localization/ben-BEN.json +301 -0
- package/renderer/dist/emulator/data/localization/de-GER.json +301 -0
- package/renderer/dist/emulator/data/localization/el-GR.json +301 -0
- package/renderer/dist/emulator/data/localization/en-US.json +339 -0
- package/renderer/dist/emulator/data/localization/es-ES.json +310 -0
- package/renderer/dist/emulator/data/localization/fa-AF.json +310 -0
- package/renderer/dist/emulator/data/localization/hi-HI.json +301 -0
- package/renderer/dist/emulator/data/localization/it-IT.json +302 -0
- package/renderer/dist/emulator/data/localization/ja-JA.json +301 -0
- package/renderer/dist/emulator/data/localization/jv-JV.json +301 -0
- package/renderer/dist/emulator/data/localization/ko-KO.json +336 -0
- package/renderer/dist/emulator/data/localization/pt-BR.json +361 -0
- package/renderer/dist/emulator/data/localization/retroarch.json +617 -0
- package/renderer/dist/emulator/data/localization/ro-RO.json +310 -0
- package/renderer/dist/emulator/data/localization/ru-RU.json +301 -0
- package/renderer/dist/emulator/data/localization/tr-TR.json +310 -0
- package/renderer/dist/emulator/data/localization/vi-VN.json +302 -0
- package/renderer/dist/emulator/data/localization/zh-CN.json +351 -0
- package/renderer/dist/emulator/data/src/GameManager.js +464 -0
- package/renderer/dist/emulator/data/src/compression.js +142 -0
- package/renderer/dist/emulator/data/src/emulator.js +6158 -0
- package/renderer/dist/emulator/data/src/gamepad.js +167 -0
- package/renderer/dist/emulator/data/src/nipplejs.js +1 -0
- package/renderer/dist/emulator/data/src/shaders.js +236 -0
- package/renderer/dist/emulator/data/src/socket.io.min.js +7 -0
- package/renderer/dist/emulator/data/src/storage.js +125 -0
- package/renderer/dist/emulator/data/version.json +5 -0
- package/renderer/dist/emulator/host.js +297 -0
- package/renderer/dist/emulator/index.html +28 -0
- package/renderer/dist/emulator/provenance.json +6 -0
- package/renderer/dist/index.html +16 -3
- package/scripts/after-install.mjs +30 -8
- package/scripts/approval-server.sh +24 -5
- package/scripts/before-publish.mjs +12 -10
- package/scripts/cloud-shim.pl +57 -0
- package/shared/account-names.d.mts +6 -0
- package/shared/account-names.mjs +15 -0
- package/shared/agent-hours.mjs +136 -0
- package/shared/agent-import.mjs +62 -34
- package/shared/agents.d.mts +1 -0
- package/shared/agents.mjs +126 -522
- package/shared/analytics-events.mjs +12 -0
- package/shared/approval-card.mjs +36 -0
- package/shared/bridge-map.mjs +45 -1
- package/shared/claude-code-shape.mjs +6 -0
- package/shared/claude-commands.generated.mjs +6 -4
- package/shared/claude-models.generated.mjs +6 -4
- package/shared/claude-usage.d.mts +1 -0
- package/shared/claude-usage.mjs +42 -1
- package/shared/cloud-hand-back.d.mts +7 -0
- package/shared/cloud-hand-back.mjs +20 -0
- package/shared/cloud-kit.d.mts +219 -0
- package/shared/cloud-kit.mjs +341 -0
- package/shared/cloud-login-name.mjs +4 -0
- package/shared/cloud-start.mjs +17 -0
- package/shared/cloud-versions.mjs +23 -0
- package/shared/codex-import.mjs +2 -2
- package/shared/codex-modes.d.mts +22 -9
- package/shared/codex-modes.mjs +67 -11
- package/shared/codex-usage.mjs +108 -2
- package/shared/corner-tag.d.mts +36 -0
- package/shared/corner-tag.mjs +396 -0
- package/shared/dead-run-trace.mjs +19 -2
- package/shared/engines.mjs +76 -300
- package/shared/feedback.d.mts +7 -0
- package/shared/feedback.mjs +25 -0
- package/shared/first-run-practice.mjs +17 -24
- package/shared/harness-definitions.mjs +111 -0
- package/shared/instruction-defaults.json +2 -2
- package/shared/key-caps.d.mts +14 -0
- package/shared/key-caps.mjs +68 -0
- package/shared/message-edits.d.mts +7 -0
- package/shared/message-edits.mjs +35 -0
- package/shared/move-thread.d.mts +24 -0
- package/shared/move-thread.mjs +112 -0
- package/shared/named-threads.mjs +72 -0
- package/shared/not-shipped.d.mts +22 -0
- package/shared/not-shipped.mjs +89 -0
- package/shared/notify-rules.d.mts +31 -0
- package/shared/notify-rules.mjs +55 -8
- package/shared/phone-files.mjs +28 -0
- package/shared/phone-settings.mjs +118 -0
- package/shared/phone-snapshot.d.mts +16 -0
- package/shared/phone-snapshot.mjs +348 -0
- package/shared/picture-formats.d.mts +1 -0
- package/shared/picture-formats.mjs +13 -0
- package/shared/provider-commands.mjs +10 -2
- package/shared/rank.d.mts +4 -1
- package/shared/rank.mjs +82 -19
- package/shared/relay-address.mjs +6 -0
- package/shared/relay-crypto.mjs +165 -0
- package/shared/repeats.d.mts +8 -0
- package/shared/repeats.mjs +33 -2
- package/shared/room-cards.mjs +167 -0
- package/shared/room-links.d.mts +4 -0
- package/shared/room-links.mjs +21 -0
- package/shared/runs-on.d.mts +3 -13
- package/shared/runs-on.mjs +31 -52
- package/shared/shipped-note.d.mts +4 -0
- package/shared/shipped-note.mjs +6 -0
- package/shared/shortcuts.mjs +24 -10
- package/shared/spawn-trouble.d.mts +4 -0
- package/shared/spawn-trouble.mjs +484 -30
- package/shared/team-rules.mjs +9 -2
- package/shared/teammates.d.mts +19 -0
- package/shared/teammates.mjs +756 -0
- package/shared/terminal-focus.d.mts +1 -0
- package/shared/terminal-focus.mjs +13 -0
- package/shared/terminal-input.d.mts +1 -0
- package/shared/terminal-input.mjs +30 -0
- package/shared/terminal-keys.d.mts +1 -0
- package/shared/terminal-keys.mjs +16 -0
- package/shared/terminal-size.d.mts +4 -0
- package/shared/terminal-size.mjs +29 -0
- package/shared/thread-cards.mjs +4 -1
- package/shared/usage.d.mts +12 -0
- package/shared/usage.mjs +56 -3
- package/shared/websites.d.mts +15 -0
- package/shared/websites.mjs +61 -0
- package/shared/where-it-runs-from.d.mts +43 -0
- package/shared/where-it-runs-from.mjs +155 -0
- package/shared/work-items.mjs +150 -29
- package/shared/work-lines.mjs +93 -245
- package/shared/xterm-invalid-range.d.mts +2 -0
- package/shared/xterm-invalid-range.mjs +58 -0
- package/renderer/dist/assets/index-ChKsQkRw.js +0 -1169
- package/renderer/dist/assets/index-ClBXnq3B.css +0 -32
package/main/agent-sessions.mjs
CHANGED
|
@@ -1,120 +1,60 @@
|
|
|
1
|
-
//
|
|
1
|
+
// Claude Code conversation threads for the import card. These are separate from
|
|
2
|
+
// the `.md` agent definitions that agent-files.mjs reads, and the card shows both.
|
|
2
3
|
//
|
|
3
|
-
//
|
|
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.
|
|
4
|
+
// A thread lives in one of two places:
|
|
8
5
|
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
6
|
+
// <claude data folder>/projects/<folder>/<id>.jsonl a terminal or editor
|
|
7
|
+
// session, one JSON object per line (data folders: main/agent-homes.mjs)
|
|
8
|
+
// ~/Library/Application Support/Claude/claude-code-sessions/**/local_<id>.json
|
|
9
|
+
// the desktop app's small record about a session, with a real `title`
|
|
12
10
|
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
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.
|
|
11
|
+
// Most transcripts on a machine running Agentbox were started by Agentbox. The
|
|
12
|
+
// `entrypoint` on every transcript row says who started the session, so the
|
|
13
|
+
// filter reads that rather than guessing from the prompt or the folder.
|
|
48
14
|
|
|
49
15
|
import fs from 'node:fs';
|
|
50
16
|
import os from 'node:os';
|
|
51
17
|
import path from 'node:path';
|
|
18
|
+
import { claudeDataDirs } from './agent-homes.mjs';
|
|
19
|
+
import { howTheChatEnded } from '../shared/agent-import.mjs';
|
|
52
20
|
|
|
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
|
-
*
|
|
63
|
-
* Ten since the launch list (w-db6f5e331e, 2026-10-05): "showing your recently
|
|
64
|
-
* active agents over the last 10 days". Codex reads the same number
|
|
21
|
+
/** How many days back counts as recent. Codex reads the same number
|
|
65
22
|
* (main/codex-threads.mjs), so the two halves of the card agree. */
|
|
66
23
|
export const RECENT_DAYS = 10;
|
|
67
24
|
|
|
68
|
-
/**
|
|
69
|
-
*
|
|
70
|
-
* turn is at the top of the file by construction, and the files are not small:
|
|
71
|
-
* the largest in her store on 2026-08-29 is 13MB, and there are hundreds. */
|
|
25
|
+
/** How much of a transcript is read to find its first human turn, which is at
|
|
26
|
+
* the top of the file. Transcripts run to many megabytes. */
|
|
72
27
|
const HEAD_BYTES = 96 * 1024;
|
|
73
28
|
|
|
74
|
-
/**
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
* ten days' terminal threads opened with rows of 240KB, 294KB and 486KB, were
|
|
79
|
-
* cut, lost their name and fell off the card. Eight megabytes is the Codex
|
|
80
|
-
* reader's own ceiling (main/codex-threads.mjs) and several screenshots. */
|
|
29
|
+
/** How far past the head one cut-off first message may be followed. A
|
|
30
|
+
* screenshot pasted into the first message is written into that row as
|
|
31
|
+
* base64, so the row can be hundreds of KB. Eight MB matches the Codex
|
|
32
|
+
* reader's ceiling (main/codex-threads.mjs). */
|
|
81
33
|
const FIRST_TURN_BYTES = 8 * 1024 * 1024;
|
|
82
34
|
|
|
83
|
-
/**
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
* cap that can drop the newest thread on the machine is not a safety rail. */
|
|
35
|
+
/** How much of a transcript's end is read for its title and last reply. */
|
|
36
|
+
const TAIL_BYTES = 256 * 1024;
|
|
37
|
+
/** The longest last reply kept, from its end, which is where the answer is. */
|
|
38
|
+
const SAID_CHARS = 8000;
|
|
39
|
+
/** The longest first prompt kept for an imported row's opening message. */
|
|
40
|
+
const FIRST_CHARS = 4000;
|
|
41
|
+
|
|
42
|
+
/** A cap on how many threads are kept, so opening the card is not a disk
|
|
43
|
+
* crawl. It caps reading, never looking: the terminal walk stats every file
|
|
44
|
+
* and sorts newest first before it opens any, so the cap only drops older
|
|
45
|
+
* threads. */
|
|
95
46
|
const MAX_READS = 1200;
|
|
96
|
-
//
|
|
97
|
-
// uses it says "Looking" meanwhile; three seconds reaches two thirds of her
|
|
98
|
-
// week's 9,073 transcripts and all of a normal Mac's.
|
|
47
|
+
// The card shows "Looking" while this runs.
|
|
99
48
|
const READ_BUDGET_MS = 3000;
|
|
100
49
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* A FOLDER NOBODY WORKS IN.
|
|
105
|
-
*
|
|
106
|
-
* `/private/var/folders/19/.../T/speedsize-6AGMVw-run-12` is a real cwd of a
|
|
107
|
-
* real Claude Code session, and it is one of twenty a measurement script of
|
|
108
|
-
* ours started in a loop. 258 of her 448 recent threads are that. Nobody opens
|
|
109
|
-
* a temp directory to work, so a thread that ran in one is not a thread she
|
|
110
|
-
* would recognise, and offering it is how a list of hundreds happens. */
|
|
50
|
+
/** A temp folder, where nobody works. Agentbox's own scripts and workers run
|
|
51
|
+
* Claude Code sessions there, so a thread from one is not the person's. */
|
|
111
52
|
export function isScratchFolder(folder, home = os.homedir()) {
|
|
112
53
|
const p = String(folder ?? '');
|
|
113
54
|
if (!p) return true;
|
|
114
|
-
// A
|
|
115
|
-
//
|
|
116
|
-
//
|
|
117
|
-
// one under `/var/folders` on purpose.
|
|
55
|
+
// A folder inside the home folder is the person's, whatever the path. This
|
|
56
|
+
// check comes first because a Mac can put a home folder anywhere, and the
|
|
57
|
+
// tests put one under `/var/folders`.
|
|
118
58
|
const here = path.resolve(String(home ?? ''));
|
|
119
59
|
if (here && (p === here || p.startsWith(`${here}/`))) return false;
|
|
120
60
|
return /^\/private\/tmp(\/|$)/.test(p)
|
|
@@ -123,14 +63,10 @@ export function isScratchFolder(folder, home = os.homedir()) {
|
|
|
123
63
|
|| /^\/var\/folders(\/|$)/.test(p);
|
|
124
64
|
}
|
|
125
65
|
|
|
126
|
-
/**
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
* character turned into `-`, so `/private/tmp/w-1` is `-private-tmp-w-1`.
|
|
131
|
-
* Of her 12,272 transcripts touched in ten days about 12,000 were in temp
|
|
132
|
-
* folders, and opening each one only to refuse it spent the card's three
|
|
133
|
-
* seconds before it reached her oldest threads. */
|
|
66
|
+
/** The same rule read off the folder Claude Code filed the transcript in, so
|
|
67
|
+
* a temp-folder transcript is refused without being opened. Claude Code names
|
|
68
|
+
* that folder after the working folder with every character that is not a
|
|
69
|
+
* letter or digit turned into `-`, so `/private/tmp/w-1` is `-private-tmp-w-1`. */
|
|
134
70
|
export function isScratchProjectDir(name, home = os.homedir()) {
|
|
135
71
|
const n = String(name ?? '');
|
|
136
72
|
const here = path.resolve(String(home ?? '')).replace(/[^a-zA-Z0-9]/g, '-');
|
|
@@ -138,60 +74,42 @@ export function isScratchProjectDir(name, home = os.homedir()) {
|
|
|
138
74
|
return /^-(private-)?(tmp|var-folders)(-|$)/.test(n);
|
|
139
75
|
}
|
|
140
76
|
|
|
141
|
-
/**
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
|
|
148
|
-
* the inbox into the inbox.
|
|
149
|
-
*
|
|
150
|
-
* An entrypoint we have never seen is treated as not hers. The cost of being
|
|
151
|
-
* wrong that way is one missing row on a card; the other way is the hundreds
|
|
152
|
-
* she is complaining about. */
|
|
77
|
+
/** Entrypoints a person starts. `cli` is a terminal, `claude-desktop` the
|
|
78
|
+
* desktop app, and `claude-vscode` the Claude Code extension in VS Code,
|
|
79
|
+
* Cursor and Windsurf. Everything else, mostly `sdk-cli`, was started by
|
|
80
|
+
* software, usually Agentbox's own workers and chats. An unknown entrypoint
|
|
81
|
+
* counts as not the person's: a missing row costs less than hundreds of
|
|
82
|
+
* wrong ones. `remote*` sessions run in the cloud, not on this disk. */
|
|
83
|
+
const BY_HAND = new Set(['cli', 'claude-desktop', 'claude-vscode']);
|
|
153
84
|
export function startedByHand(entrypoint) {
|
|
154
|
-
return entrypoint
|
|
85
|
+
return BY_HAND.has(entrypoint);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Which of the three places a person starts Claude Code a session came from. */
|
|
89
|
+
export function sourceOf(entrypoint) {
|
|
90
|
+
if (entrypoint === 'claude-desktop') return 'desktop';
|
|
91
|
+
return entrypoint === 'claude-vscode' ? 'editor' : 'terminal';
|
|
155
92
|
}
|
|
156
93
|
|
|
157
|
-
/**
|
|
158
|
-
*
|
|
159
|
-
* `<session>/subagents/`, one per fan-out, so the parent already stands for
|
|
160
|
-
* them and listing both says the same work twice. */
|
|
94
|
+
/** Claude Code files subagent transcripts under `<session>/subagents/`. The
|
|
95
|
+
* parent session already stands for that work. */
|
|
161
96
|
export function isSubagentPath(file) {
|
|
162
97
|
return /(^|\/)subagents\//.test(String(file ?? ''));
|
|
163
98
|
}
|
|
164
99
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
/**
|
|
168
|
-
* Lines a person did not type, which a transcript's first turn is often made
|
|
169
|
-
* of: the slash command wrapper, the resume caveat, and anything the harness
|
|
170
|
-
* injected. A thread named `<command-name>/clear</command-name>` is a thread
|
|
171
|
-
* she cannot recognise. */
|
|
100
|
+
/** Lines Claude Code writes into a first turn that a person did not type: the
|
|
101
|
+
* slash command wrapper, the resume caveat and injected reminders. */
|
|
172
102
|
const NOT_TYPED = [
|
|
173
103
|
/^<command-name>/, /^<command-message>/, /^<command-args>/, /^<local-command/,
|
|
174
104
|
/^<system-reminder>/, /^<user-memory/, /^Caveat: The messages below/,
|
|
175
105
|
/^This session is being continued from/, /^\[Request interrupted/,
|
|
176
106
|
];
|
|
177
107
|
|
|
178
|
-
/**
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
*
|
|
183
|
-
* conversation. The first human sentence is what the desktop app titles from
|
|
184
|
-
* too, and it is the line she would know the thread by.
|
|
185
|
-
*
|
|
186
|
-
* SEVERAL MESSAGES ARE OFFERED, NOT ONE, and that is a fix rather than a
|
|
187
|
-
* flourish. A session that opens with a slash command has the wrapper as its
|
|
188
|
-
* first user message and her actual words in the second, so reading only the
|
|
189
|
-
* first left four of her own threads in an outside repo nameless and
|
|
190
|
-
* therefore off the card. The first message that is something a person typed
|
|
191
|
-
* wins; a thread with no such message has no name and is dropped, which is the
|
|
192
|
-
* right outcome for a session that was never spoken to.
|
|
193
|
-
*
|
|
194
|
-
* Markdown is flattened rather than rendered, because this lands in a list. */
|
|
108
|
+
/** The row's name, from the first line a person typed. Terminal transcripts
|
|
109
|
+
* carry no title, so the conversation is all there is. Takes several messages
|
|
110
|
+
* because a session that opens with a slash command has the wrapper first and
|
|
111
|
+
* the person's words second. Returns '' when nobody typed anything, and the
|
|
112
|
+
* caller drops that thread. */
|
|
195
113
|
export function threadTitle(texts, max = 72) {
|
|
196
114
|
for (const text of Array.isArray(texts) ? texts : [texts]) {
|
|
197
115
|
const got = titleFromOne(text, max);
|
|
@@ -204,7 +122,7 @@ function titleFromOne(text, max) {
|
|
|
204
122
|
const raw = String(text ?? '').replace(/\r/g, '');
|
|
205
123
|
for (const line of raw.split('\n')) {
|
|
206
124
|
// `[Image #1]` and `[Pasted text #1 +40 lines]` are Claude Code's stand-ins
|
|
207
|
-
// for what was pasted
|
|
125
|
+
// for what was pasted.
|
|
208
126
|
const t = line.replace(/\[(?:Image|Pasted text) #\d+[^\]]*\]/g, '').trim();
|
|
209
127
|
if (!t) continue;
|
|
210
128
|
if (NOT_TYPED.some((re) => re.test(t))) return '';
|
|
@@ -222,27 +140,17 @@ function titleFromOne(text, max) {
|
|
|
222
140
|
return '';
|
|
223
141
|
}
|
|
224
142
|
|
|
225
|
-
/**
|
|
226
|
-
* `/Users/you/Desktop/dev/zero` → `~/Desktop/dev/zero`. Same reason as the
|
|
227
|
-
* one in agent-files.mjs: a path carrying her account name reads like a
|
|
228
|
-
* system string rather than a folder she knows. */
|
|
143
|
+
/** Writes a folder under home as `~/...`. */
|
|
229
144
|
export function shortFolder(folder, home = os.homedir()) {
|
|
230
145
|
const here = path.resolve(home);
|
|
231
146
|
const full = path.resolve(String(folder ?? '') || here);
|
|
232
147
|
return full === here || full.startsWith(`${here}/`) ? `~${full.slice(here.length)}` : full;
|
|
233
148
|
}
|
|
234
149
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
*
|
|
239
|
-
* started the session, which folder it ran in, and the first thing the person
|
|
240
|
-
* typed.
|
|
241
|
-
*
|
|
242
|
-
* Read as a fixed head rather than a whole file. `entrypoint` and `cwd` are on
|
|
243
|
-
* every message row and the first human turn is the first message, so all
|
|
244
|
-
* three are at the top, and the alternative is reading gigabytes to fill in a
|
|
245
|
-
* list. Her largest transcript is 13MB and there are hundreds. */
|
|
150
|
+
/** Reads a transcript's head for who started the session, which folder it ran
|
|
151
|
+
* in and the first messages the person typed. `entrypoint` and `cwd` are on
|
|
152
|
+
* every message row and the first human turn comes first, so a fixed head is
|
|
153
|
+
* enough. */
|
|
246
154
|
export function readTranscriptHead(file, bytes = HEAD_BYTES, most = FIRST_TURN_BYTES) {
|
|
247
155
|
let fd = null;
|
|
248
156
|
try {
|
|
@@ -250,16 +158,18 @@ export function readTranscriptHead(file, bytes = HEAD_BYTES, most = FIRST_TURN_B
|
|
|
250
158
|
const buf = Buffer.alloc(bytes);
|
|
251
159
|
const n = fs.readSync(fd, buf, 0, bytes, 0);
|
|
252
160
|
const text = buf.slice(0, n).toString('utf8');
|
|
253
|
-
// The last line of a
|
|
254
|
-
//
|
|
161
|
+
// The last line of a full-size read is usually cut in half, so it is set
|
|
162
|
+
// aside. When the whole file fit, the last line is complete and is kept.
|
|
255
163
|
const lines = text.split('\n');
|
|
256
|
-
const cut = lines.pop();
|
|
164
|
+
const cut = n < bytes ? null : lines.pop();
|
|
257
165
|
let cwd = null;
|
|
258
166
|
let entrypoint = null;
|
|
167
|
+
let firstAt = 0;
|
|
259
168
|
const said = [];
|
|
260
169
|
const take = (line) => {
|
|
261
170
|
let row;
|
|
262
171
|
try { row = JSON.parse(line); } catch { return; }
|
|
172
|
+
if (!firstAt && row.timestamp) firstAt = Date.parse(row.timestamp) || 0;
|
|
263
173
|
if (!cwd && typeof row.cwd === 'string' && row.cwd) cwd = row.cwd;
|
|
264
174
|
if (!entrypoint && typeof row.entrypoint === 'string' && row.entrypoint) entrypoint = row.entrypoint;
|
|
265
175
|
if (row.type === 'user' && said.length < 5) {
|
|
@@ -272,29 +182,81 @@ export function readTranscriptHead(file, bytes = HEAD_BYTES, most = FIRST_TURN_B
|
|
|
272
182
|
take(line);
|
|
273
183
|
if (cwd && entrypoint && said.length >= 5) break;
|
|
274
184
|
}
|
|
275
|
-
//
|
|
276
|
-
// pasted into it
|
|
277
|
-
//
|
|
278
|
-
//
|
|
279
|
-
|
|
280
|
-
// never read past their head.
|
|
281
|
-
if (!said.length && n === bytes && cut && /"type":"user"/.test(cut.slice(0, 2048))
|
|
185
|
+
// If the cut line is the first user message, cut because a screenshot was
|
|
186
|
+
// pasted into it, read it to its end, up to `most`. Only when nothing typed
|
|
187
|
+
// was found and the session is not known to be the app's own, so worker
|
|
188
|
+
// transcripts are never read past their head.
|
|
189
|
+
if (!said.length && n === bytes && cut && /"type"\s*:\s*"user"/.test(cut.slice(0, 2048))
|
|
282
190
|
&& (!entrypoint || startedByHand(entrypoint))) {
|
|
283
191
|
const rest = restOfLine(fd, n, most - n);
|
|
284
192
|
if (rest) take(Buffer.concat([buf.subarray(buf.lastIndexOf(0x0a, n - 1) + 1, n), rest]).toString('utf8'));
|
|
285
193
|
}
|
|
286
|
-
return { cwd, entrypoint, said };
|
|
194
|
+
return { cwd, entrypoint, said, firstAt };
|
|
287
195
|
} catch {
|
|
288
|
-
return { cwd: null, entrypoint: null, said: [] };
|
|
196
|
+
return { cwd: null, entrypoint: null, said: [], firstAt: 0 };
|
|
289
197
|
} finally {
|
|
290
198
|
if (fd !== null) { try { fs.closeSync(fd); } catch { /* nothing to do */ } }
|
|
291
199
|
}
|
|
292
200
|
}
|
|
293
201
|
|
|
294
|
-
/**
|
|
295
|
-
*
|
|
296
|
-
|
|
297
|
-
|
|
202
|
+
/** A `user` line a person typed, as opposed to a tool result, a reminder or a
|
|
203
|
+
* command's wrapper. Only these end the agent's reply. */
|
|
204
|
+
function typedTurn(row) {
|
|
205
|
+
if (row?.type !== 'user' || row.toolUseResult || row.isMeta || row.isCompactSummary) return false;
|
|
206
|
+
return threadTitle(messageText(row)) !== '';
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** Reads a transcript's end for Claude Code's own title (`ai-title`, the last
|
|
210
|
+
* one wins) and the agent's last reply, every text block of it since the last
|
|
211
|
+
* thing a person typed. */
|
|
212
|
+
export function readTranscriptTail(file, bytes = TAIL_BYTES) {
|
|
213
|
+
let fd = null;
|
|
214
|
+
try {
|
|
215
|
+
fd = fs.openSync(file, 'r');
|
|
216
|
+
const size = fs.fstatSync(fd).size;
|
|
217
|
+
const from = Math.max(0, size - bytes);
|
|
218
|
+
const buf = Buffer.alloc(size - from);
|
|
219
|
+
const n = fs.readSync(fd, buf, 0, buf.length, from);
|
|
220
|
+
const lines = buf.slice(0, n).toString('utf8').split('\n');
|
|
221
|
+
if (from > 0) lines.shift();
|
|
222
|
+
let aiTitle = '';
|
|
223
|
+
let reply = '';
|
|
224
|
+
for (const line of lines) {
|
|
225
|
+
if (!line.trim()) continue;
|
|
226
|
+
let row;
|
|
227
|
+
try { row = JSON.parse(line); } catch { continue; }
|
|
228
|
+
if (row.type === 'ai-title' && row.aiTitle) aiTitle = String(row.aiTitle);
|
|
229
|
+
else if (typedTurn(row)) reply = '';
|
|
230
|
+
else if (row.type === 'assistant') {
|
|
231
|
+
const text = messageText(row).trim();
|
|
232
|
+
if (text) reply = reply ? `${reply}\n\n${text}` : text;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return { aiTitle, lastSaid: reply.slice(-SAID_CHARS) };
|
|
236
|
+
} catch {
|
|
237
|
+
return { aiTitle: '', lastSaid: '' };
|
|
238
|
+
} finally {
|
|
239
|
+
if (fd !== null) { try { fs.closeSync(fd); } catch { /* nothing to do */ } }
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** What a finished read knows about how a chat ended, for its row and the
|
|
244
|
+
* setup card. */
|
|
245
|
+
function endOf(file) {
|
|
246
|
+
const { aiTitle, lastSaid } = file ? readTranscriptTail(file) : { aiTitle: '', lastSaid: '' };
|
|
247
|
+
return { aiTitle, lastSaid, ...howTheChatEnded(lastSaid) };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** The first message a person typed, whole, for an imported row's opening. */
|
|
251
|
+
function firstTyped(said) {
|
|
252
|
+
for (const text of said) {
|
|
253
|
+
if (threadTitle(text)) return String(text).trim().slice(0, FIRST_CHARS);
|
|
254
|
+
}
|
|
255
|
+
return '';
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** The bytes from `from` up to the next newline or the end of the file, or
|
|
259
|
+
* null if neither comes within `budget` bytes. */
|
|
298
260
|
function restOfLine(fd, from, budget) {
|
|
299
261
|
const parts = [];
|
|
300
262
|
let at = from;
|
|
@@ -310,9 +272,7 @@ function restOfLine(fd, from, budget) {
|
|
|
310
272
|
return null;
|
|
311
273
|
}
|
|
312
274
|
|
|
313
|
-
/**
|
|
314
|
-
* The words out of one transcript row, whichever of the two shapes it is in.
|
|
315
|
-
* Content is a string on the plain ones and a list of blocks on the rest. */
|
|
275
|
+
/** A row's content is either a string or a list of blocks. */
|
|
316
276
|
function messageText(row) {
|
|
317
277
|
const c = row?.message?.content;
|
|
318
278
|
if (typeof c === 'string') return c;
|
|
@@ -325,77 +285,125 @@ function messageText(row) {
|
|
|
325
285
|
return '';
|
|
326
286
|
}
|
|
327
287
|
|
|
328
|
-
/**
|
|
329
|
-
*
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
* filters, and a session that changes one of them should be able to see what
|
|
333
|
-
* it changed rather than take this comment's word for it. */
|
|
334
|
-
function readTerminalThreads({ home, cutoff, max, skipIds = new Set() }) {
|
|
335
|
-
const root = path.join(home, '.claude', 'projects');
|
|
336
|
-
const skipped = { old: 0, scratch: 0, started: 0, subagent: 0, empty: 0, unread: 0, desktopDupe: 0 };
|
|
288
|
+
/** `skipped` counts why each transcript was left out, so a change to a filter
|
|
289
|
+
* can be measured. */
|
|
290
|
+
function readTerminalThreads({ home, dirs, cutoff, max, budgetMs = READ_BUDGET_MS, skipIds = new Set() }) {
|
|
291
|
+
const skipped = { old: 0, scratch: 0, started: 0, subagent: 0, empty: 0, unread: 0, desktopDupe: 0, copy: 0 };
|
|
337
292
|
const out = [];
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
// Stat everything, keep the recent, newest first, and only then start opening
|
|
341
|
-
// files. This is the order that makes `max` safe: whatever it cuts off is
|
|
342
|
-
// older than everything it kept.
|
|
293
|
+
// Every data folder is pooled and sorted newest first before any file is
|
|
294
|
+
// opened, so `max` and the time budget only ever cut off older sessions.
|
|
343
295
|
const recent = [];
|
|
344
|
-
for (const
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
296
|
+
for (const dir of dirs) {
|
|
297
|
+
const root = path.join(dir, 'projects');
|
|
298
|
+
const all = [];
|
|
299
|
+
walkTranscripts(root, all);
|
|
300
|
+
for (const file of all) {
|
|
301
|
+
let when = 0;
|
|
302
|
+
try { when = fs.statSync(file).mtimeMs; } catch { continue; }
|
|
303
|
+
if (when < cutoff) { skipped.old += 1; continue; }
|
|
304
|
+
if (isSubagentPath(file.slice(root.length))) { skipped.subagent += 1; continue; }
|
|
305
|
+
if (isScratchProjectDir(path.relative(root, file).split(path.sep)[0], home)) { skipped.scratch += 1; continue; }
|
|
306
|
+
recent.push({ file, when });
|
|
307
|
+
}
|
|
351
308
|
}
|
|
352
309
|
recent.sort((a, b) => b.when - a.when);
|
|
353
|
-
//
|
|
354
|
-
//
|
|
355
|
-
|
|
356
|
-
//
|
|
357
|
-
//
|
|
358
|
-
// all. Somebody running dozens of agents fills this folder with ours. So it
|
|
359
|
-
// opens newest first until it has `max` of the person's own, or until
|
|
360
|
-
// READ_BUDGET_MS has gone, whichever is first. Reading all 9,073 heads took
|
|
361
|
-
// 5.0s there.
|
|
310
|
+
// One session is one row even when two data folders hold a copy. Newest
|
|
311
|
+
// first, so the copy kept is the one still being written to.
|
|
312
|
+
const seenIds = new Set();
|
|
313
|
+
// The cap counts threads kept, not files opened: on a machine running many
|
|
314
|
+
// workers, the newest thousand transcripts can all be Agentbox's own.
|
|
362
315
|
const started = Date.now();
|
|
363
316
|
let opened = 0;
|
|
317
|
+
// The moment the read gave up at, zero when it read the whole window. A
|
|
318
|
+
// name is only known once the file is open, so a search that stopped in
|
|
319
|
+
// August must not answer "no such chat" as if it had looked at September.
|
|
320
|
+
let reached = 0;
|
|
321
|
+
let lastOpened = 0;
|
|
322
|
+
// The desktop app's own record wins over its transcript, but how the chat
|
|
323
|
+
// ended is only in the transcript.
|
|
324
|
+
const desktopFiles = new Map();
|
|
364
325
|
for (const { file, when } of recent) {
|
|
365
|
-
if (out.length >= max || Date.now() - started >
|
|
326
|
+
if (out.length >= max || Date.now() - started > budgetMs) {
|
|
366
327
|
skipped.unread = recent.length - opened;
|
|
328
|
+
reached = lastOpened;
|
|
367
329
|
break;
|
|
368
330
|
}
|
|
369
331
|
opened += 1;
|
|
332
|
+
lastOpened = when;
|
|
370
333
|
const id = path.basename(file, '.jsonl');
|
|
371
|
-
//
|
|
372
|
-
//
|
|
373
|
-
//
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
334
|
+
// The desktop app writes a thread twice, as a transcript here and as its
|
|
335
|
+
// own record joined by `cliSessionId`. Its record wins because it has a
|
|
336
|
+
// real title.
|
|
337
|
+
if (skipIds.has(id)) {
|
|
338
|
+
skipped.desktopDupe += 1;
|
|
339
|
+
if (!desktopFiles.has(id)) desktopFiles.set(id, file);
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
if (seenIds.has(id)) { skipped.copy += 1; continue; }
|
|
343
|
+
const { cwd, entrypoint, said, firstAt } = readTranscriptHead(file);
|
|
344
|
+
// Only a readable copy counts, so a newer copy cut off mid-write does not
|
|
345
|
+
// hide a good older one.
|
|
346
|
+
if (cwd || entrypoint) seenIds.add(id);
|
|
377
347
|
if (!startedByHand(entrypoint)) { skipped.started += 1; continue; }
|
|
378
348
|
if (isScratchFolder(cwd, home)) { skipped.scratch += 1; continue; }
|
|
379
349
|
const title = threadTitle(said);
|
|
380
350
|
if (!title) { skipped.empty += 1; continue; }
|
|
381
351
|
out.push({
|
|
382
352
|
id,
|
|
383
|
-
source: entrypoint
|
|
353
|
+
source: sourceOf(entrypoint),
|
|
384
354
|
folder: cwd,
|
|
385
355
|
folderName: path.basename(cwd),
|
|
386
356
|
short: shortFolder(cwd, home),
|
|
387
357
|
title,
|
|
388
358
|
when,
|
|
389
359
|
path: file,
|
|
360
|
+
firstSaid: firstTyped(said),
|
|
361
|
+
startedAt: firstAt || when,
|
|
390
362
|
});
|
|
391
363
|
}
|
|
392
|
-
return { threads: out, skipped };
|
|
364
|
+
return { threads: out, skipped, reached, desktopFiles };
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/** How many of the newest threads get their end read, and for how long. The
|
|
368
|
+
* head walk above has its own budget, so this one only ever adds detail to
|
|
369
|
+
* threads already listed. */
|
|
370
|
+
const ENDS_MOST = 120;
|
|
371
|
+
const ENDS_BUDGET_MS = 2000;
|
|
372
|
+
|
|
373
|
+
function withEnds(threads, desktopFiles) {
|
|
374
|
+
const started = Date.now();
|
|
375
|
+
return threads.map((t, i) => {
|
|
376
|
+
const file = t.source === 'desktop' ? desktopFiles.get(t.id) ?? null : t.path;
|
|
377
|
+
if (i >= ENDS_MOST || Date.now() - started > ENDS_BUDGET_MS || !file) {
|
|
378
|
+
// Not "finished": nothing is known of how it ended until it is picked.
|
|
379
|
+
return { ...t, waiting: false, ask: '', lastSaid: '', endUnknown: true };
|
|
380
|
+
}
|
|
381
|
+
const end = endOf(file);
|
|
382
|
+
const firstSaid = t.firstSaid ?? firstTyped(readTranscriptHead(file).said);
|
|
383
|
+
return {
|
|
384
|
+
...t,
|
|
385
|
+
title: t.source === 'desktop' ? t.title : threadTitle(end.aiTitle) || t.title,
|
|
386
|
+
waiting: end.waiting,
|
|
387
|
+
ask: end.ask,
|
|
388
|
+
lastSaid: end.lastSaid,
|
|
389
|
+
firstSaid,
|
|
390
|
+
};
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/** The chats somebody picked, with the end read for any the list left unread
|
|
395
|
+
* (past its cap, or found by name). Codex and desktop chats come read. */
|
|
396
|
+
export function readChatEnds(threads) {
|
|
397
|
+
return threads.map((t) => {
|
|
398
|
+
if (t.source === 'codex' || t.source === 'desktop' || !t.path) return t;
|
|
399
|
+
if (t.lastSaid !== undefined && !t.endUnknown) return t;
|
|
400
|
+
const { endUnknown, ...rest } = t;
|
|
401
|
+
return withEnds([rest], new Map())[0];
|
|
402
|
+
});
|
|
393
403
|
}
|
|
394
404
|
|
|
395
|
-
/**
|
|
396
|
-
*
|
|
397
|
-
* filter above can count them rather than the walk hiding them. Unbounded on
|
|
398
|
-
* purpose: this only lists names, and the cap that matters is on opening. */
|
|
405
|
+
/** Lists every transcript, subagents included so the caller can count them.
|
|
406
|
+
* Not capped: listing names is cheap, and the cap is on opening. */
|
|
399
407
|
function walkTranscripts(dir, into, depth = 4) {
|
|
400
408
|
if (depth < 0) return;
|
|
401
409
|
let entries = [];
|
|
@@ -407,30 +415,17 @@ function walkTranscripts(dir, into, depth = 4) {
|
|
|
407
415
|
}
|
|
408
416
|
}
|
|
409
417
|
|
|
410
|
-
/* ------------------------------- THE DESKTOP SIDE -------------------------- */
|
|
411
|
-
|
|
412
|
-
/** Where the desktop app keeps what it knows about a session. */
|
|
413
418
|
export function desktopSessionsDir(home = os.homedir()) {
|
|
414
419
|
return path.join(home, 'Library', 'Application Support', 'Claude', 'claude-code-sessions');
|
|
415
420
|
}
|
|
416
421
|
|
|
417
|
-
/**
|
|
418
|
-
*
|
|
419
|
-
*
|
|
420
|
-
* Its record is small and it carries a real `title` the app wrote, plus the
|
|
421
|
-
* folder, the branch and the moment of the last turn. So there is no
|
|
422
|
-
* transcript to read and no name to guess: measured on her Mac 2026-08-29, 38
|
|
423
|
-
* records, every one of them titled, for example "Fix duplicate cowork task
|
|
424
|
-
* display issue".
|
|
425
|
-
*
|
|
426
|
-
* Archived ones are left out. She archived them, which is the desktop app's
|
|
427
|
-
* word for done with. */
|
|
422
|
+
/** The desktop app's record carries a real `title`, the folder and the time of
|
|
423
|
+
* the last turn, so no transcript is read. Archived sessions are left out. */
|
|
428
424
|
function readDesktopThreads({ home, cutoff, max }) {
|
|
429
425
|
const root = desktopSessionsDir(home);
|
|
430
426
|
const skipped = { old: 0, scratch: 0, archived: 0, empty: 0 };
|
|
431
427
|
const out = [];
|
|
432
|
-
// Every id this side owns, recent or not, so the transcript walk
|
|
433
|
-
// them alone rather than draw the same thread under a worse name.
|
|
428
|
+
// Every id this side owns, recent or not, so the transcript walk skips them.
|
|
434
429
|
const owned = new Set();
|
|
435
430
|
const files = [];
|
|
436
431
|
walkDesktop(root, files, max);
|
|
@@ -454,6 +449,7 @@ function readDesktopThreads({ home, cutoff, max }) {
|
|
|
454
449
|
title,
|
|
455
450
|
when,
|
|
456
451
|
path: file,
|
|
452
|
+
startedAt: Number(rec?.createdAt || when),
|
|
457
453
|
});
|
|
458
454
|
}
|
|
459
455
|
return { threads: out, skipped, owned };
|
|
@@ -471,41 +467,96 @@ function walkDesktop(dir, into, max, depth = 3) {
|
|
|
471
467
|
}
|
|
472
468
|
}
|
|
473
469
|
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
/**
|
|
477
|
-
* BOTH SOURCES, NEWEST FIRST, THE HANDFUL SHE MEANT.
|
|
478
|
-
*
|
|
479
|
-
* `days` is her "last few days" and it is a parameter rather than a constant
|
|
480
|
-
* because the number is the one thing on this card somebody may want to argue
|
|
481
|
-
* about. Everything else here is a fact about the file.
|
|
482
|
-
*
|
|
483
|
-
* One row per thread and no folding by folder: the folder is on the row and
|
|
484
|
-
* the card groups by it, the same way it already groups agent files. */
|
|
470
|
+
/** Terminal, editor and desktop threads a person started, newest first, one
|
|
471
|
+
* row per thread. */
|
|
485
472
|
export function readSessionThreads({
|
|
486
473
|
home = os.homedir(),
|
|
474
|
+
// The app passes the data folders it found with its own environment and
|
|
475
|
+
// settings. The default is the folders under `home`, ignoring whatever
|
|
476
|
+
// CLAUDE_CONFIG_DIR the calling process has.
|
|
477
|
+
dirs = claudeDataDirs({ home }),
|
|
487
478
|
days = RECENT_DAYS,
|
|
488
479
|
now = Date.now(),
|
|
489
480
|
max = MAX_READS,
|
|
481
|
+
// The card's list keeps the three seconds it was measured for; a search is
|
|
482
|
+
// allowed longer, reaching back a year with somebody waiting on it.
|
|
483
|
+
budgetMs = READ_BUDGET_MS,
|
|
484
|
+
// Whether to read each listed chat's end too: its own title, its last reply
|
|
485
|
+
// and whether it is waiting on you. The year-long search does not need it.
|
|
486
|
+
ends = true,
|
|
490
487
|
} = {}) {
|
|
491
488
|
const cutoff = now - Math.max(0, Number(days) || 0) * 86400 * 1000;
|
|
492
|
-
// The desktop side goes first so its ids are known before the transcripts
|
|
493
|
-
// walked, which
|
|
489
|
+
// The desktop side goes first so its ids are known before the transcripts
|
|
490
|
+
// are walked, which stops one thread appearing twice.
|
|
494
491
|
const desk = readDesktopThreads({ home, cutoff, max });
|
|
495
|
-
const term = readTerminalThreads({ home, cutoff, max, skipIds: desk.owned });
|
|
496
|
-
const
|
|
492
|
+
const term = readTerminalThreads({ home, dirs, cutoff, max, budgetMs, skipIds: desk.owned });
|
|
493
|
+
const listed = [...term.threads, ...desk.threads].sort((a, b) => b.when - a.when);
|
|
494
|
+
const threads = ends ? withEnds(listed, term.desktopFiles) : listed;
|
|
497
495
|
return {
|
|
498
496
|
threads,
|
|
499
497
|
skipped: { ...term.skipped, desktopOld: desk.skipped.old, archived: desk.skipped.archived },
|
|
498
|
+
/** Zero when the whole window was read. See `reached` above. */
|
|
499
|
+
reached: term.reached,
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/* -------------------- AND THE ONE THE NEW-CHATS WATCH USES ------------------ */
|
|
504
|
+
// The watch (main/new-chats-watch.mjs) runs every minute on the main process,
|
|
505
|
+
// so it reads only the folders the projects point at, which also leaves out
|
|
506
|
+
// Agentbox's own runs in their task folders, only files changed since the
|
|
507
|
+
// switch went on, and each file once until it changes.
|
|
508
|
+
|
|
509
|
+
const PROJECT_CHATS_KEPT = 2000;
|
|
510
|
+
const projectChats = new Map();
|
|
511
|
+
const slugOf = (folder) => String(folder ?? '').replace(/[^a-zA-Z0-9]/g, '-');
|
|
512
|
+
const sameFolder = (a, b) => String(a ?? '').replace(/\/+$/, '') === String(b ?? '').replace(/\/+$/, '');
|
|
513
|
+
|
|
514
|
+
function readOneChat(file, { id, folder, home, when }) {
|
|
515
|
+
const { cwd, entrypoint, said, firstAt } = readTranscriptHead(file);
|
|
516
|
+
if (!startedByHand(entrypoint) || !sameFolder(cwd, folder)) return null;
|
|
517
|
+
const title = threadTitle(said);
|
|
518
|
+
if (!title) return null;
|
|
519
|
+
const end = endOf(file);
|
|
520
|
+
return {
|
|
521
|
+
id, source: sourceOf(entrypoint), folder: cwd, folderName: path.basename(cwd), short: shortFolder(cwd, home),
|
|
522
|
+
title: threadTitle(end.aiTitle) || title, when, path: file,
|
|
523
|
+
waiting: end.waiting, ask: end.ask, lastSaid: end.lastSaid, firstSaid: firstTyped(said), startedAt: firstAt || when,
|
|
500
524
|
};
|
|
501
525
|
}
|
|
502
526
|
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
527
|
+
export function readProjectChats({ home = os.homedir(), dirs = claudeDataDirs({ home }), folders = [], since = 0 } = {}) {
|
|
528
|
+
const out = [];
|
|
529
|
+
const seen = new Set();
|
|
530
|
+
for (const dir of dirs) {
|
|
531
|
+
for (const folder of folders) {
|
|
532
|
+
const at = path.join(dir, 'projects', slugOf(folder));
|
|
533
|
+
let names = [];
|
|
534
|
+
try { names = fs.readdirSync(at); } catch { continue; }
|
|
535
|
+
for (const name of names) {
|
|
536
|
+
if (!name.endsWith('.jsonl')) continue;
|
|
537
|
+
const id = name.slice(0, -'.jsonl'.length);
|
|
538
|
+
if (seen.has(id)) continue;
|
|
539
|
+
const file = path.join(at, name);
|
|
540
|
+
let st;
|
|
541
|
+
try { st = fs.statSync(file); } catch { continue; }
|
|
542
|
+
if (st.mtimeMs < since) continue;
|
|
543
|
+
const kept = projectChats.get(file);
|
|
544
|
+
let thread = kept?.thread ?? null;
|
|
545
|
+
if (!kept || kept.size !== st.size || kept.mtimeMs !== st.mtimeMs) {
|
|
546
|
+
thread = readOneChat(file, { id, folder, home, when: st.mtimeMs });
|
|
547
|
+
projectChats.delete(file);
|
|
548
|
+
projectChats.set(file, { size: st.size, mtimeMs: st.mtimeMs, thread });
|
|
549
|
+
if (projectChats.size > PROJECT_CHATS_KEPT) projectChats.delete(projectChats.keys().next().value);
|
|
550
|
+
}
|
|
551
|
+
if (thread) { seen.add(id); out.push(thread); }
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
return out.sort((a, b) => b.when - a.when);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/** Groups threads by folder, each folder sorted by its newest thread. Expects
|
|
559
|
+
* the threads newest first, as readSessionThreads returns them. */
|
|
509
560
|
export function threadsByFolder(threads = []) {
|
|
510
561
|
const byFolder = new Map();
|
|
511
562
|
for (const t of threads) {
|