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.
- package/AGENTS.md +367 -0
- package/CHANGELOG.md +66 -0
- package/CONTRIBUTING.md +101 -0
- package/DESIGN.md +405 -0
- package/Dockerfile +61 -0
- package/LICENSE +656 -17
- package/README.md +174 -2
- package/SECURITY.md +213 -0
- package/dist/absorb/brane-id.js +11 -0
- package/dist/absorb/chatgpt.js +161 -0
- package/dist/absorb/claude-code.js +504 -0
- package/dist/absorb/index.js +543 -0
- package/dist/absorb/organelle.js +345 -0
- package/dist/absorb/run.js +520 -0
- package/dist/absorb/sources.js +168 -0
- package/dist/absorb/types.js +3 -0
- package/dist/agent-env.js +45 -0
- package/dist/agents.js +1168 -0
- package/dist/claude-live-session.js +124 -0
- package/dist/cli.js +50 -0
- package/dist/codex-tool-log.js +144 -0
- package/dist/commands/absorb.js +291 -0
- package/dist/commands/attach.js +31 -0
- package/dist/commands/certify.js +62 -0
- package/dist/commands/config.js +133 -0
- package/dist/commands/cost.js +126 -0
- package/dist/commands/daemon.js +350 -0
- package/dist/commands/doctor.js +262 -0
- package/dist/commands/fold.js +95 -0
- package/dist/commands/list.js +89 -0
- package/dist/commands/mcp.js +84 -0
- package/dist/commands/meld.js +360 -0
- package/dist/commands/memory.js +215 -0
- package/dist/commands/revive.js +74 -0
- package/dist/commands/run.js +85 -0
- package/dist/commands/spawn.js +1101 -0
- package/dist/commands/token.js +154 -0
- package/dist/copilot-tool-log.js +145 -0
- package/dist/cost/pricing.js +116 -0
- package/dist/cost/reprice.js +136 -0
- package/dist/crush-tool-log.js +293 -0
- package/dist/daemon/agent-scope.js +116 -0
- package/dist/daemon/awaiting.js +54 -0
- package/dist/daemon/cert-manager.js +349 -0
- package/dist/daemon/client.js +262 -0
- package/dist/daemon/gated-routes.js +112 -0
- package/dist/daemon/keep-awake.js +103 -0
- package/dist/daemon/local-socket.js +145 -0
- package/dist/daemon/presence-gate.js +206 -0
- package/dist/daemon/server.js +6353 -0
- package/dist/daemon/signal-source.js +35 -0
- package/dist/daemon/tailscale-rebind.js +58 -0
- package/dist/daemon/token-ring.js +263 -0
- package/dist/daemon/token-routes.js +190 -0
- package/dist/db/events.js +152 -0
- package/dist/db/mentions.js +179 -0
- package/dist/db/schema.js +643 -0
- package/dist/db/tokens.js +624 -0
- package/dist/db/tool-uses.js +647 -0
- package/dist/diagram/index.js +85 -0
- package/dist/diagram/label.js +455 -0
- package/dist/diagram/layout.js +476 -0
- package/dist/diagram/links.js +50 -0
- package/dist/diagram/metrics.js +152 -0
- package/dist/diagram/parse.js +487 -0
- package/dist/diagram/svg.js +389 -0
- package/dist/diagram/types.js +9 -0
- package/dist/export.js +629 -0
- package/dist/files/archive-types.js +2 -0
- package/dist/files/archive.js +182 -0
- package/dist/files/browse.js +327 -0
- package/dist/files/bytes.js +162 -0
- package/dist/files/codecs.js +33 -0
- package/dist/files/find.js +212 -0
- package/dist/files/tar.js +223 -0
- package/dist/files/zip.js +209 -0
- package/dist/folds.js +72 -0
- package/dist/grammars/ebnf.js +92 -0
- package/dist/highlight.js +250 -0
- package/dist/ids.js +24 -0
- package/dist/markdown-diagnostics.js +233 -0
- package/dist/math.js +168 -0
- package/dist/mcp/server.js +886 -0
- package/dist/memory/embedder.js +145 -0
- package/dist/memory/embedding-worker.js +71 -0
- package/dist/memory/embeddings.js +91 -0
- package/dist/memory/index.js +232 -0
- package/dist/memory/storage.js +587 -0
- package/dist/memory/wiki.js +688 -0
- package/dist/memory/wordpiece.js +247 -0
- package/dist/mentions-scan.js +40 -0
- package/dist/presence.js +259 -0
- package/dist/session-affinity.js +185 -0
- package/dist/shared/api-request.js +124 -0
- package/dist/shared/app-origin.js +26 -0
- package/dist/shared/block-glyphs.js +114 -0
- package/dist/shared/diagram-css.js +17 -0
- package/dist/shared/export-selection.js +101 -0
- package/dist/shared/find-in-text.js +110 -0
- package/dist/shared/glow-hues.js +225 -0
- package/dist/shared/hex-dump.js +62 -0
- package/dist/shared/hex-pages.js +52 -0
- package/dist/shared/hit-drum.js +79 -0
- package/dist/shared/known-nids.js +58 -0
- package/dist/shared/list-scroll-keys.js +56 -0
- package/dist/shared/markdown-lists.js +72 -0
- package/dist/shared/markdown-math.js +485 -0
- package/dist/shared/markdown-mentions.js +79 -0
- package/dist/shared/markdown-nids.js +41 -0
- package/dist/shared/markdown-protocols.js +53 -0
- package/dist/shared/markdown-refs.js +123 -0
- package/dist/shared/membrane-invasion.js +342 -0
- package/dist/shared/membrane-motion.js +83 -0
- package/dist/shared/membrane-palette.js +253 -0
- package/dist/shared/membrane-terrain.js +212 -0
- package/dist/shared/message-selection.js +123 -0
- package/dist/shared/mo-glyph.js +57 -0
- package/dist/shared/narrative-format.js +237 -0
- package/dist/shared/narrative-menu.js +84 -0
- package/dist/shared/narrative-sort.js +47 -0
- package/dist/shared/narrative-status.js +52 -0
- package/dist/shared/no-pan.js +85 -0
- package/dist/shared/pane-layout.js +50 -0
- package/dist/shared/payload-inline.js +83 -0
- package/dist/shared/speaker-color.js +61 -0
- package/dist/shared/token-rotation.js +14 -0
- package/dist/shared/view-url.js +216 -0
- package/dist/shared/wiki-links.js +70 -0
- package/dist/shared/wiki-window.js +4 -0
- package/dist/tailscale.js +55 -0
- package/dist/test/absorb-claude.test.js +892 -0
- package/dist/test/agent-scope.test.js +193 -0
- package/dist/test/agents.test.js +55 -0
- package/dist/test/api-request.test.js +122 -0
- package/dist/test/app-host-literal.test.js +53 -0
- package/dist/test/awaiting-glow.test.js +54 -0
- package/dist/test/awaiting.test.js +122 -0
- package/dist/test/bind-token.test.js +88 -0
- package/dist/test/block-glyphs.test.js +68 -0
- package/dist/test/cert-manager.test.js +262 -0
- package/dist/test/claude-live-session.test.js +110 -0
- package/dist/test/codex-tool-log.test.js +568 -0
- package/dist/test/config.test.js +71 -0
- package/dist/test/copilot-tool-log.test.js +93 -0
- package/dist/test/cors-preflight.test.js +56 -0
- package/dist/test/cost-linger.test.js +31 -0
- package/dist/test/cost-pricing.test.js +72 -0
- package/dist/test/cost-reprice.test.js +139 -0
- package/dist/test/cost-sessions.test.js +301 -0
- package/dist/test/crush-tool-log.test.js +289 -0
- package/dist/test/daemon-client.test.js +109 -0
- package/dist/test/daemon-socket.test.js +293 -0
- package/dist/test/diagram-arrows.test.js +205 -0
- package/dist/test/diagram-export.test.js +210 -0
- package/dist/test/diagram-labels.test.js +313 -0
- package/dist/test/diagram-parse.test.js +191 -0
- package/dist/test/diagram-render.test.js +175 -0
- package/dist/test/diagram-review.test.js +288 -0
- package/dist/test/diagram-self-loop-labels.test.js +303 -0
- package/dist/test/embedder.test.js +84 -0
- package/dist/test/events.test.js +123 -0
- package/dist/test/export-selection.test.js +102 -0
- package/dist/test/export.test.js +505 -0
- package/dist/test/files-browse.test.js +393 -0
- package/dist/test/files-find.test.js +272 -0
- package/dist/test/find-in-text.test.js +86 -0
- package/dist/test/first-paint-viewport.test.js +59 -0
- package/dist/test/fixtures/embedding-golden.js +23 -0
- package/dist/test/fixtures/wordpiece-golden.js +167 -0
- package/dist/test/glow-hues.test.js +202 -0
- package/dist/test/harness-cost.test.js +423 -0
- package/dist/test/helpers.js +21 -0
- package/dist/test/hex-pages.test.js +68 -0
- package/dist/test/highlight.test.js +69 -0
- package/dist/test/hit-drum.test.js +71 -0
- package/dist/test/hook-config.test.js +299 -0
- package/dist/test/json-gzip.test.js +70 -0
- package/dist/test/key-routing.test.js +58 -0
- package/dist/test/known-nids.test.js +106 -0
- package/dist/test/link-preview.test.js +67 -0
- package/dist/test/list-scroll-keys.test.js +66 -0
- package/dist/test/listen-own-words.test.js +147 -0
- package/dist/test/listen-timeout.test.js +33 -0
- package/dist/test/markdown-diagnostics.test.js +121 -0
- package/dist/test/markdown-lists.test.js +182 -0
- package/dist/test/markdown-math.test.js +256 -0
- package/dist/test/markdown-nids.test.js +146 -0
- package/dist/test/markdown-protocols.test.js +139 -0
- package/dist/test/mcp-bridge-cancel.test.js +98 -0
- package/dist/test/mcp-bridge-reconnect.test.js +121 -0
- package/dist/test/mcp-narrative.test.js +193 -0
- package/dist/test/mcp.test.js +548 -0
- package/dist/test/meld.test.js +143 -0
- package/dist/test/membrane-invasion.test.js +482 -0
- package/dist/test/membrane-motion.test.js +101 -0
- package/dist/test/membrane-palette.test.js +166 -0
- package/dist/test/membrane-terrain.test.js +170 -0
- package/dist/test/memory-log.test.js +202 -0
- package/dist/test/mentions-e2e.test.js +261 -0
- package/dist/test/mentions-ledger.test.js +265 -0
- package/dist/test/mentions-listen.test.js +265 -0
- package/dist/test/mentions.test.js +238 -0
- package/dist/test/message-selection.test.js +114 -0
- package/dist/test/mo-glyph.test.js +22 -0
- package/dist/test/model-resolution.test.js +166 -0
- package/dist/test/narrate-validate.test.js +77 -0
- package/dist/test/narrate-wait.test.js +160 -0
- package/dist/test/narrative-format.test.js +219 -0
- package/dist/test/narrative-menu.test.js +150 -0
- package/dist/test/narrative-sort.test.js +75 -0
- package/dist/test/no-pan.test.js +60 -0
- package/dist/test/pane-layout.test.js +153 -0
- package/dist/test/paths.test.js +37 -0
- package/dist/test/payload-inline.test.js +91 -0
- package/dist/test/precache-fonts.test.js +72 -0
- package/dist/test/presence-gate.test.js +440 -0
- package/dist/test/presence.test.js +160 -0
- package/dist/test/read-events.test.js +220 -0
- package/dist/test/remote-meld.test.js +128 -0
- package/dist/test/revive-cli.test.js +93 -0
- package/dist/test/search-floor.test.js +206 -0
- package/dist/test/search-fusion.test.js +168 -0
- package/dist/test/search.test.js +205 -0
- package/dist/test/security.test.js +48 -0
- package/dist/test/selection-wiring.test.js +61 -0
- package/dist/test/session-affinity.test.js +295 -0
- package/dist/test/signal-source.test.js +32 -0
- package/dist/test/socket-account.test.js +268 -0
- package/dist/test/spawn-toml.test.js +72 -0
- package/dist/test/speaker-color.test.js +90 -0
- package/dist/test/tailscale-rebind.test.js +79 -0
- package/dist/test/terminal-cursor.test.js +61 -0
- package/dist/test/terminal-resync.test.js +23 -0
- package/dist/test/terminal-size.test.js +36 -0
- package/dist/test/thin-cli.test.js +382 -0
- package/dist/test/token-bootstrap.test.js +272 -0
- package/dist/test/token-devices.test.js +646 -0
- package/dist/test/token-lapse.test.js +94 -0
- package/dist/test/token-ring.test.js +188 -0
- package/dist/test/token-rotate-cli.test.js +58 -0
- package/dist/test/token-routes.test.js +95 -0
- package/dist/test/tool-uses.test.js +1006 -0
- package/dist/test/usage-ledger.test.js +126 -0
- package/dist/test/view-url.test.js +627 -0
- package/dist/test/wiki-links.test.js +57 -0
- package/dist/test/wiki-sources.test.js +85 -0
- package/dist/test/wiki-window.test.js +31 -0
- package/dist/test/wiki.test.js +399 -0
- package/dist/test/wordpiece.test.js +68 -0
- package/dist/test/wrangler-route.test.js +27 -0
- package/dist/tls.js +148 -0
- package/dist/tmux.js +213 -0
- package/dist/user-agent.js +46 -0
- package/docker-entrypoint.sh +154 -0
- package/docs/README.md +3 -0
- package/docs/absorb.md +231 -0
- package/docs/cost-ledger.md +125 -0
- package/docs/daemon-api.md +189 -0
- package/docs/diagrams.md +163 -0
- package/docs/doctor.md +36 -0
- package/docs/export-scope.md +69 -0
- package/docs/files-view.md +133 -0
- package/docs/link-previews.md +38 -0
- package/docs/math.md +32 -0
- package/docs/mcp.md +69 -0
- package/docs/memory-search.md +75 -0
- package/docs/mentions.md +101 -0
- package/docs/multi-brane.md +14 -0
- package/docs/passkeys.md +165 -0
- package/docs/presence.md +202 -0
- package/docs/releasing.md +104 -0
- package/docs/rendering-warnings.md +22 -0
- package/docs/request-layer.md +72 -0
- package/docs/selection-store.md +50 -0
- package/docs/session-affinity.md +61 -0
- package/docs/terminal-mirroring.md +243 -0
- package/docs/token-rotation.md +431 -0
- package/docs/view-urls.md +134 -0
- package/docs/web-app.md +23 -0
- package/docs/wiki.md +43 -0
- package/expo/dist/_expo/static/js/web/entry-e94247f119fdcd3b424a1764233a41d2.js +1099 -0
- package/expo/dist/apple-touch-icon.png +0 -0
- package/expo/dist/favicon.svg +3 -0
- package/expo/dist/index.html +177 -0
- package/expo/dist/katex/fonts/KaTeX_AMS-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Main-Bold.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Main-Italic.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Main-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Math-Italic.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Script-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Size1-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Size2-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Size3-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Size4-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
- package/expo/dist/katex/katex.min.css +1 -0
- package/expo/dist/manifest.json +16 -0
- package/expo/dist/metadata.json +1 -0
- package/expo/dist/og/membrane.v1.png +0 -0
- package/expo/dist/pdf.min.mjs +28 -0
- package/expo/dist/pdf.worker.min.mjs +28 -0
- package/expo/dist/sw.js +154 -0
- package/package.json +79 -27
- package/protocols/LIVE_loop.md +407 -0
- package/protocols/bebrief.md +41 -0
- package/protocols/digest.md +231 -0
- package/protocols/fly.md +266 -0
- package/protocols/idle.md +81 -0
- package/protocols/keeper.md +829 -0
- package/protocols/markdown.md +80 -0
- package/protocols/memory.md +419 -0
- package/protocols/mentions.md +93 -0
- package/protocols/payloads.md +53 -0
- package/protocols/prelude.md +91 -0
- package/protocols/ramify.md +105 -0
- package/protocols/review.md +356 -0
- package/protocols/rrr.md +232 -0
- package/protocols/scribe.md +429 -0
- package/protocols/summarize.md +203 -0
- package/protocols/twin.md +174 -0
- package/protocols/wiki.md +438 -0
- package/protocols/worker.md +38 -0
- package/wiki/shared/append-only-event-log.md +148 -0
- package/wiki/shared/index.md +53 -0
- package/wiki/shared/owning-your-narratives.md +161 -0
- package/wiki/shared/premature-ontologies.md +435 -0
- package/wiki/shared/sedimentary-superintelligence.md +128 -0
- package/wiki/shared/sequestering-the-narrative.md +143 -0
- package/wiki/shared/why-wikis-work.md +216 -0
- package/.travis.yml +0 -7
- package/lib/brane.cjs.js +0 -12
- package/lib/brane.cjs.js.map +0 -1
- package/lib/brane.d.ts +0 -11
- package/lib/brane.js +0 -6
- package/lib/brane.js.map +0 -1
- package/lib/tests.d.ts +0 -1
- package/lib/tests.js +0 -8
- package/lib/tests.js.map +0 -1
- package/rollup.config.js +0 -21
- package/scripts/test.sh +0 -8
- package/src/brane.ts +0 -21
- package/src/tests.ts +0 -8
- 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
|