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.
- package/LICENSE +674 -0
- package/README.md +176 -0
- package/bin/agentbox.mjs +103 -0
- package/briefs/adhd-mode.md +5 -0
- package/briefs/message-rules.md +95 -0
- package/briefs/worker.md +230 -0
- package/briefs/writing-rules.md +336 -0
- package/main/account-discovery.mjs +145 -0
- package/main/account-tooling.mjs +261 -0
- package/main/agent-activity.mjs +56 -0
- package/main/agent-files.mjs +422 -0
- package/main/agent-schedule.mjs +254 -0
- package/main/agent-sessions.mjs +435 -0
- package/main/agents.mjs +1095 -0
- package/main/analytics.mjs +206 -0
- package/main/answer-modes.mjs +206 -0
- package/main/approval-prompt-server.mjs +160 -0
- package/main/approvals.mjs +487 -0
- package/main/artifact-path.mjs +144 -0
- package/main/claude-bin.mjs +311 -0
- package/main/claude-input.mjs +117 -0
- package/main/claude-models.mjs +244 -0
- package/main/claude-plan.mjs +148 -0
- package/main/claude-usage.mjs +149 -0
- package/main/code-change.mjs +383 -0
- package/main/code-file.mjs +116 -0
- package/main/codex-account.mjs +146 -0
- package/main/codex-app-server.mjs +796 -0
- package/main/codex-approvals.mjs +712 -0
- package/main/codex-bin.mjs +261 -0
- package/main/codex-compaction.mjs +49 -0
- package/main/codex-models.mjs +194 -0
- package/main/codex-session.mjs +797 -0
- package/main/codex-threads.mjs +268 -0
- package/main/codex-usage-file.mjs +192 -0
- package/main/codex-usage.mjs +149 -0
- package/main/codex-watch.mjs +35 -0
- package/main/codex.mjs +757 -0
- package/main/config.mjs +345 -0
- package/main/copy-selection.mjs +55 -0
- package/main/crash-report.mjs +158 -0
- package/main/demo.mjs +254 -0
- package/main/dev-window.mjs +103 -0
- package/main/doc-file.mjs +99 -0
- package/main/doc-scheme.mjs +126 -0
- package/main/first-run.mjs +190 -0
- package/main/folders.mjs +91 -0
- package/main/fresh-user.mjs +120 -0
- package/main/git-change.mjs +323 -0
- package/main/img-scheme.mjs +90 -0
- package/main/instruction-settings.mjs +205 -0
- package/main/ipc.mjs +1558 -0
- package/main/live-replies.mjs +77 -0
- package/main/machine.mjs +113 -0
- package/main/main.mjs +972 -0
- package/main/menu.mjs +177 -0
- package/main/notify.mjs +155 -0
- package/main/open-source-file.mjs +8 -0
- package/main/project-identity.mjs +149 -0
- package/main/provider-commands.mjs +32 -0
- package/main/rail-note.mjs +74 -0
- package/main/repeats.mjs +261 -0
- package/main/row-label.mjs +185 -0
- package/main/screen-detail.mjs +44 -0
- package/main/serve.mjs +262 -0
- package/main/settings.mjs +912 -0
- package/main/staleness.mjs +76 -0
- package/main/store/chats.mjs +188 -0
- package/main/store/home.mjs +383 -0
- package/main/store/migrate.mjs +40 -0
- package/main/store/paths.mjs +82 -0
- package/main/store/pending-writes.mjs +122 -0
- package/main/store/project-lock.mjs +200 -0
- package/main/store/project.mjs +523 -0
- package/main/store/queues.mjs +31 -0
- package/main/store/work-items.mjs +299 -0
- package/main/store/write-queue.mjs +14 -0
- package/main/store-modules.mjs +23 -0
- package/main/store.mjs +1269 -0
- package/main/supervisor.mjs +6875 -0
- package/main/task-commands.mjs +161 -0
- package/main/task-folders-offthread.mjs +84 -0
- package/main/task-folders-thread.mjs +13 -0
- package/main/task-folders.mjs +409 -0
- package/main/task-remote-control.mjs +65 -0
- package/main/task-terminals.mjs +169 -0
- package/main/terminal-access.mjs +6 -0
- package/main/updater.mjs +240 -0
- package/main/zoom-keys.mjs +54 -0
- package/main/zoom-readout.mjs +20 -0
- package/mcp/agentbox-mcp.sh +34 -0
- package/mcp/core/account.mjs +77 -0
- package/mcp/core/chat.mjs +61 -0
- package/mcp/core/documents.mjs +238 -0
- package/mcp/core/page.mjs +152 -0
- package/mcp/core/products.mjs +110 -0
- package/mcp/core/skills.mjs +126 -0
- package/mcp/core/work.mjs +377 -0
- package/mcp/stdio.mjs +70 -0
- package/mcp/tools.mjs +257 -0
- package/package.json +186 -0
- package/renderer/dist/assets/Geist-Variable-Bj2R_7yk.woff2 +0 -0
- package/renderer/dist/assets/GeistMono-Variable-Dispecij.woff2 +0 -0
- package/renderer/dist/assets/addon-fit-YJmn1quW.js +12 -0
- package/renderer/dist/assets/addon-serialize-CsFQTvP_.js +13 -0
- package/renderer/dist/assets/cel-dusk-CS5I7kJS.webp +0 -0
- package/renderer/dist/assets/cel-dusk-DUslqS6B.webp +0 -0
- package/renderer/dist/assets/cel-dusk-Jt_xVGxp.webp +0 -0
- package/renderer/dist/assets/cel-dusk-sm-BBkV-tWT.webp +0 -0
- package/renderer/dist/assets/frost-haze-CxPSOBEg.webp +0 -0
- package/renderer/dist/assets/frost-haze-sm-C_w21qgd.webp +0 -0
- package/renderer/dist/assets/gouache-valley-ChSu14-t.webp +0 -0
- package/renderer/dist/assets/gouache-valley-d9qasI6k.webp +0 -0
- package/renderer/dist/assets/gouache-valley-sm-CdLjT5Ec.webp +0 -0
- package/renderer/dist/assets/ice-BC8luINc.webp +0 -0
- package/renderer/dist/assets/ice-CP9PrKHo.webp +0 -0
- package/renderer/dist/assets/ice-river-D6ziOhLY.webp +0 -0
- package/renderer/dist/assets/ice-river-UVM2-JY_.webp +0 -0
- package/renderer/dist/assets/ice-river-bErJwYWW.webp +0 -0
- package/renderer/dist/assets/ice-river-sm-Bg1U-SIj.webp +0 -0
- package/renderer/dist/assets/ice-sm-CbVcg3_D.webp +0 -0
- package/renderer/dist/assets/index-CwiDv5bL.css +32 -0
- package/renderer/dist/assets/index-D9hCAVPl.js +684 -0
- package/renderer/dist/assets/lake-C0WaW-6E.webp +0 -0
- package/renderer/dist/assets/lake-CSKjLC5K.webp +0 -0
- package/renderer/dist/assets/lake-ETGcOb25.webp +0 -0
- package/renderer/dist/assets/lake-harbour-BCQRbrHb.webp +0 -0
- package/renderer/dist/assets/lake-harbour-B_wfXTmE.webp +0 -0
- package/renderer/dist/assets/lake-harbour-Te1pEvBh.webp +0 -0
- package/renderer/dist/assets/lake-harbour-sm-Cme_ShEg.webp +0 -0
- package/renderer/dist/assets/lake-sm-CbG72Hbv.webp +0 -0
- package/renderer/dist/assets/lino-coast-CUZHcucI.webp +0 -0
- package/renderer/dist/assets/lino-coast-DP4J7cgd.webp +0 -0
- package/renderer/dist/assets/lino-coast-kOIAffr_.webp +0 -0
- package/renderer/dist/assets/lino-coast-sm-C1Mdbgit.webp +0 -0
- package/renderer/dist/assets/moon-pines-BskgnnXR.webp +0 -0
- package/renderer/dist/assets/moon-pines-CFSIIwt0.webp +0 -0
- package/renderer/dist/assets/moon-pines-PNhdOYww.webp +0 -0
- package/renderer/dist/assets/moon-pines-sm-C6K1NomK.webp +0 -0
- package/renderer/dist/assets/orbit-rings-2XV36Ch3.webp +0 -0
- package/renderer/dist/assets/orbit-rings-C4NrcYGQ.webp +0 -0
- package/renderer/dist/assets/orbit-rings-DGLk07dT.webp +0 -0
- package/renderer/dist/assets/orbit-rings-sm-Dky3YdvL.webp +0 -0
- package/renderer/dist/assets/peach-haze-2-C6f7xnWw.webp +0 -0
- package/renderer/dist/assets/peach-haze-2-sm-Dj_Ez_Sb.webp +0 -0
- package/renderer/dist/assets/pixel-harbour-DQjqMROg.webp +0 -0
- package/renderer/dist/assets/pixel-harbour-Dw71H_bc.webp +0 -0
- package/renderer/dist/assets/pixel-harbour-kOZfm-GM.webp +0 -0
- package/renderer/dist/assets/pixel-harbour-sm-Dturjuky.webp +0 -0
- package/renderer/dist/assets/pixel-orbit-C296JVMO.webp +0 -0
- package/renderer/dist/assets/pixel-orbit-C4oMRDSx.webp +0 -0
- package/renderer/dist/assets/pixel-orbit-CqVmQ4eh.webp +0 -0
- package/renderer/dist/assets/pixel-orbit-sm-CHagC_nO.webp +0 -0
- package/renderer/dist/assets/pixel-ridge-C6LYu6uE.webp +0 -0
- package/renderer/dist/assets/pixel-ridge-CIUCEESs.webp +0 -0
- package/renderer/dist/assets/pixel-ridge-sm-BnE0F_w-.webp +0 -0
- package/renderer/dist/assets/riso-hills-7heKVtry.webp +0 -0
- package/renderer/dist/assets/riso-hills-CIjAL3J6.webp +0 -0
- package/renderer/dist/assets/riso-hills-sm-DHsnbCfP.webp +0 -0
- package/renderer/dist/assets/slate-haze-BPWdkNsw.webp +0 -0
- package/renderer/dist/assets/slate-haze-sm-B0Cb5a2W.webp +0 -0
- package/renderer/dist/assets/source-sans-3-cyrillic-ext-wght-normal-DzyfIafT.woff2 +0 -0
- package/renderer/dist/assets/source-sans-3-cyrillic-wght-normal-BMDVbyM7.woff2 +0 -0
- package/renderer/dist/assets/source-sans-3-greek-ext-wght-normal-BWSLJLk6.woff2 +0 -0
- package/renderer/dist/assets/source-sans-3-greek-wght-normal-C9H9m1vD.woff2 +0 -0
- package/renderer/dist/assets/source-sans-3-latin-ext-wght-normal-C8iNium2.woff2 +0 -0
- package/renderer/dist/assets/source-sans-3-latin-wght-normal-BqRLTx4X.woff2 +0 -0
- package/renderer/dist/assets/source-sans-3-vietnamese-wght-normal-C1uRvKPU.woff2 +0 -0
- package/renderer/dist/assets/valley-haze-BDHPqoJy.webp +0 -0
- package/renderer/dist/assets/valley-haze-sm-CTyjmPAG.webp +0 -0
- package/renderer/dist/assets/vector-dunes-DzzWLdKo.webp +0 -0
- package/renderer/dist/assets/vector-dunes-Yr9NBQUy.webp +0 -0
- package/renderer/dist/assets/vector-dunes-sm-D8MYY6ms.webp +0 -0
- package/renderer/dist/assets/watercolour-mist-BCGKN4u4.webp +0 -0
- package/renderer/dist/assets/watercolour-mist-BF-jk5MC.webp +0 -0
- package/renderer/dist/assets/watercolour-mist-sm-Ds1xzTw9.webp +0 -0
- package/renderer/dist/assets/woodblock-sea-B1um67Z9.webp +0 -0
- package/renderer/dist/assets/woodblock-sea-DKQdYs32.webp +0 -0
- package/renderer/dist/assets/woodblock-sea-sm-BJzNq8xh.webp +0 -0
- package/renderer/dist/assets/xterm-BT7tBf8A.js +27 -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 +299 -0
- package/renderer/dist/emulator/index.html +28 -0
- package/renderer/dist/emulator/provenance.json +6 -0
- package/renderer/dist/fixtures/onboard/designs/w-n13/the-first-row.html +1 -0
- package/renderer/dist/index.html +13 -0
- package/scripts/after-install.mjs +59 -0
- package/scripts/approval-server.sh +43 -0
- package/scripts/before-publish.mjs +133 -0
- package/shared/agent-import.d.mts +13 -0
- package/shared/agent-import.mjs +252 -0
- package/shared/agents.d.mts +190 -0
- package/shared/agents.mjs +937 -0
- package/shared/analytics-events.mjs +63 -0
- package/shared/answers.d.mts +12 -0
- package/shared/answers.mjs +80 -0
- package/shared/artifact-keys.d.mts +28 -0
- package/shared/artifact-keys.mjs +140 -0
- package/shared/bridge-map.mjs +128 -0
- package/shared/claude-commands.d.mts +20 -0
- package/shared/claude-commands.generated.mjs +149 -0
- package/shared/claude-commands.mjs +131 -0
- package/shared/claude-models.generated.d.mts +15 -0
- package/shared/claude-models.generated.mjs +26 -0
- package/shared/claude-usage.d.mts +16 -0
- package/shared/claude-usage.mjs +172 -0
- package/shared/codex-commands.d.mts +4 -0
- package/shared/codex-commands.mjs +17 -0
- package/shared/codex-import.d.mts +38 -0
- package/shared/codex-import.mjs +232 -0
- package/shared/codex-modes.mjs +159 -0
- package/shared/codex-usage.mjs +180 -0
- package/shared/contracts.mjs +412 -0
- package/shared/crash-scrub.mjs +182 -0
- package/shared/dashboard.mjs +481 -0
- package/shared/dead-run-trace.mjs +127 -0
- package/shared/demo-world.mjs +415 -0
- package/shared/effort-levels.d.mts +15 -0
- package/shared/effort-levels.mjs +76 -0
- package/shared/engines.d.mts +62 -0
- package/shared/engines.mjs +389 -0
- package/shared/first-run-examples.mjs +60 -0
- package/shared/first-run-line.mjs +149 -0
- package/shared/first-run-practice.d.mts +32 -0
- package/shared/first-run-practice.mjs +254 -0
- package/shared/first-run-shape.mjs +100 -0
- package/shared/fresh-user-home.mjs +189 -0
- package/shared/hook-failure.mjs +22 -0
- package/shared/install-id.mjs +39 -0
- package/shared/instruction-defaults.json +5 -0
- package/shared/library-folder.mjs +28 -0
- package/shared/notify-rules.mjs +120 -0
- package/shared/product-name.d.mts +6 -0
- package/shared/product-name.mjs +144 -0
- package/shared/project-folder-check.d.mts +22 -0
- package/shared/project-folder-check.mjs +152 -0
- package/shared/provider-commands.d.mts +8 -0
- package/shared/provider-commands.mjs +58 -0
- package/shared/rank.d.mts +19 -0
- package/shared/rank.mjs +146 -0
- package/shared/recovery.mjs +26 -0
- package/shared/referenced-files.d.mts +11 -0
- package/shared/referenced-files.mjs +122 -0
- package/shared/repeats.d.mts +45 -0
- package/shared/repeats.mjs +193 -0
- package/shared/schemes.mjs +33 -0
- package/shared/settings-terminal.d.mts +1 -0
- package/shared/settings-terminal.mjs +14 -0
- package/shared/side-build.mjs +57 -0
- package/shared/spawn-trouble.d.mts +8 -0
- package/shared/spawn-trouble.mjs +599 -0
- package/shared/terminal-state.d.mts +1 -0
- package/shared/terminal-state.mjs +58 -0
- package/shared/usage.d.mts +53 -0
- package/shared/usage.mjs +319 -0
- package/shared/work-items.mjs +561 -0
- package/shared/work-lines.mjs +360 -0
- 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
|
+
}
|