brane 0.1.1 → 0.3.1

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 (352) hide show
  1. package/AGENTS.md +367 -0
  2. package/CHANGELOG.md +66 -0
  3. package/CONTRIBUTING.md +101 -0
  4. package/DESIGN.md +405 -0
  5. package/Dockerfile +61 -0
  6. package/LICENSE +656 -17
  7. package/README.md +174 -2
  8. package/SECURITY.md +213 -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 +133 -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 +360 -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 +1101 -0
  37. package/dist/commands/token.js +154 -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 +6353 -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 +643 -0
  58. package/dist/db/tokens.js +624 -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/app-origin.js +26 -0
  96. package/dist/shared/block-glyphs.js +114 -0
  97. package/dist/shared/diagram-css.js +17 -0
  98. package/dist/shared/export-selection.js +101 -0
  99. package/dist/shared/find-in-text.js +110 -0
  100. package/dist/shared/glow-hues.js +225 -0
  101. package/dist/shared/hex-dump.js +62 -0
  102. package/dist/shared/hex-pages.js +52 -0
  103. package/dist/shared/hit-drum.js +79 -0
  104. package/dist/shared/known-nids.js +58 -0
  105. package/dist/shared/list-scroll-keys.js +56 -0
  106. package/dist/shared/markdown-lists.js +72 -0
  107. package/dist/shared/markdown-math.js +485 -0
  108. package/dist/shared/markdown-mentions.js +79 -0
  109. package/dist/shared/markdown-nids.js +41 -0
  110. package/dist/shared/markdown-protocols.js +53 -0
  111. package/dist/shared/markdown-refs.js +123 -0
  112. package/dist/shared/membrane-invasion.js +342 -0
  113. package/dist/shared/membrane-motion.js +83 -0
  114. package/dist/shared/membrane-palette.js +253 -0
  115. package/dist/shared/membrane-terrain.js +212 -0
  116. package/dist/shared/message-selection.js +123 -0
  117. package/dist/shared/mo-glyph.js +57 -0
  118. package/dist/shared/narrative-format.js +237 -0
  119. package/dist/shared/narrative-menu.js +84 -0
  120. package/dist/shared/narrative-sort.js +47 -0
  121. package/dist/shared/narrative-status.js +52 -0
  122. package/dist/shared/no-pan.js +85 -0
  123. package/dist/shared/pane-layout.js +50 -0
  124. package/dist/shared/payload-inline.js +83 -0
  125. package/dist/shared/speaker-color.js +61 -0
  126. package/dist/shared/token-rotation.js +14 -0
  127. package/dist/shared/view-url.js +216 -0
  128. package/dist/shared/wiki-links.js +70 -0
  129. package/dist/shared/wiki-window.js +4 -0
  130. package/dist/tailscale.js +55 -0
  131. package/dist/test/absorb-claude.test.js +892 -0
  132. package/dist/test/agent-scope.test.js +193 -0
  133. package/dist/test/agents.test.js +55 -0
  134. package/dist/test/api-request.test.js +122 -0
  135. package/dist/test/app-host-literal.test.js +53 -0
  136. package/dist/test/awaiting-glow.test.js +54 -0
  137. package/dist/test/awaiting.test.js +122 -0
  138. package/dist/test/bind-token.test.js +88 -0
  139. package/dist/test/block-glyphs.test.js +68 -0
  140. package/dist/test/cert-manager.test.js +262 -0
  141. package/dist/test/claude-live-session.test.js +110 -0
  142. package/dist/test/codex-tool-log.test.js +568 -0
  143. package/dist/test/config.test.js +71 -0
  144. package/dist/test/copilot-tool-log.test.js +93 -0
  145. package/dist/test/cors-preflight.test.js +56 -0
  146. package/dist/test/cost-linger.test.js +31 -0
  147. package/dist/test/cost-pricing.test.js +72 -0
  148. package/dist/test/cost-reprice.test.js +139 -0
  149. package/dist/test/cost-sessions.test.js +301 -0
  150. package/dist/test/crush-tool-log.test.js +289 -0
  151. package/dist/test/daemon-client.test.js +109 -0
  152. package/dist/test/daemon-socket.test.js +293 -0
  153. package/dist/test/diagram-arrows.test.js +205 -0
  154. package/dist/test/diagram-export.test.js +210 -0
  155. package/dist/test/diagram-labels.test.js +313 -0
  156. package/dist/test/diagram-parse.test.js +191 -0
  157. package/dist/test/diagram-render.test.js +175 -0
  158. package/dist/test/diagram-review.test.js +288 -0
  159. package/dist/test/diagram-self-loop-labels.test.js +303 -0
  160. package/dist/test/embedder.test.js +84 -0
  161. package/dist/test/events.test.js +123 -0
  162. package/dist/test/export-selection.test.js +102 -0
  163. package/dist/test/export.test.js +505 -0
  164. package/dist/test/files-browse.test.js +393 -0
  165. package/dist/test/files-find.test.js +272 -0
  166. package/dist/test/find-in-text.test.js +86 -0
  167. package/dist/test/first-paint-viewport.test.js +59 -0
  168. package/dist/test/fixtures/embedding-golden.js +23 -0
  169. package/dist/test/fixtures/wordpiece-golden.js +167 -0
  170. package/dist/test/glow-hues.test.js +202 -0
  171. package/dist/test/harness-cost.test.js +423 -0
  172. package/dist/test/helpers.js +21 -0
  173. package/dist/test/hex-pages.test.js +68 -0
  174. package/dist/test/highlight.test.js +69 -0
  175. package/dist/test/hit-drum.test.js +71 -0
  176. package/dist/test/hook-config.test.js +299 -0
  177. package/dist/test/json-gzip.test.js +70 -0
  178. package/dist/test/key-routing.test.js +58 -0
  179. package/dist/test/known-nids.test.js +106 -0
  180. package/dist/test/link-preview.test.js +67 -0
  181. package/dist/test/list-scroll-keys.test.js +66 -0
  182. package/dist/test/listen-own-words.test.js +147 -0
  183. package/dist/test/listen-timeout.test.js +33 -0
  184. package/dist/test/markdown-diagnostics.test.js +121 -0
  185. package/dist/test/markdown-lists.test.js +182 -0
  186. package/dist/test/markdown-math.test.js +256 -0
  187. package/dist/test/markdown-nids.test.js +146 -0
  188. package/dist/test/markdown-protocols.test.js +139 -0
  189. package/dist/test/mcp-bridge-cancel.test.js +98 -0
  190. package/dist/test/mcp-bridge-reconnect.test.js +121 -0
  191. package/dist/test/mcp-narrative.test.js +193 -0
  192. package/dist/test/mcp.test.js +548 -0
  193. package/dist/test/meld.test.js +143 -0
  194. package/dist/test/membrane-invasion.test.js +482 -0
  195. package/dist/test/membrane-motion.test.js +101 -0
  196. package/dist/test/membrane-palette.test.js +166 -0
  197. package/dist/test/membrane-terrain.test.js +170 -0
  198. package/dist/test/memory-log.test.js +202 -0
  199. package/dist/test/mentions-e2e.test.js +261 -0
  200. package/dist/test/mentions-ledger.test.js +265 -0
  201. package/dist/test/mentions-listen.test.js +265 -0
  202. package/dist/test/mentions.test.js +238 -0
  203. package/dist/test/message-selection.test.js +114 -0
  204. package/dist/test/mo-glyph.test.js +22 -0
  205. package/dist/test/model-resolution.test.js +166 -0
  206. package/dist/test/narrate-validate.test.js +77 -0
  207. package/dist/test/narrate-wait.test.js +160 -0
  208. package/dist/test/narrative-format.test.js +219 -0
  209. package/dist/test/narrative-menu.test.js +150 -0
  210. package/dist/test/narrative-sort.test.js +75 -0
  211. package/dist/test/no-pan.test.js +60 -0
  212. package/dist/test/pane-layout.test.js +153 -0
  213. package/dist/test/paths.test.js +37 -0
  214. package/dist/test/payload-inline.test.js +91 -0
  215. package/dist/test/precache-fonts.test.js +72 -0
  216. package/dist/test/presence-gate.test.js +440 -0
  217. package/dist/test/presence.test.js +160 -0
  218. package/dist/test/read-events.test.js +220 -0
  219. package/dist/test/remote-meld.test.js +128 -0
  220. package/dist/test/revive-cli.test.js +93 -0
  221. package/dist/test/search-floor.test.js +206 -0
  222. package/dist/test/search-fusion.test.js +168 -0
  223. package/dist/test/search.test.js +205 -0
  224. package/dist/test/security.test.js +48 -0
  225. package/dist/test/selection-wiring.test.js +61 -0
  226. package/dist/test/session-affinity.test.js +295 -0
  227. package/dist/test/signal-source.test.js +32 -0
  228. package/dist/test/socket-account.test.js +268 -0
  229. package/dist/test/spawn-toml.test.js +72 -0
  230. package/dist/test/speaker-color.test.js +90 -0
  231. package/dist/test/tailscale-rebind.test.js +79 -0
  232. package/dist/test/terminal-cursor.test.js +61 -0
  233. package/dist/test/terminal-resync.test.js +23 -0
  234. package/dist/test/terminal-size.test.js +36 -0
  235. package/dist/test/thin-cli.test.js +382 -0
  236. package/dist/test/token-bootstrap.test.js +272 -0
  237. package/dist/test/token-devices.test.js +646 -0
  238. package/dist/test/token-lapse.test.js +94 -0
  239. package/dist/test/token-ring.test.js +188 -0
  240. package/dist/test/token-rotate-cli.test.js +58 -0
  241. package/dist/test/token-routes.test.js +95 -0
  242. package/dist/test/tool-uses.test.js +1006 -0
  243. package/dist/test/usage-ledger.test.js +126 -0
  244. package/dist/test/view-url.test.js +627 -0
  245. package/dist/test/wiki-links.test.js +57 -0
  246. package/dist/test/wiki-sources.test.js +85 -0
  247. package/dist/test/wiki-window.test.js +31 -0
  248. package/dist/test/wiki.test.js +399 -0
  249. package/dist/test/wordpiece.test.js +68 -0
  250. package/dist/test/wrangler-route.test.js +27 -0
  251. package/dist/tls.js +148 -0
  252. package/dist/tmux.js +213 -0
  253. package/dist/user-agent.js +46 -0
  254. package/docker-entrypoint.sh +154 -0
  255. package/docs/README.md +3 -0
  256. package/docs/absorb.md +231 -0
  257. package/docs/cost-ledger.md +125 -0
  258. package/docs/daemon-api.md +189 -0
  259. package/docs/diagrams.md +163 -0
  260. package/docs/doctor.md +36 -0
  261. package/docs/export-scope.md +69 -0
  262. package/docs/files-view.md +133 -0
  263. package/docs/link-previews.md +38 -0
  264. package/docs/math.md +32 -0
  265. package/docs/mcp.md +69 -0
  266. package/docs/memory-search.md +75 -0
  267. package/docs/mentions.md +101 -0
  268. package/docs/multi-brane.md +14 -0
  269. package/docs/passkeys.md +165 -0
  270. package/docs/presence.md +202 -0
  271. package/docs/releasing.md +104 -0
  272. package/docs/rendering-warnings.md +22 -0
  273. package/docs/request-layer.md +72 -0
  274. package/docs/selection-store.md +50 -0
  275. package/docs/session-affinity.md +61 -0
  276. package/docs/terminal-mirroring.md +243 -0
  277. package/docs/token-rotation.md +431 -0
  278. package/docs/view-urls.md +134 -0
  279. package/docs/web-app.md +23 -0
  280. package/docs/wiki.md +43 -0
  281. package/expo/dist/_expo/static/js/web/entry-e94247f119fdcd3b424a1764233a41d2.js +1099 -0
  282. package/expo/dist/apple-touch-icon.png +0 -0
  283. package/expo/dist/favicon.svg +3 -0
  284. package/expo/dist/index.html +177 -0
  285. package/expo/dist/katex/fonts/KaTeX_AMS-Regular.woff2 +0 -0
  286. package/expo/dist/katex/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
  287. package/expo/dist/katex/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
  288. package/expo/dist/katex/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
  289. package/expo/dist/katex/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
  290. package/expo/dist/katex/fonts/KaTeX_Main-Bold.woff2 +0 -0
  291. package/expo/dist/katex/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
  292. package/expo/dist/katex/fonts/KaTeX_Main-Italic.woff2 +0 -0
  293. package/expo/dist/katex/fonts/KaTeX_Main-Regular.woff2 +0 -0
  294. package/expo/dist/katex/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
  295. package/expo/dist/katex/fonts/KaTeX_Math-Italic.woff2 +0 -0
  296. package/expo/dist/katex/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
  297. package/expo/dist/katex/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
  298. package/expo/dist/katex/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
  299. package/expo/dist/katex/fonts/KaTeX_Script-Regular.woff2 +0 -0
  300. package/expo/dist/katex/fonts/KaTeX_Size1-Regular.woff2 +0 -0
  301. package/expo/dist/katex/fonts/KaTeX_Size2-Regular.woff2 +0 -0
  302. package/expo/dist/katex/fonts/KaTeX_Size3-Regular.woff2 +0 -0
  303. package/expo/dist/katex/fonts/KaTeX_Size4-Regular.woff2 +0 -0
  304. package/expo/dist/katex/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
  305. package/expo/dist/katex/katex.min.css +1 -0
  306. package/expo/dist/manifest.json +16 -0
  307. package/expo/dist/metadata.json +1 -0
  308. package/expo/dist/og/membrane.v1.png +0 -0
  309. package/expo/dist/pdf.min.mjs +28 -0
  310. package/expo/dist/pdf.worker.min.mjs +28 -0
  311. package/expo/dist/sw.js +154 -0
  312. package/package.json +79 -27
  313. package/protocols/LIVE_loop.md +407 -0
  314. package/protocols/bebrief.md +41 -0
  315. package/protocols/digest.md +231 -0
  316. package/protocols/fly.md +266 -0
  317. package/protocols/idle.md +81 -0
  318. package/protocols/keeper.md +829 -0
  319. package/protocols/markdown.md +80 -0
  320. package/protocols/memory.md +419 -0
  321. package/protocols/mentions.md +93 -0
  322. package/protocols/payloads.md +53 -0
  323. package/protocols/prelude.md +91 -0
  324. package/protocols/ramify.md +105 -0
  325. package/protocols/review.md +356 -0
  326. package/protocols/rrr.md +232 -0
  327. package/protocols/scribe.md +429 -0
  328. package/protocols/summarize.md +203 -0
  329. package/protocols/twin.md +174 -0
  330. package/protocols/wiki.md +438 -0
  331. package/protocols/worker.md +38 -0
  332. package/wiki/shared/append-only-event-log.md +148 -0
  333. package/wiki/shared/index.md +53 -0
  334. package/wiki/shared/owning-your-narratives.md +161 -0
  335. package/wiki/shared/premature-ontologies.md +435 -0
  336. package/wiki/shared/sedimentary-superintelligence.md +128 -0
  337. package/wiki/shared/sequestering-the-narrative.md +143 -0
  338. package/wiki/shared/why-wikis-work.md +216 -0
  339. package/.travis.yml +0 -7
  340. package/lib/brane.cjs.js +0 -12
  341. package/lib/brane.cjs.js.map +0 -1
  342. package/lib/brane.d.ts +0 -11
  343. package/lib/brane.js +0 -6
  344. package/lib/brane.js.map +0 -1
  345. package/lib/tests.d.ts +0 -1
  346. package/lib/tests.js +0 -8
  347. package/lib/tests.js.map +0 -1
  348. package/rollup.config.js +0 -21
  349. package/scripts/test.sh +0 -8
  350. package/src/brane.ts +0 -21
  351. package/src/tests.ts +0 -8
  352. package/tsconfig.json +0 -16
@@ -0,0 +1,543 @@
1
+ /** Core absorb logic — provider-agnostic import into brane */
2
+ import { randomBase36 } from "../ids.js";
3
+ import { copyFileSync, createReadStream, existsSync, readFileSync, readdirSync, statSync, writeFileSync, } from "node:fs";
4
+ import { join, extname } from "node:path";
5
+ import { pipeline } from "node:stream/promises";
6
+ import { Transform } from "node:stream";
7
+ import { getPayloadsDir, toPortablePath } from "../db/schema.js";
8
+ import { recordToolUse } from "../db/tool-uses.js";
9
+ import parserStream from "stream-json";
10
+ import { streamArray } from "stream-json/streamers/stream-array.js";
11
+ import { chatgptProvider } from "./chatgpt.js";
12
+ /** Registry of available providers (stream-JSON based) */
13
+ const providers = [chatgptProvider];
14
+ /** All known provider names (includes non-streaming providers like organelle) */
15
+ const allProviderNames = ["chatgpt", "organelle", "claude"];
16
+ export function getProvider(name) {
17
+ return providers.find((p) => p.name === name);
18
+ }
19
+ export function isKnownProvider(name) {
20
+ return allProviderNames.includes(name);
21
+ }
22
+ export function listProviders() {
23
+ return allProviderNames;
24
+ }
25
+ /** Map message roles to brane event types */
26
+ function roleToEventType(role) {
27
+ switch (role) {
28
+ case "user":
29
+ return "human_signal";
30
+ case "assistant":
31
+ case "system":
32
+ case "tool":
33
+ default:
34
+ return "narration";
35
+ }
36
+ }
37
+ /** Build source string for user-role events */
38
+ function userSource(providerName, identity) {
39
+ if (identity) {
40
+ return `${providerName}:${identity.label}`;
41
+ }
42
+ return `${providerName}:user`;
43
+ }
44
+ /** Parse user.json into an AbsorbIdentity */
45
+ function parseUserJson(text) {
46
+ try {
47
+ const user = JSON.parse(text);
48
+ if (typeof user.id !== "string")
49
+ return undefined;
50
+ return {
51
+ id: user.id,
52
+ email: typeof user.email === "string" ? user.email : undefined,
53
+ label: typeof user.email === "string" ? user.email : user.id,
54
+ };
55
+ }
56
+ catch {
57
+ return undefined;
58
+ }
59
+ }
60
+ /**
61
+ * Extract identity from user.json in an extracted export directory.
62
+ */
63
+ export function extractIdentityFromDirectory(dirPath) {
64
+ const userJsonPath = join(dirPath, "user.json");
65
+ if (!existsSync(userJsonPath))
66
+ return undefined;
67
+ return parseUserJson(readFileSync(userJsonPath, "utf-8"));
68
+ }
69
+ /**
70
+ * Find conversation JSON files in an extracted export directory.
71
+ * Supports both single `conversations.json` and sharded `conversations-NNN.json`.
72
+ * Returns sorted array of absolute file paths, or undefined if none found.
73
+ */
74
+ export function findConversationsInDirectory(dirPath) {
75
+ // Try single conversations.json first
76
+ const singlePath = join(dirPath, "conversations.json");
77
+ if (existsSync(singlePath))
78
+ return [singlePath];
79
+ // Try sharded conversations-NNN.json
80
+ const entries = readdirSync(dirPath);
81
+ const shardKeys = findConversationShardKeys(entries);
82
+ if (shardKeys.length === 0)
83
+ return undefined;
84
+ return shardKeys.map((k) => join(dirPath, k));
85
+ }
86
+ /** Sort keys matching conversations-NNN.json pattern */
87
+ function findConversationShardKeys(keys) {
88
+ const shardPattern = /(?:^|\/)(conversations-\d+\.json)$/;
89
+ return keys
90
+ .filter((k) => shardPattern.test(k))
91
+ .sort((a, b) => {
92
+ const aMatch = a.match(shardPattern)[1];
93
+ const bMatch = b.match(shardPattern)[1];
94
+ return aMatch.localeCompare(bMatch);
95
+ });
96
+ }
97
+ /**
98
+ * Describe the contents of a ChatGPT export directory for dry-run display.
99
+ */
100
+ export function describeExportDirectory(dirPath) {
101
+ const entries = readdirSync(dirPath);
102
+ const convFiles = entries.filter((e) => e === "conversations.json" || /^conversations-\d+\.json$/.test(e));
103
+ const hasUserJson = entries.includes("user.json");
104
+ const knownFiles = new Set([
105
+ ...convFiles,
106
+ "user.json",
107
+ "chat.html",
108
+ ".DS_Store",
109
+ ]);
110
+ const extraFiles = [];
111
+ const mediaDirs = [];
112
+ for (const entry of entries) {
113
+ if (knownFiles.has(entry))
114
+ continue;
115
+ const fullPath = join(dirPath, entry);
116
+ try {
117
+ if (statSync(fullPath).isDirectory()) {
118
+ mediaDirs.push(entry);
119
+ }
120
+ else {
121
+ extraFiles.push(entry);
122
+ }
123
+ }
124
+ catch {
125
+ extraFiles.push(entry);
126
+ }
127
+ }
128
+ return { conversationFiles: convFiles.sort(), hasUserJson, extraFiles, mediaDirs };
129
+ }
130
+ /**
131
+ * Build a file index for resolving asset pointers to local files.
132
+ * Maps file ID prefixes to absolute file paths.
133
+ *
134
+ * Asset pointers use two schemes:
135
+ * - file-service://file-XYZ → files named file-XYZ-* at root or in dalle-generations/
136
+ * - sediment://file_NNNN... → files named file_NNNN...-* in user dirs or root
137
+ */
138
+ export function buildFileIndex(dirPath) {
139
+ const index = new Map();
140
+ function walk(dir) {
141
+ let entries;
142
+ try {
143
+ entries = readdirSync(dir);
144
+ }
145
+ catch {
146
+ return;
147
+ }
148
+ for (const entry of entries) {
149
+ if (entry.startsWith("."))
150
+ continue;
151
+ const fullPath = join(dir, entry);
152
+ try {
153
+ const st = statSync(fullPath);
154
+ if (st.isDirectory()) {
155
+ // Skip conversation JSON directories and other non-media dirs
156
+ if (!/^conversations/.test(entry)) {
157
+ walk(fullPath);
158
+ }
159
+ }
160
+ else if (st.isFile()) {
161
+ // Index by the file ID prefix (before the first -UUID or -filename suffix)
162
+ // file-XYZ-UUID.jpeg → file-XYZ
163
+ // file_NNNN...-UUID.jpeg → file_NNNN...
164
+ const match = entry.match(/^(file[-_][A-Za-z0-9]+)/);
165
+ if (match) {
166
+ index.set(match[1], fullPath);
167
+ }
168
+ }
169
+ }
170
+ catch {
171
+ // skip inaccessible entries
172
+ }
173
+ }
174
+ }
175
+ walk(dirPath);
176
+ return index;
177
+ }
178
+ /**
179
+ * Resolve an asset pointer URI to a local file path using the file index.
180
+ * Returns undefined if the file cannot be found.
181
+ */
182
+ function resolveAssetPointer(pointer, fileIndex) {
183
+ // file-service://file-XYZ → file-XYZ
184
+ // sediment://file_NNNN... → file_NNNN...
185
+ const match = pointer.match(/:\/\/(.+)/);
186
+ if (!match)
187
+ return undefined;
188
+ return fileIndex.get(match[1]);
189
+ }
190
+ /** The extension a stored attachment gets, from its media type. The payload
191
+ * route picks its Content-Type by extension and the chat decides what to
192
+ * render inline the same way, so this is what makes an image show. */
193
+ function extensionFor(mediaType) {
194
+ switch (mediaType.toLowerCase()) {
195
+ case "image/png": return ".png";
196
+ case "image/jpeg": return ".jpg";
197
+ case "image/gif": return ".gif";
198
+ case "image/webp": return ".webp";
199
+ default: return ".bin"; // only `image` blocks reach here, and the API takes only these four
200
+ }
201
+ }
202
+ /** Resolve a message's assets and put them in payload storage: pointers
203
+ * through the export's file index, inline attachments decoded from base64.
204
+ * Both land as `<time>_<seq>_<j><ext>` under the narrative's payload dir,
205
+ * attachments numbered after pointers, so a message may carry either or
206
+ * both. On a dry run nothing is written and `payloadsDir` is never called;
207
+ * the summary is still counted. */
208
+ function resolveAndCopyPayloads(msg, narrativeId, seq, fileIndex, summary, dryRun, payloadsDir) {
209
+ const pointers = msg.assetPointers ?? [];
210
+ const attachments = msg.attachments ?? [];
211
+ if (pointers.length === 0 && attachments.length === 0)
212
+ return undefined;
213
+ const payloadPaths = [];
214
+ for (let j = 0; j < pointers.length; j++) {
215
+ const localPath = resolveAssetPointer(pointers[j], fileIndex);
216
+ if (!localPath) {
217
+ summary.unresolvedAssets++;
218
+ continue;
219
+ }
220
+ try {
221
+ const size = statSync(localPath).size;
222
+ summary.assets++;
223
+ summary.assetsBytes += size;
224
+ if (!dryRun) {
225
+ const ext = extname(localPath) || ".bin";
226
+ const storedName = `${msg.time}_${seq}_${j}${ext}`;
227
+ copyFileSync(localPath, join(payloadsDir(narrativeId), storedName));
228
+ payloadPaths.push(`${narrativeId}/${storedName}`);
229
+ }
230
+ }
231
+ catch {
232
+ summary.unresolvedAssets++;
233
+ }
234
+ }
235
+ for (let k = 0; k < attachments.length; k++) {
236
+ const bytes = Buffer.from(attachments[k].data, "base64");
237
+ if (bytes.length === 0) {
238
+ summary.unresolvedAssets++;
239
+ continue;
240
+ }
241
+ if (!dryRun) {
242
+ const storedName = `${msg.time}_${seq}_${pointers.length + k}${extensionFor(attachments[k].mediaType)}`;
243
+ try {
244
+ writeFileSync(join(payloadsDir(narrativeId), storedName), bytes);
245
+ }
246
+ catch {
247
+ // A full disk or an unwritable payload dir costs this one image, as
248
+ // an unreadable file does on the pointer path; the message still
249
+ // lands, and the import goes on to the next session.
250
+ summary.unresolvedAssets++;
251
+ continue;
252
+ }
253
+ payloadPaths.push(`${narrativeId}/${storedName}`);
254
+ }
255
+ summary.assets++;
256
+ summary.assetsBytes += bytes.length;
257
+ }
258
+ return payloadPaths.length > 0 ? payloadPaths : undefined;
259
+ }
260
+ /** Prepare a run: statements, summary, and the plan/write pair. */
261
+ export function createAbsorbContext(db, providerName, braneId, foldPath, identity, dryRun, fileIndex, opts = {}) {
262
+ const payloadsDir = opts.payloadsDir ?? getPayloadsDir;
263
+ const summary = {
264
+ provider: providerName,
265
+ conversations: 0,
266
+ messages: 0,
267
+ oldest: null,
268
+ newest: null,
269
+ models: {},
270
+ skipped: 0,
271
+ appended: 0,
272
+ assets: 0,
273
+ assetsBytes: 0,
274
+ unresolvedAssets: 0,
275
+ };
276
+ const assetIndex = fileIndex ?? new Map();
277
+ const insertNarrative = db.prepare(`INSERT INTO narratives (id, brane_id, fold_path, agent_type, role, created_at, status, title, archived, agent_session_id)
278
+ VALUES (?, ?, ?, ?, 'absorbed', ?, 'ended', ?, 1, ?)`);
279
+ const insertEvent = db.prepare(`INSERT INTO events (narrative_id, seq, time, type, content, content_type, source, re, payloads, meta)
280
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
281
+ // Only absorbed narratives in *this* brane are candidates for append. A
282
+ // live narrative with the same session id is brane's own record of that
283
+ // session and is never written to by an import (the local runner skips
284
+ // those as "managed" before the parse; this is the belt to that
285
+ // suspender). An absorbed one in another brane is another destination's
286
+ // copy, and this destination gets its own.
287
+ const checkExisting = db.prepare(`SELECT id FROM narratives
288
+ WHERE agent_session_id = ? AND agent_type = ? AND role = 'absorbed' AND brane_id = ?`);
289
+ const getMaxSeq = db.prepare("SELECT MAX(seq) as max_seq FROM events WHERE narrative_id = ?");
290
+ const getExistingEventCount = db.prepare("SELECT COUNT(*) as cnt FROM events WHERE narrative_id = ?");
291
+ const findMessage = db.prepare("SELECT narrative_id, seq FROM absorb_messages WHERE brane_id = ? AND provider = ? AND key = ?");
292
+ const recordMessage = db.prepare("INSERT OR IGNORE INTO absorb_messages (brane_id, provider, key, narrative_id, seq) VALUES (?, ?, ?, ?, ?)");
293
+ const hasToolUse = db.prepare("SELECT 1 FROM tool_uses WHERE narrative_id = ? AND dedup_key = ?");
294
+ const refreshTitle = db.prepare("UPDATE narratives SET title = ? WHERE id = ? AND (title IS NULL OR title != ?)");
295
+ const source = userSource(providerName, identity);
296
+ function plan(conv) {
297
+ const existing = checkExisting.get(conv.externalId, providerName, braneId);
298
+ const landing = new Map();
299
+ const newMessages = [];
300
+ let continuedFrom = null;
301
+ const keyed = conv.messages.length > 0 && conv.messages.every((m) => typeof m.key === "string");
302
+ let narrativeId;
303
+ let nextSeq;
304
+ if (existing) {
305
+ narrativeId = existing.id;
306
+ const { max_seq } = getMaxSeq.get(existing.id);
307
+ nextSeq = (max_seq ?? 0) + 1;
308
+ }
309
+ else {
310
+ narrativeId = `${providerName}-${randomBase36(8)}`;
311
+ nextSeq = 1;
312
+ }
313
+ if (keyed) {
314
+ // Per-message: a key already held anywhere is not written again. Held
315
+ // in *this* narrative means the file grew; held in another means this
316
+ // file repeats that one's history (a fork), and the thread continues
317
+ // from wherever the repeated run ends.
318
+ let inPrefix = !existing;
319
+ for (let i = 0; i < conv.messages.length; i++) {
320
+ const held = findMessage.get(braneId, providerName, conv.messages[i].key);
321
+ if (held) {
322
+ landing.set(i, { narrativeId: held.narrative_id, seq: held.seq });
323
+ if (inPrefix && held.narrative_id !== narrativeId) {
324
+ continuedFrom = { narrativeId: held.narrative_id, seq: held.seq };
325
+ }
326
+ continue;
327
+ }
328
+ inPrefix = false;
329
+ landing.set(i, { narrativeId, seq: nextSeq++ });
330
+ newMessages.push(i);
331
+ }
332
+ }
333
+ else {
334
+ // Unkeyed (ChatGPT): the count-based append this path has always done.
335
+ const cnt = existing ? getExistingEventCount.get(existing.id).cnt : 0;
336
+ for (let i = 0; i < conv.messages.length; i++) {
337
+ if (i < cnt) {
338
+ landing.set(i, { narrativeId, seq: i + 1 });
339
+ }
340
+ else {
341
+ landing.set(i, { narrativeId, seq: nextSeq++ });
342
+ newMessages.push(i);
343
+ }
344
+ }
345
+ }
346
+ const lateFor = new Map();
347
+ if (existing && keyed) {
348
+ const fresh = new Set(newMessages);
349
+ let lastHeld = -1; // the last message this narrative already holds
350
+ for (let i = 0; i < conv.messages.length; i++) {
351
+ if (!fresh.has(i) && landing.get(i).narrativeId === narrativeId)
352
+ lastHeld = i;
353
+ }
354
+ // The nearest held message before i. A fresh message ahead of every
355
+ // held one (a record type read at the very head of a session) gets no
356
+ // anchor: `re` points back, and there is nothing behind it to point
357
+ // at. No record type this parser reads can arrive there — a queued
358
+ // message follows at least the prompt that started the turn.
359
+ let anchor = null;
360
+ for (let i = 0; i < lastHeld; i++) {
361
+ if (!fresh.has(i))
362
+ anchor = landing.get(i);
363
+ else if (anchor)
364
+ lateFor.set(i, anchor);
365
+ }
366
+ }
367
+ const newToolCalls = [];
368
+ for (let i = 0; i < (conv.toolCalls?.length ?? 0); i++) {
369
+ const call = conv.toolCalls[i];
370
+ // A call anchored to a message that lives in another narrative was
371
+ // recorded there when that narrative was absorbed.
372
+ const anchor = call.afterMessage >= 0 ? landing.get(call.afterMessage) : undefined;
373
+ if (anchor && anchor.narrativeId !== narrativeId)
374
+ continue;
375
+ if (existing && hasToolUse.get(narrativeId, call.key))
376
+ continue;
377
+ newToolCalls.push(i);
378
+ }
379
+ let outcome;
380
+ if (newMessages.length === 0 && newToolCalls.length === 0)
381
+ outcome = "skipped";
382
+ else if (existing)
383
+ outcome = "appended";
384
+ else if (newMessages.length === 0)
385
+ outcome = "skipped"; // calls alone don't make a narrative
386
+ else
387
+ outcome = "inserted";
388
+ if (outcome === "skipped")
389
+ newToolCalls.length = 0;
390
+ return { outcome, narrativeId, newMessages, landing, continuedFrom, lateFor, newToolCalls };
391
+ }
392
+ /** A message's `re` — conversation-local 1-based indices from the parser
393
+ * — resolved through where those messages actually landed. */
394
+ function resolveRe(re, landing, narrativeId) {
395
+ if (!re || re.length === 0)
396
+ return undefined;
397
+ const out = [];
398
+ for (const r of re) {
399
+ if (typeof r !== "number") {
400
+ out.push(r);
401
+ continue;
402
+ }
403
+ const l = landing.get(r - 1);
404
+ if (!l)
405
+ continue;
406
+ out.push(l.narrativeId === narrativeId ? l.seq : `${l.narrativeId}:${l.seq}`);
407
+ }
408
+ return out.length > 0 ? out : undefined;
409
+ }
410
+ const write = dryRun
411
+ ? undefined
412
+ : db.transaction((conv) => {
413
+ const p = plan(conv);
414
+ if (p.outcome === "skipped")
415
+ return p.outcome;
416
+ const { narrativeId } = p;
417
+ if (p.outcome === "inserted") {
418
+ const fold = conv.cwd ? toPortablePath(conv.cwd) : foldPath;
419
+ insertNarrative.run(narrativeId, braneId, fold, providerName, conv.createdAt, conv.title, conv.externalId);
420
+ }
421
+ else if (conv.title) {
422
+ // A harness retitles a session as it goes (Claude Code's
423
+ // `ai-title`), so the title read from a grown file is the better
424
+ // one. Also what gives a rebuilt narrative its title back.
425
+ refreshTitle.run(conv.title, narrativeId, conv.title);
426
+ }
427
+ let first = true;
428
+ for (const i of p.newMessages) {
429
+ const msg = conv.messages[i];
430
+ const { seq } = p.landing.get(i);
431
+ const type = roleToEventType(msg.role);
432
+ const msgSource = msg.role === "user" ? source : providerName;
433
+ const payloads = resolveAndCopyPayloads(msg, narrativeId, seq, assetIndex, summary, false, payloadsDir);
434
+ let re = resolveRe(msg.re, p.landing, narrativeId);
435
+ if (first && p.continuedFrom) {
436
+ const link = `${p.continuedFrom.narrativeId}:${p.continuedFrom.seq}`;
437
+ re = re && !re.includes(link) ? [link, ...re] : [link];
438
+ }
439
+ first = false;
440
+ const late = p.lateFor.get(i);
441
+ if (late) {
442
+ const link = late.narrativeId === narrativeId ? late.seq : `${late.narrativeId}:${late.seq}`;
443
+ re = re && !re.includes(link) ? [link, ...re] : [link];
444
+ }
445
+ insertEvent.run(narrativeId, seq, msg.time, type, msg.content, null, msgSource, re ? JSON.stringify(re) : null, payloads ? JSON.stringify(payloads) : null, msg.model ? JSON.stringify({ model: msg.model }) : null);
446
+ if (msg.key)
447
+ recordMessage.run(braneId, providerName, msg.key, narrativeId, seq);
448
+ }
449
+ for (const i of p.newToolCalls) {
450
+ const call = conv.toolCalls[i];
451
+ const anchor = call.afterMessage >= 0 ? p.landing.get(call.afterMessage) : undefined;
452
+ recordToolUse(db, narrativeId, {
453
+ dedupKey: call.key,
454
+ name: call.name,
455
+ input: call.input,
456
+ output: call.output,
457
+ status: call.status,
458
+ startedAt: call.startedAt,
459
+ durationMs: call.durationMs,
460
+ afterSeq: anchor?.seq ?? 0,
461
+ source: "absorb",
462
+ agentType: providerName,
463
+ parentId: call.parentKey,
464
+ });
465
+ }
466
+ return p.outcome;
467
+ });
468
+ return {
469
+ db, providerName, braneId, foldPath, identity, fileIndex: assetIndex, summary, dryRun, write, plan, payloadsDir,
470
+ };
471
+ }
472
+ /** Absorb one parsed conversation: write it (or, on a dry run, plan it) and
473
+ * fold the result into the context's summary. */
474
+ export function absorbOne(ctx, conv) {
475
+ const { summary } = ctx;
476
+ if (conv.model)
477
+ summary.models[conv.model] = (summary.models[conv.model] || 0) + 1;
478
+ if (!summary.oldest || conv.createdAt < summary.oldest.time) {
479
+ summary.oldest = { time: conv.createdAt, title: conv.title };
480
+ }
481
+ if (!summary.newest || conv.updatedAt > summary.newest.time) {
482
+ summary.newest = { time: conv.updatedAt, title: conv.title };
483
+ }
484
+ const p = ctx.plan(conv);
485
+ const outcome = ctx.write ? ctx.write(conv) : p.outcome;
486
+ summary.messages += p.newMessages.length;
487
+ if (p.newToolCalls.length > 0)
488
+ summary.toolCalls = (summary.toolCalls ?? 0) + p.newToolCalls.length;
489
+ if (outcome === "inserted")
490
+ summary.conversations++;
491
+ else if (outcome === "appended")
492
+ summary.appended++;
493
+ else
494
+ summary.skipped++;
495
+ if (ctx.dryRun) {
496
+ // Resolve assets on a dry run too, for the report.
497
+ for (const i of p.newMessages) {
498
+ const msg = conv.messages[i];
499
+ if (msg.assetPointers || msg.attachments)
500
+ resolveAndCopyPayloads(msg, "dry-run", 0, ctx.fileIndex, summary, true, ctx.payloadsDir);
501
+ }
502
+ }
503
+ return outcome;
504
+ }
505
+ /**
506
+ * Stream-absorb conversations from one or more readable sources.
507
+ *
508
+ * Uses stream-json to parse each top-level JSON array one element at a time,
509
+ * keeping memory usage proportional to the largest single conversation
510
+ * rather than the total file size.
511
+ *
512
+ * When `dryRun` is true, conversations are parsed and summarized but not
513
+ * written to the database.
514
+ */
515
+ export async function absorb(db, sources, provider, braneId, foldPath, identity, onProgress, dryRun, fileIndex) {
516
+ const ctx = createAbsorbContext(db, provider.name, braneId, foldPath, identity, dryRun === true, fileIndex);
517
+ let processed = 0;
518
+ const sourceList = Array.isArray(sources) ? sources : [sources];
519
+ for (const source of sourceList) {
520
+ // Create a transform that processes each array element
521
+ const processor = new Transform({
522
+ objectMode: true,
523
+ transform(chunk, _encoding, callback) {
524
+ try {
525
+ const conv = provider.parseOne(chunk.value);
526
+ if (conv) {
527
+ absorbOne(ctx, conv);
528
+ processed++;
529
+ onProgress?.(processed, conv.title);
530
+ }
531
+ callback();
532
+ }
533
+ catch (err) {
534
+ callback(err);
535
+ }
536
+ },
537
+ });
538
+ const inputStream = typeof source === "string" ? createReadStream(source) : source;
539
+ await pipeline(inputStream, parserStream(), streamArray.asStream(), processor);
540
+ }
541
+ return ctx.summary;
542
+ }
543
+ //# sourceMappingURL=index.js.map