brane 0.1.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (348) hide show
  1. package/AGENTS.md +362 -0
  2. package/CHANGELOG.md +33 -0
  3. package/CONTRIBUTING.md +91 -0
  4. package/DESIGN.md +402 -0
  5. package/Dockerfile +61 -0
  6. package/LICENSE +656 -17
  7. package/README.md +174 -2
  8. package/SECURITY.md +205 -0
  9. package/dist/absorb/brane-id.js +11 -0
  10. package/dist/absorb/chatgpt.js +161 -0
  11. package/dist/absorb/claude-code.js +504 -0
  12. package/dist/absorb/index.js +543 -0
  13. package/dist/absorb/organelle.js +345 -0
  14. package/dist/absorb/run.js +520 -0
  15. package/dist/absorb/sources.js +168 -0
  16. package/dist/absorb/types.js +3 -0
  17. package/dist/agent-env.js +45 -0
  18. package/dist/agents.js +1168 -0
  19. package/dist/claude-live-session.js +124 -0
  20. package/dist/cli.js +50 -0
  21. package/dist/codex-tool-log.js +144 -0
  22. package/dist/commands/absorb.js +291 -0
  23. package/dist/commands/attach.js +31 -0
  24. package/dist/commands/certify.js +62 -0
  25. package/dist/commands/config.js +116 -0
  26. package/dist/commands/cost.js +126 -0
  27. package/dist/commands/daemon.js +350 -0
  28. package/dist/commands/doctor.js +262 -0
  29. package/dist/commands/fold.js +95 -0
  30. package/dist/commands/list.js +89 -0
  31. package/dist/commands/mcp.js +84 -0
  32. package/dist/commands/meld.js +332 -0
  33. package/dist/commands/memory.js +215 -0
  34. package/dist/commands/revive.js +74 -0
  35. package/dist/commands/run.js +85 -0
  36. package/dist/commands/spawn.js +1094 -0
  37. package/dist/commands/token.js +153 -0
  38. package/dist/copilot-tool-log.js +145 -0
  39. package/dist/cost/pricing.js +116 -0
  40. package/dist/cost/reprice.js +136 -0
  41. package/dist/crush-tool-log.js +293 -0
  42. package/dist/daemon/agent-scope.js +116 -0
  43. package/dist/daemon/awaiting.js +54 -0
  44. package/dist/daemon/cert-manager.js +349 -0
  45. package/dist/daemon/client.js +262 -0
  46. package/dist/daemon/gated-routes.js +112 -0
  47. package/dist/daemon/keep-awake.js +103 -0
  48. package/dist/daemon/local-socket.js +145 -0
  49. package/dist/daemon/presence-gate.js +206 -0
  50. package/dist/daemon/server.js +6349 -0
  51. package/dist/daemon/signal-source.js +35 -0
  52. package/dist/daemon/tailscale-rebind.js +58 -0
  53. package/dist/daemon/token-ring.js +263 -0
  54. package/dist/daemon/token-routes.js +190 -0
  55. package/dist/db/events.js +152 -0
  56. package/dist/db/mentions.js +179 -0
  57. package/dist/db/schema.js +633 -0
  58. package/dist/db/tokens.js +583 -0
  59. package/dist/db/tool-uses.js +647 -0
  60. package/dist/diagram/index.js +85 -0
  61. package/dist/diagram/label.js +455 -0
  62. package/dist/diagram/layout.js +476 -0
  63. package/dist/diagram/links.js +50 -0
  64. package/dist/diagram/metrics.js +152 -0
  65. package/dist/diagram/parse.js +487 -0
  66. package/dist/diagram/svg.js +389 -0
  67. package/dist/diagram/types.js +9 -0
  68. package/dist/export.js +629 -0
  69. package/dist/files/archive-types.js +2 -0
  70. package/dist/files/archive.js +182 -0
  71. package/dist/files/browse.js +327 -0
  72. package/dist/files/bytes.js +162 -0
  73. package/dist/files/codecs.js +33 -0
  74. package/dist/files/find.js +212 -0
  75. package/dist/files/tar.js +223 -0
  76. package/dist/files/zip.js +209 -0
  77. package/dist/folds.js +72 -0
  78. package/dist/grammars/ebnf.js +92 -0
  79. package/dist/highlight.js +250 -0
  80. package/dist/ids.js +24 -0
  81. package/dist/markdown-diagnostics.js +233 -0
  82. package/dist/math.js +168 -0
  83. package/dist/mcp/server.js +886 -0
  84. package/dist/memory/embedder.js +145 -0
  85. package/dist/memory/embedding-worker.js +71 -0
  86. package/dist/memory/embeddings.js +91 -0
  87. package/dist/memory/index.js +232 -0
  88. package/dist/memory/storage.js +587 -0
  89. package/dist/memory/wiki.js +688 -0
  90. package/dist/memory/wordpiece.js +247 -0
  91. package/dist/mentions-scan.js +40 -0
  92. package/dist/presence.js +259 -0
  93. package/dist/session-affinity.js +185 -0
  94. package/dist/shared/api-request.js +124 -0
  95. package/dist/shared/block-glyphs.js +114 -0
  96. package/dist/shared/diagram-css.js +17 -0
  97. package/dist/shared/export-selection.js +101 -0
  98. package/dist/shared/find-in-text.js +110 -0
  99. package/dist/shared/glow-hues.js +225 -0
  100. package/dist/shared/hex-dump.js +62 -0
  101. package/dist/shared/hex-pages.js +52 -0
  102. package/dist/shared/hit-drum.js +79 -0
  103. package/dist/shared/known-nids.js +58 -0
  104. package/dist/shared/list-scroll-keys.js +56 -0
  105. package/dist/shared/markdown-lists.js +72 -0
  106. package/dist/shared/markdown-math.js +485 -0
  107. package/dist/shared/markdown-mentions.js +79 -0
  108. package/dist/shared/markdown-nids.js +41 -0
  109. package/dist/shared/markdown-protocols.js +53 -0
  110. package/dist/shared/markdown-refs.js +123 -0
  111. package/dist/shared/membrane-invasion.js +342 -0
  112. package/dist/shared/membrane-motion.js +83 -0
  113. package/dist/shared/membrane-palette.js +253 -0
  114. package/dist/shared/membrane-terrain.js +212 -0
  115. package/dist/shared/message-selection.js +123 -0
  116. package/dist/shared/mo-glyph.js +57 -0
  117. package/dist/shared/narrative-format.js +237 -0
  118. package/dist/shared/narrative-menu.js +82 -0
  119. package/dist/shared/narrative-sort.js +47 -0
  120. package/dist/shared/narrative-status.js +52 -0
  121. package/dist/shared/no-pan.js +85 -0
  122. package/dist/shared/pane-layout.js +50 -0
  123. package/dist/shared/payload-inline.js +83 -0
  124. package/dist/shared/speaker-color.js +61 -0
  125. package/dist/shared/token-rotation.js +14 -0
  126. package/dist/shared/view-url.js +197 -0
  127. package/dist/shared/wiki-links.js +70 -0
  128. package/dist/shared/wiki-window.js +4 -0
  129. package/dist/tailscale.js +55 -0
  130. package/dist/test/absorb-claude.test.js +892 -0
  131. package/dist/test/agent-scope.test.js +193 -0
  132. package/dist/test/agents.test.js +55 -0
  133. package/dist/test/api-request.test.js +122 -0
  134. package/dist/test/awaiting-glow.test.js +54 -0
  135. package/dist/test/awaiting.test.js +122 -0
  136. package/dist/test/bind-token.test.js +88 -0
  137. package/dist/test/block-glyphs.test.js +68 -0
  138. package/dist/test/cert-manager.test.js +262 -0
  139. package/dist/test/claude-live-session.test.js +110 -0
  140. package/dist/test/codex-tool-log.test.js +568 -0
  141. package/dist/test/config.test.js +71 -0
  142. package/dist/test/copilot-tool-log.test.js +93 -0
  143. package/dist/test/cors-preflight.test.js +49 -0
  144. package/dist/test/cost-linger.test.js +31 -0
  145. package/dist/test/cost-pricing.test.js +72 -0
  146. package/dist/test/cost-reprice.test.js +139 -0
  147. package/dist/test/cost-sessions.test.js +301 -0
  148. package/dist/test/crush-tool-log.test.js +289 -0
  149. package/dist/test/daemon-client.test.js +109 -0
  150. package/dist/test/daemon-socket.test.js +293 -0
  151. package/dist/test/diagram-arrows.test.js +205 -0
  152. package/dist/test/diagram-export.test.js +210 -0
  153. package/dist/test/diagram-labels.test.js +313 -0
  154. package/dist/test/diagram-parse.test.js +191 -0
  155. package/dist/test/diagram-render.test.js +175 -0
  156. package/dist/test/diagram-review.test.js +288 -0
  157. package/dist/test/diagram-self-loop-labels.test.js +303 -0
  158. package/dist/test/embedder.test.js +84 -0
  159. package/dist/test/events.test.js +123 -0
  160. package/dist/test/export-selection.test.js +102 -0
  161. package/dist/test/export.test.js +505 -0
  162. package/dist/test/files-browse.test.js +393 -0
  163. package/dist/test/files-find.test.js +272 -0
  164. package/dist/test/find-in-text.test.js +86 -0
  165. package/dist/test/first-paint-viewport.test.js +59 -0
  166. package/dist/test/fixtures/embedding-golden.js +23 -0
  167. package/dist/test/fixtures/wordpiece-golden.js +167 -0
  168. package/dist/test/glow-hues.test.js +202 -0
  169. package/dist/test/harness-cost.test.js +423 -0
  170. package/dist/test/helpers.js +21 -0
  171. package/dist/test/hex-pages.test.js +68 -0
  172. package/dist/test/highlight.test.js +69 -0
  173. package/dist/test/hit-drum.test.js +71 -0
  174. package/dist/test/hook-config.test.js +262 -0
  175. package/dist/test/json-gzip.test.js +70 -0
  176. package/dist/test/key-routing.test.js +58 -0
  177. package/dist/test/known-nids.test.js +106 -0
  178. package/dist/test/link-preview.test.js +67 -0
  179. package/dist/test/list-scroll-keys.test.js +66 -0
  180. package/dist/test/listen-own-words.test.js +147 -0
  181. package/dist/test/listen-timeout.test.js +33 -0
  182. package/dist/test/markdown-diagnostics.test.js +121 -0
  183. package/dist/test/markdown-lists.test.js +182 -0
  184. package/dist/test/markdown-math.test.js +256 -0
  185. package/dist/test/markdown-nids.test.js +146 -0
  186. package/dist/test/markdown-protocols.test.js +139 -0
  187. package/dist/test/mcp-bridge-cancel.test.js +98 -0
  188. package/dist/test/mcp-bridge-reconnect.test.js +121 -0
  189. package/dist/test/mcp-narrative.test.js +193 -0
  190. package/dist/test/mcp.test.js +548 -0
  191. package/dist/test/meld.test.js +85 -0
  192. package/dist/test/membrane-invasion.test.js +482 -0
  193. package/dist/test/membrane-motion.test.js +101 -0
  194. package/dist/test/membrane-palette.test.js +166 -0
  195. package/dist/test/membrane-terrain.test.js +170 -0
  196. package/dist/test/memory-log.test.js +202 -0
  197. package/dist/test/mentions-e2e.test.js +261 -0
  198. package/dist/test/mentions-ledger.test.js +265 -0
  199. package/dist/test/mentions-listen.test.js +265 -0
  200. package/dist/test/mentions.test.js +238 -0
  201. package/dist/test/message-selection.test.js +114 -0
  202. package/dist/test/mo-glyph.test.js +22 -0
  203. package/dist/test/model-resolution.test.js +166 -0
  204. package/dist/test/narrate-validate.test.js +77 -0
  205. package/dist/test/narrate-wait.test.js +160 -0
  206. package/dist/test/narrative-format.test.js +219 -0
  207. package/dist/test/narrative-menu.test.js +137 -0
  208. package/dist/test/narrative-sort.test.js +75 -0
  209. package/dist/test/no-pan.test.js +60 -0
  210. package/dist/test/pane-layout.test.js +153 -0
  211. package/dist/test/paths.test.js +37 -0
  212. package/dist/test/payload-inline.test.js +91 -0
  213. package/dist/test/precache-fonts.test.js +72 -0
  214. package/dist/test/presence-gate.test.js +440 -0
  215. package/dist/test/presence.test.js +160 -0
  216. package/dist/test/read-events.test.js +220 -0
  217. package/dist/test/remote-meld.test.js +115 -0
  218. package/dist/test/revive-cli.test.js +93 -0
  219. package/dist/test/search-floor.test.js +206 -0
  220. package/dist/test/search-fusion.test.js +168 -0
  221. package/dist/test/search.test.js +205 -0
  222. package/dist/test/security.test.js +48 -0
  223. package/dist/test/selection-wiring.test.js +61 -0
  224. package/dist/test/session-affinity.test.js +295 -0
  225. package/dist/test/signal-source.test.js +32 -0
  226. package/dist/test/socket-account.test.js +268 -0
  227. package/dist/test/spawn-toml.test.js +72 -0
  228. package/dist/test/speaker-color.test.js +90 -0
  229. package/dist/test/tailscale-rebind.test.js +79 -0
  230. package/dist/test/terminal-cursor.test.js +61 -0
  231. package/dist/test/terminal-resync.test.js +23 -0
  232. package/dist/test/thin-cli.test.js +382 -0
  233. package/dist/test/token-bootstrap.test.js +272 -0
  234. package/dist/test/token-devices.test.js +587 -0
  235. package/dist/test/token-lapse.test.js +94 -0
  236. package/dist/test/token-ring.test.js +188 -0
  237. package/dist/test/token-rotate-cli.test.js +58 -0
  238. package/dist/test/token-routes.test.js +95 -0
  239. package/dist/test/tool-uses.test.js +1006 -0
  240. package/dist/test/usage-ledger.test.js +126 -0
  241. package/dist/test/view-url.test.js +581 -0
  242. package/dist/test/wiki-links.test.js +57 -0
  243. package/dist/test/wiki-sources.test.js +85 -0
  244. package/dist/test/wiki-window.test.js +31 -0
  245. package/dist/test/wiki.test.js +399 -0
  246. package/dist/test/wordpiece.test.js +68 -0
  247. package/dist/tls.js +148 -0
  248. package/dist/tmux.js +202 -0
  249. package/dist/user-agent.js +46 -0
  250. package/docker-entrypoint.sh +141 -0
  251. package/docs/README.md +3 -0
  252. package/docs/absorb.md +231 -0
  253. package/docs/cost-ledger.md +125 -0
  254. package/docs/daemon-api.md +189 -0
  255. package/docs/diagrams.md +163 -0
  256. package/docs/doctor.md +36 -0
  257. package/docs/export-scope.md +69 -0
  258. package/docs/files-view.md +133 -0
  259. package/docs/link-previews.md +38 -0
  260. package/docs/math.md +32 -0
  261. package/docs/mcp.md +69 -0
  262. package/docs/memory-search.md +75 -0
  263. package/docs/mentions.md +101 -0
  264. package/docs/multi-brane.md +14 -0
  265. package/docs/passkeys.md +165 -0
  266. package/docs/presence.md +202 -0
  267. package/docs/releasing.md +53 -0
  268. package/docs/rendering-warnings.md +22 -0
  269. package/docs/request-layer.md +72 -0
  270. package/docs/selection-store.md +50 -0
  271. package/docs/session-affinity.md +61 -0
  272. package/docs/terminal-mirroring.md +211 -0
  273. package/docs/token-rotation.md +413 -0
  274. package/docs/view-urls.md +114 -0
  275. package/docs/web-app.md +23 -0
  276. package/docs/wiki.md +43 -0
  277. package/expo/dist/_expo/static/js/web/entry-e56efc1291dca7258ea00b8108b033a4.js +1097 -0
  278. package/expo/dist/apple-touch-icon.png +0 -0
  279. package/expo/dist/favicon.svg +3 -0
  280. package/expo/dist/index.html +177 -0
  281. package/expo/dist/katex/fonts/KaTeX_AMS-Regular.woff2 +0 -0
  282. package/expo/dist/katex/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
  283. package/expo/dist/katex/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
  284. package/expo/dist/katex/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
  285. package/expo/dist/katex/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
  286. package/expo/dist/katex/fonts/KaTeX_Main-Bold.woff2 +0 -0
  287. package/expo/dist/katex/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
  288. package/expo/dist/katex/fonts/KaTeX_Main-Italic.woff2 +0 -0
  289. package/expo/dist/katex/fonts/KaTeX_Main-Regular.woff2 +0 -0
  290. package/expo/dist/katex/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
  291. package/expo/dist/katex/fonts/KaTeX_Math-Italic.woff2 +0 -0
  292. package/expo/dist/katex/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
  293. package/expo/dist/katex/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
  294. package/expo/dist/katex/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
  295. package/expo/dist/katex/fonts/KaTeX_Script-Regular.woff2 +0 -0
  296. package/expo/dist/katex/fonts/KaTeX_Size1-Regular.woff2 +0 -0
  297. package/expo/dist/katex/fonts/KaTeX_Size2-Regular.woff2 +0 -0
  298. package/expo/dist/katex/fonts/KaTeX_Size3-Regular.woff2 +0 -0
  299. package/expo/dist/katex/fonts/KaTeX_Size4-Regular.woff2 +0 -0
  300. package/expo/dist/katex/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
  301. package/expo/dist/katex/katex.min.css +1 -0
  302. package/expo/dist/manifest.json +16 -0
  303. package/expo/dist/metadata.json +1 -0
  304. package/expo/dist/og/membrane.v1.png +0 -0
  305. package/expo/dist/pdf.min.mjs +28 -0
  306. package/expo/dist/pdf.worker.min.mjs +28 -0
  307. package/expo/dist/sw.js +154 -0
  308. package/package.json +79 -27
  309. package/protocols/LIVE_loop.md +407 -0
  310. package/protocols/bebrief.md +41 -0
  311. package/protocols/digest.md +231 -0
  312. package/protocols/fly.md +266 -0
  313. package/protocols/idle.md +81 -0
  314. package/protocols/keeper.md +829 -0
  315. package/protocols/markdown.md +80 -0
  316. package/protocols/memory.md +419 -0
  317. package/protocols/mentions.md +93 -0
  318. package/protocols/payloads.md +53 -0
  319. package/protocols/prelude.md +91 -0
  320. package/protocols/ramify.md +105 -0
  321. package/protocols/review.md +356 -0
  322. package/protocols/rrr.md +232 -0
  323. package/protocols/scribe.md +429 -0
  324. package/protocols/summarize.md +203 -0
  325. package/protocols/twin.md +174 -0
  326. package/protocols/wiki.md +438 -0
  327. package/protocols/worker.md +38 -0
  328. package/wiki/shared/append-only-event-log.md +148 -0
  329. package/wiki/shared/index.md +48 -0
  330. package/wiki/shared/owning-your-narratives.md +161 -0
  331. package/wiki/shared/premature-ontologies.md +435 -0
  332. package/wiki/shared/sedimentary-superintelligence.md +128 -0
  333. package/wiki/shared/sequestering-the-narrative.md +143 -0
  334. package/wiki/shared/why-wikis-work.md +216 -0
  335. package/.travis.yml +0 -7
  336. package/lib/brane.cjs.js +0 -12
  337. package/lib/brane.cjs.js.map +0 -1
  338. package/lib/brane.d.ts +0 -11
  339. package/lib/brane.js +0 -6
  340. package/lib/brane.js.map +0 -1
  341. package/lib/tests.d.ts +0 -1
  342. package/lib/tests.js +0 -8
  343. package/lib/tests.js.map +0 -1
  344. package/rollup.config.js +0 -21
  345. package/scripts/test.sh +0 -8
  346. package/src/brane.ts +0 -21
  347. package/src/tests.ts +0 -8
  348. package/tsconfig.json +0 -16
@@ -0,0 +1,504 @@
1
+ /** Claude Code's local session store as an absorb source.
2
+ *
3
+ * Claude Code writes every session to `~/.claude/projects/<cwd, with `/` as
4
+ * `-`>/<session uuid>.jsonl`, one JSON record per line, appended as the
5
+ * session runs. It is the engineering half of a Claude user's history — the
6
+ * half no export request can produce and no hosted product ever sees — and
7
+ * it is already on the disk of anyone who has run `claude` once. Nothing here
8
+ * makes a network call; the file is read and the database is written.
9
+ *
10
+ * The file is heterogeneous. Of its record types only two carry
11
+ * conversation — `user` and `assistant` — and even those double as
12
+ * bookkeeping: a `user` record whose content is a `tool_result` block is a
13
+ * tool's return value, not something the operator typed, and a `user`
14
+ * record with `isMeta` is text the harness injected. Everything else (`mode`,
15
+ * `ai-title`, `attachment`, `file-history-snapshot`, `cost-state`, …) is
16
+ * state the harness keeps for itself. Two such records are read here:
17
+ * `ai-title`, for the narrative's title, and the `attachment` that carries a
18
+ * message the operator sent while a turn was running (`queuedTurnOf`).
19
+ *
20
+ * A session written by a brane-spawned agent has the same shape plus
21
+ * `tool_use` blocks named `mcp__brane__*`; the parser makes no distinction.
22
+ * What distinguishes those sessions is that brane already owns them, which
23
+ * `runLocalAbsorb` decides from `narratives.session_ids` before any parse.
24
+ */
25
+ import { closeSync, existsSync, openSync, readdirSync, readFileSync, readSync, statSync } from "node:fs";
26
+ import { basename, join } from "node:path";
27
+ import { userInfo } from "node:os";
28
+ /** Where the store lives under a home directory. */
29
+ export function claudeProjectsRoot(home) {
30
+ return join(home, ".claude", "projects");
31
+ }
32
+ /** The account Claude Code is signed into, from `~/.claude.json`'s
33
+ * `oauthAccount`; the OS user when there is none (an API-key install, a
34
+ * copied store). The account uuid keys the derived brane id, so the same
35
+ * person's sessions from two machines land in one brane. */
36
+ export function claudeIdentity(home) {
37
+ try {
38
+ const raw = JSON.parse(readFileSync(join(home, ".claude.json"), "utf-8"));
39
+ const acct = raw?.oauthAccount;
40
+ if (acct && typeof acct === "object") {
41
+ const id = typeof acct.accountUuid === "string" ? acct.accountUuid : undefined;
42
+ const email = typeof acct.emailAddress === "string" ? acct.emailAddress : undefined;
43
+ if (id || email) {
44
+ return { id: id ?? email, email, label: email ?? id };
45
+ }
46
+ }
47
+ }
48
+ catch {
49
+ // no ~/.claude.json, or not ours to parse — fall through
50
+ }
51
+ let user = "operator";
52
+ try {
53
+ user = userInfo().username || user;
54
+ }
55
+ catch {
56
+ // userInfo can throw on exotic setups; the fallback label is fine
57
+ }
58
+ return { id: user, label: user };
59
+ }
60
+ /** How many leading bytes the survey reads to learn a session's cwd and
61
+ * start time. Sessions open with a handful of one-line bookkeeping records
62
+ * before the first message; the first record with a `cwd` is within a few
63
+ * KB of the top in every file seen. A file whose head yields nothing is
64
+ * still listed, with the fields it lacks null. */
65
+ const SURVEY_HEAD_BYTES = 64 * 1024;
66
+ function readHead(path, bytes) {
67
+ const fd = openSync(path, "r");
68
+ try {
69
+ const buf = Buffer.alloc(bytes);
70
+ const n = readSync(fd, buf, 0, bytes, 0);
71
+ return buf.subarray(0, n).toString("utf-8");
72
+ }
73
+ finally {
74
+ closeSync(fd);
75
+ }
76
+ }
77
+ /** Session files are `<uuid>.jsonl`; `agent-<id>.jsonl` files beside them are
78
+ * subagent transcripts, which belong to their parent session and are not
79
+ * sessions of their own (the same rule `claudeAdapter.discoverSessionId`
80
+ * applies). */
81
+ function isSessionFile(name) {
82
+ return name.endsWith(".jsonl") && !name.startsWith("agent-");
83
+ }
84
+ /** Every session in the store, on a stat and a head read each. */
85
+ export function surveyClaudeSessions(root) {
86
+ const out = [];
87
+ let projects;
88
+ try {
89
+ projects = readdirSync(root);
90
+ }
91
+ catch {
92
+ return out;
93
+ }
94
+ for (const project of projects) {
95
+ const dir = join(root, project);
96
+ let files;
97
+ try {
98
+ if (!statSync(dir).isDirectory())
99
+ continue;
100
+ files = readdirSync(dir);
101
+ }
102
+ catch {
103
+ continue;
104
+ }
105
+ for (const file of files) {
106
+ if (!isSessionFile(file))
107
+ continue;
108
+ const path = join(dir, file);
109
+ let st;
110
+ try {
111
+ st = statSync(path);
112
+ }
113
+ catch {
114
+ continue;
115
+ }
116
+ const head = readHead(path, Math.min(SURVEY_HEAD_BYTES, st.size));
117
+ let cwd = null;
118
+ let startedAt = null;
119
+ for (const line of head.split("\n")) {
120
+ if (!line)
121
+ continue;
122
+ let rec;
123
+ try {
124
+ rec = JSON.parse(line);
125
+ }
126
+ catch {
127
+ continue; // the last line of a head read is usually cut mid-record
128
+ }
129
+ if (cwd === null && typeof rec.cwd === "string")
130
+ cwd = rec.cwd;
131
+ if (startedAt === null && typeof rec.timestamp === "string") {
132
+ const t = Date.parse(rec.timestamp);
133
+ if (!Number.isNaN(t))
134
+ startedAt = t;
135
+ }
136
+ if (cwd !== null && startedAt !== null)
137
+ break;
138
+ }
139
+ out.push({
140
+ externalId: file.slice(0, -".jsonl".length),
141
+ path,
142
+ cwd,
143
+ startedAt,
144
+ updatedAt: Math.floor(st.mtimeMs),
145
+ bytes: st.size,
146
+ });
147
+ }
148
+ }
149
+ return out;
150
+ }
151
+ /** Text of a message: string content as is, block content as its `text`
152
+ * blocks joined. `thinking` blocks are deliberately not included — they are
153
+ * the model's scratch, not its voice, and a narrative is the voice. Nor is
154
+ * reasoning something absorb promises for any source: some tools withhold
155
+ * it and brane has nowhere to show it (claude-xiy9e8wq:20). */
156
+ function textOf(content) {
157
+ if (typeof content === "string")
158
+ return content;
159
+ if (!Array.isArray(content))
160
+ return "";
161
+ return content
162
+ .filter((b) => b.type === "text" && typeof b.text === "string")
163
+ .map((b) => b.text)
164
+ .join("\n\n");
165
+ }
166
+ /** The image blocks of a user turn, as attachments. A pasted image is the
167
+ * one binary a session file carries: Claude Code writes it into the record
168
+ * itself, base64 in `source.data`, beside a `[Image #N]` text block. Only
169
+ * base64 sources occur in the store; a `url` source would need a fetch
170
+ * absorb never makes, so it is left out rather than guessed at. */
171
+ function attachmentsOf(blocks) {
172
+ const out = [];
173
+ for (const b of blocks) {
174
+ if (b.type !== "image" || !b.source || b.source.type !== "base64")
175
+ continue;
176
+ if (typeof b.source.data !== "string" || b.source.data.length === 0)
177
+ continue;
178
+ out.push({
179
+ mediaType: typeof b.source.media_type === "string" ? b.source.media_type : "application/octet-stream",
180
+ data: b.source.data,
181
+ });
182
+ }
183
+ return out;
184
+ }
185
+ // ---- what the harness writes into user turns -------------------------------
186
+ //
187
+ // A `user` record is not always something the operator typed. Claude Code
188
+ // writes several kinds of its own text into user turns, each wrapped in a
189
+ // tag, and a transcript read as prose shows the tags (claude-xiy9e8wq:29).
190
+ // Measured over 3,168 non-meta user text blocks on one machine:
191
+ //
192
+ // <task-notification> 2,100 a background task (bash, subagent) finished
193
+ // [Request interrupted 398 the operator pressed Escape
194
+ // <command-name> 17 a slash command, with <command-args>
195
+ // <local-command-stdout> 9 what the slash command printed
196
+ // <bash-input> 9 a `!` shell command …
197
+ // <bash-stdout> 9 … and its output (with <bash-stderr>)
198
+ //
199
+ // Each gets the rendering its meaning calls for, per text block, so a block
200
+ // the operator typed beside an injected one keeps its own shape.
201
+ /** A fenced block whose fence is longer than any backtick run inside it. */
202
+ function fence(text, lang = "") {
203
+ const longest = Math.max(2, ...[...text.matchAll(/`+/g)].map((m) => m[0].length));
204
+ const f = "`".repeat(longest + 1);
205
+ return `${f}${lang}\n${text.replace(/\n$/, "")}\n${f}`;
206
+ }
207
+ function tagged(text, tag) {
208
+ const m = new RegExp(`<${tag}>([\\s\\S]*?)</${tag}>`).exec(text);
209
+ return m ? m[1] : null;
210
+ }
211
+ export function classifyUserBlock(raw) {
212
+ const t = raw.trim();
213
+ if (!t)
214
+ return { kind: "drop" };
215
+ if (/^\[Request interrupted by user[^\]]*\]$/.test(t)) {
216
+ return { kind: "interrupted", text: `*${t}*` };
217
+ }
218
+ if (t.startsWith("<task-notification>")) {
219
+ return {
220
+ kind: "notification",
221
+ taskId: tagged(t, "task-id"),
222
+ toolUseId: tagged(t, "tool-use-id"),
223
+ status: tagged(t, "status"),
224
+ summary: tagged(t, "summary"),
225
+ result: tagged(t, "result"),
226
+ };
227
+ }
228
+ if (t.startsWith("<bash-input>")) {
229
+ const cmd = tagged(t, "bash-input") ?? "";
230
+ return { kind: "shell", text: fence(`$ ${cmd.trim()}`, "sh") };
231
+ }
232
+ if (t.startsWith("<bash-stdout>")) {
233
+ const out = (tagged(t, "bash-stdout") ?? "").trim();
234
+ const err = (tagged(t, "bash-stderr") ?? "").trim();
235
+ const parts = [];
236
+ if (out)
237
+ parts.push(fence(out));
238
+ if (err)
239
+ parts.push(`stderr:\n${fence(err)}`);
240
+ return parts.length ? { kind: "shell", text: parts.join("\n\n") } : { kind: "drop" };
241
+ }
242
+ if (t.startsWith("<command-name>")) {
243
+ const name = (tagged(t, "command-name") ?? "").trim();
244
+ const args = (tagged(t, "command-args") ?? "").trim();
245
+ return { kind: "command", text: `\`${[name, args].filter(Boolean).join(" ")}\`` };
246
+ }
247
+ if (t.startsWith("<local-command-stdout>")) {
248
+ const out = (tagged(t, "local-command-stdout") ?? "").trim();
249
+ return out ? { kind: "command", text: fence(out) } : { kind: "drop" };
250
+ }
251
+ if (t.startsWith("<system-reminder>") || t.startsWith("<local-command-caveat>")) {
252
+ return { kind: "drop" };
253
+ }
254
+ return { kind: "prose", text: t };
255
+ }
256
+ // ---- turns that arrive mid-turn ---------------------------------------------
257
+ //
258
+ // A message the operator sends while the agent is still working is never
259
+ // written as a `user` record. The harness queues it (`queue-operation`
260
+ // records: enqueue, then dequeue or remove), and if the running turn takes
261
+ // it in, writes it as an `attachment` of type `queued_command` at the point
262
+ // the agent read it — after the tool result it was waiting on, in the
263
+ // `parentUuid` chain — stamped with the time it was typed. Background-task
264
+ // completions arrive by the same route, `commandMode: "task-notification"`.
265
+ //
266
+ // The attachment is the delivery receipt, and it is the only thing read. A
267
+ // queued message the operator cancelled (popped back into the editor), or
268
+ // that the session ended before delivering, has queue-operation records and
269
+ // no attachment, so it stays out of the transcript as it stayed out of the
270
+ // conversation. The queue-operation records are not a usable second signal:
271
+ // older harness versions write `remove` with no reason for a delivery, and
272
+ // most task notifications carry no `origin`. Measured on one machine's
273
+ // store: 450 queued_command attachments, 103 `prompt` (the operator's) and
274
+ // 347 `task-notification`; of 113 queued messages that never went through,
275
+ // none had an attachment.
276
+ /** A `queued_command` attachment as the text blocks and images of a user
277
+ * turn; `null` for any other record, or a command mode this parser does not
278
+ * know (whose text would be guessed at). */
279
+ function queuedTurnOf(rec) {
280
+ const a = rec.attachment;
281
+ if (!a || a.type !== "queued_command")
282
+ return null;
283
+ if (a.commandMode !== "prompt" && a.commandMode !== "task-notification")
284
+ return null;
285
+ const p = a.prompt;
286
+ if (typeof p === "string")
287
+ return { texts: [p], attachments: [] };
288
+ if (!Array.isArray(p))
289
+ return null;
290
+ return {
291
+ texts: p.filter((b) => b.type === "text" && typeof b.text === "string").map((b) => b.text),
292
+ attachments: attachmentsOf(p),
293
+ };
294
+ }
295
+ /** Parse one session's JSONL text into a conversation. `null` when it holds
296
+ * no message at all. */
297
+ export function parseClaudeSession(text, candidate) {
298
+ const messages = [];
299
+ const toolCalls = [];
300
+ const callsByKey = new Map();
301
+ const seqOfUuid = new Map(); // record uuid → messages index
302
+ const models = {};
303
+ let title = null;
304
+ let firstProse = null; // the fallback title: words, not a `/clear`
305
+ let createdAt = null;
306
+ let updatedAt = null;
307
+ let cwd = candidate.cwd ?? undefined;
308
+ for (const line of text.split("\n")) {
309
+ if (!line)
310
+ continue;
311
+ let rec;
312
+ try {
313
+ rec = JSON.parse(line);
314
+ }
315
+ catch {
316
+ continue; // a torn last line while the session is still being written
317
+ }
318
+ if (rec.type === "ai-title" && typeof rec.aiTitle === "string") {
319
+ title = rec.aiTitle; // the newest wins; the harness retitles as it goes
320
+ continue;
321
+ }
322
+ const queued = rec.type === "attachment" ? queuedTurnOf(rec) : null;
323
+ if (rec.type !== "user" && rec.type !== "assistant" && !queued)
324
+ continue;
325
+ if (rec.isSidechain)
326
+ continue; // a subagent's turns; not this conversation
327
+ const time = rec.timestamp ? Date.parse(rec.timestamp) : NaN;
328
+ const at = Number.isNaN(time) ? (updatedAt ?? candidate.startedAt ?? candidate.updatedAt) : time;
329
+ if (createdAt === null || at < createdAt)
330
+ createdAt = at;
331
+ if (updatedAt === null || at > updatedAt)
332
+ updatedAt = at;
333
+ if (cwd === undefined && typeof rec.cwd === "string")
334
+ cwd = rec.cwd;
335
+ if (queued) {
336
+ userTurn(queued.texts, queued.attachments, rec, at);
337
+ continue;
338
+ }
339
+ const content = rec.message?.content;
340
+ const blocks = Array.isArray(content) ? content : [];
341
+ if (rec.type === "user") {
342
+ // Tool results come back as user records. They close a call; they are
343
+ // never a message.
344
+ let sawResult = false;
345
+ for (const b of blocks) {
346
+ if (b.type !== "tool_result" || typeof b.tool_use_id !== "string")
347
+ continue;
348
+ sawResult = true;
349
+ const call = callsByKey.get(b.tool_use_id);
350
+ if (!call)
351
+ continue;
352
+ call.output = b.content;
353
+ call.status = b.is_error ? "failed" : "completed";
354
+ if (at >= call.startedAt)
355
+ call.durationMs = at - call.startedAt;
356
+ }
357
+ if (sawResult)
358
+ continue;
359
+ // An `isMeta` record's text is the harness's, never the operator's,
360
+ // and stays out. An image it carries is the operator's: a paste that
361
+ // arrives mid-turn is written as a `turnCompanion` record with `isMeta`
362
+ // set and the image block alone, and that image is kept.
363
+ const attachments = attachmentsOf(blocks);
364
+ if (rec.isMeta && attachments.length === 0)
365
+ continue;
366
+ const texts = rec.isMeta ? [] : typeof content === "string" ? [content] : blocks.filter((b) => b.type === "text" && typeof b.text === "string").map((b) => b.text);
367
+ userTurn(texts, attachments, rec, at);
368
+ continue;
369
+ }
370
+ // assistant
371
+ const model = rec.message?.model;
372
+ if (typeof model === "string")
373
+ models[model] = (models[model] || 0) + 1;
374
+ const body = textOf(content);
375
+ if (body.trim()) {
376
+ pushMessage({ role: "assistant", content: body, time: at, model, key: rec.uuid }, rec);
377
+ }
378
+ for (const b of blocks) {
379
+ if (b.type !== "tool_use" || typeof b.id !== "string" || typeof b.name !== "string")
380
+ continue;
381
+ const call = {
382
+ key: b.id,
383
+ name: b.name,
384
+ input: b.input,
385
+ status: "unknown",
386
+ startedAt: at,
387
+ afterMessage: messages.length - 1,
388
+ };
389
+ callsByKey.set(b.id, call);
390
+ toolCalls.push(call);
391
+ }
392
+ }
393
+ /** One user turn's text blocks, each classified: prose and the renderings
394
+ * of harness text become the message, a task notification becomes a tool
395
+ * row. Shared by `user` records and queued turns, which carry the same
396
+ * kinds of text by a different route. */
397
+ function userTurn(texts, attachments, rec, at) {
398
+ const kept = [];
399
+ for (const text of texts) {
400
+ const block = classifyUserBlock(text);
401
+ if (block.kind === "drop")
402
+ continue;
403
+ if (block.kind === "notification") {
404
+ // A finished background task is the completion of a call the
405
+ // agent made, not something the operator said: a tool row of its
406
+ // own, parented to the call when the harness names it. Keyed on the
407
+ // record uuid, since one task can notify more than once.
408
+ const call = {
409
+ key: `task:${rec.uuid ?? `${at}:${toolCalls.length}`}`,
410
+ name: "task-notification",
411
+ input: { task_id: block.taskId, summary: block.summary },
412
+ output: block.result,
413
+ status: block.status === "failed" ? "failed" : "completed",
414
+ startedAt: at,
415
+ afterMessage: messages.length - 1,
416
+ parentKey: block.toolUseId ?? undefined,
417
+ };
418
+ toolCalls.push(call);
419
+ continue;
420
+ }
421
+ if (block.kind === "prose" && firstProse === null && !rec.isCompactSummary)
422
+ firstProse = block.text;
423
+ kept.push(block.text);
424
+ }
425
+ const body = kept.join("\n\n");
426
+ if (!body.trim() && attachments.length === 0)
427
+ return;
428
+ pushMessage({
429
+ role: rec.isCompactSummary ? "system" : "user",
430
+ // An image with no words gets the placeholder the web app writes for
431
+ // an image-only signal, so it renders instead of being dropped.
432
+ content: body.trim() ? body : "(attachment)",
433
+ time: at,
434
+ key: rec.uuid,
435
+ ...(attachments.length > 0 ? { attachments } : {}),
436
+ }, rec);
437
+ }
438
+ function pushMessage(msg, rec) {
439
+ const idx = messages.length;
440
+ if (rec.parentUuid) {
441
+ const parentIdx = seqOfUuid.get(rec.parentUuid);
442
+ // Same rule as the ChatGPT linearizer: `re` only where the parent is not
443
+ // simply the previous message, so a straight line stays unadorned and a
444
+ // branch point is visible.
445
+ if (typeof parentIdx === "number" && parentIdx !== idx - 1) {
446
+ msg.re = [parentIdx + 1]; // seq is 1-based
447
+ }
448
+ }
449
+ messages.push(msg);
450
+ if (rec.uuid)
451
+ seqOfUuid.set(rec.uuid, idx);
452
+ }
453
+ if (messages.length === 0)
454
+ return null;
455
+ if (!title) {
456
+ // Something the operator wrote, in preference to a slash command or a
457
+ // `!` command they ran first, which would title the session `/clear`.
458
+ const first = firstProse ?? messages.find((m) => m.role === "user")?.content;
459
+ if (first) {
460
+ const line = first.split("\n").find((l) => l.trim()) ?? "";
461
+ title = line.length > 80 ? line.slice(0, 77) + "…" : line;
462
+ }
463
+ }
464
+ let model;
465
+ let best = 0;
466
+ for (const [m, n] of Object.entries(models)) {
467
+ if (n > best) {
468
+ best = n;
469
+ model = m;
470
+ }
471
+ }
472
+ return {
473
+ externalId: candidate.externalId,
474
+ title: title || null,
475
+ createdAt: createdAt ?? candidate.startedAt ?? candidate.updatedAt,
476
+ updatedAt: updatedAt ?? candidate.updatedAt,
477
+ messages,
478
+ model,
479
+ toolCalls,
480
+ cwd,
481
+ };
482
+ }
483
+ export const claudeCodeSource = {
484
+ name: "claude",
485
+ // 1: messages sent mid-turn (`queued_command` attachments).
486
+ parserVersion: 1,
487
+ locate(home) {
488
+ const root = claudeProjectsRoot(home);
489
+ return existsSync(root) ? root : null;
490
+ },
491
+ identity: claudeIdentity,
492
+ survey: surveyClaudeSessions,
493
+ extract(candidate) {
494
+ return parseClaudeSession(readFileSync(candidate.path, "utf-8"), candidate);
495
+ },
496
+ };
497
+ /** A store handed in by path instead of found under HOME: either the
498
+ * `projects` directory itself or a copied `~/.claude` that contains one. */
499
+ export function resolveClaudeRoot(path) {
500
+ if (existsSync(join(path, "projects")) && basename(path) === ".claude")
501
+ return join(path, "projects");
502
+ return existsSync(path) ? path : null;
503
+ }
504
+ //# sourceMappingURL=claude-code.js.map