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.
- package/AGENTS.md +362 -0
- package/CHANGELOG.md +33 -0
- package/CONTRIBUTING.md +91 -0
- package/DESIGN.md +402 -0
- package/Dockerfile +61 -0
- package/LICENSE +656 -17
- package/README.md +174 -2
- package/SECURITY.md +205 -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 +116 -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 +332 -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 +1094 -0
- package/dist/commands/token.js +153 -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 +6349 -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 +633 -0
- package/dist/db/tokens.js +583 -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/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 +82 -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 +197 -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/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 +49 -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 +262 -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 +85 -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 +137 -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 +115 -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/thin-cli.test.js +382 -0
- package/dist/test/token-bootstrap.test.js +272 -0
- package/dist/test/token-devices.test.js +587 -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 +581 -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/tls.js +148 -0
- package/dist/tmux.js +202 -0
- package/dist/user-agent.js +46 -0
- package/docker-entrypoint.sh +141 -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 +53 -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 +211 -0
- package/docs/token-rotation.md +413 -0
- package/docs/view-urls.md +114 -0
- package/docs/web-app.md +23 -0
- package/docs/wiki.md +43 -0
- package/expo/dist/_expo/static/js/web/entry-e56efc1291dca7258ea00b8108b033a4.js +1097 -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 +48 -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,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
|