@100xprompt/brain 0.0.0-stage → 0.1.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/LICENSE +48 -0
- package/README.md +235 -2
- package/package.json +48 -4
- package/src/adapters/aider/index.ts +8 -0
- package/src/adapters/aider/reader.ts +129 -0
- package/src/adapters/aider/records.ts +220 -0
- package/src/adapters/aider/writer.ts +269 -0
- package/src/adapters/amazon-q-cli/index.ts +8 -0
- package/src/adapters/amazon-q-cli/reader.ts +84 -0
- package/src/adapters/amazon-q-cli/records.ts +191 -0
- package/src/adapters/amazon-q-cli/writer.ts +370 -0
- package/src/adapters/amazon-q-ide/index.ts +9 -0
- package/src/adapters/amazon-q-ide/reader.ts +131 -0
- package/src/adapters/amazon-q-ide/records.ts +141 -0
- package/src/adapters/amazon-q-ide/writer.ts +325 -0
- package/src/adapters/antigravity/index.ts +9 -0
- package/src/adapters/antigravity/proto.ts +194 -0
- package/src/adapters/antigravity/reader.ts +424 -0
- package/src/adapters/antigravity/records.ts +236 -0
- package/src/adapters/antigravity/writer.ts +463 -0
- package/src/adapters/auggie/index.ts +36 -0
- package/src/adapters/claude-code/index.ts +8 -0
- package/src/adapters/claude-code/reader.ts +639 -0
- package/src/adapters/claude-code/records.ts +88 -0
- package/src/adapters/claude-code/writer.ts +329 -0
- package/src/adapters/cline/index.ts +12 -0
- package/src/adapters/cline/reader.ts +432 -0
- package/src/adapters/cline/records.ts +151 -0
- package/src/adapters/cline/vscdb.ts +56 -0
- package/src/adapters/cline/writer.ts +656 -0
- package/src/adapters/cline/xml.ts +177 -0
- package/src/adapters/cline4/index.ts +9 -0
- package/src/adapters/cline4/reader.ts +179 -0
- package/src/adapters/cline4/records.ts +92 -0
- package/src/adapters/cline4/writer.ts +361 -0
- package/src/adapters/codebuff/index.ts +8 -0
- package/src/adapters/codebuff/project.ts +207 -0
- package/src/adapters/codebuff/reader.ts +215 -0
- package/src/adapters/codebuff/records.ts +173 -0
- package/src/adapters/codebuff/writer.ts +383 -0
- package/src/adapters/codex/index.ts +8 -0
- package/src/adapters/codex/native.ts +239 -0
- package/src/adapters/codex/reader.ts +747 -0
- package/src/adapters/codex/records.ts +114 -0
- package/src/adapters/codex/writer.ts +479 -0
- package/src/adapters/continue/index.ts +9 -0
- package/src/adapters/continue/reader.ts +68 -0
- package/src/adapters/continue/records.ts +418 -0
- package/src/adapters/continue/writer.ts +162 -0
- package/src/adapters/copilot-cli/index.ts +9 -0
- package/src/adapters/copilot-cli/reader.ts +186 -0
- package/src/adapters/copilot-cli/records.ts +241 -0
- package/src/adapters/copilot-cli/writer.ts +353 -0
- package/src/adapters/crush/index.ts +8 -0
- package/src/adapters/crush/reader.ts +180 -0
- package/src/adapters/crush/records.ts +169 -0
- package/src/adapters/crush/writer.ts +333 -0
- package/src/adapters/cursor-cli/index.ts +405 -0
- package/src/adapters/cursor-ide/agentkv.ts +95 -0
- package/src/adapters/cursor-ide/index.ts +501 -0
- package/src/adapters/cursor-ide/transcripts.ts +122 -0
- package/src/adapters/cursor-ide/workspaces.ts +119 -0
- package/src/adapters/cursor-ide/writer.ts +170 -0
- package/src/adapters/devin/index.ts +8 -0
- package/src/adapters/devin/reader.ts +305 -0
- package/src/adapters/devin/records.ts +277 -0
- package/src/adapters/devin/writer.ts +523 -0
- package/src/adapters/factory-droid/index.ts +12 -0
- package/src/adapters/factory-droid/reader.ts +184 -0
- package/src/adapters/factory-droid/records.ts +79 -0
- package/src/adapters/factory-droid/writer.ts +350 -0
- package/src/adapters/gemini-cli/exchanges.ts +228 -0
- package/src/adapters/gemini-cli/index.ts +396 -0
- package/src/adapters/gemini-cli/records.ts +521 -0
- package/src/adapters/gemini-cli/writer.ts +354 -0
- package/src/adapters/goose/index.ts +8 -0
- package/src/adapters/goose/reader.ts +138 -0
- package/src/adapters/goose/records.ts +199 -0
- package/src/adapters/goose/writer.ts +340 -0
- package/src/adapters/grok-cli/image.ts +44 -0
- package/src/adapters/grok-cli/index.ts +8 -0
- package/src/adapters/grok-cli/reader.ts +302 -0
- package/src/adapters/grok-cli/records.ts +172 -0
- package/src/adapters/grok-cli/writer.ts +329 -0
- package/src/adapters/hermes/index.ts +8 -0
- package/src/adapters/hermes/reader.ts +249 -0
- package/src/adapters/hermes/records.ts +285 -0
- package/src/adapters/hermes/writer.ts +452 -0
- package/src/adapters/iflow-cli/index.ts +72 -0
- package/src/adapters/iflow-cli/records.ts +215 -0
- package/src/adapters/iflow-cli/writer.ts +204 -0
- package/src/adapters/index.ts +112 -0
- package/src/adapters/kilo-cli/index.ts +21 -0
- package/src/adapters/kilo-code/index.ts +11 -0
- package/src/adapters/kilo-code/writer.ts +92 -0
- package/src/adapters/kimi/index.ts +9 -0
- package/src/adapters/kimi/reader.ts +92 -0
- package/src/adapters/kimi/records.ts +221 -0
- package/src/adapters/kimi/writer.ts +367 -0
- package/src/adapters/kiro-cli/index.ts +8 -0
- package/src/adapters/kiro-cli/reader.ts +87 -0
- package/src/adapters/kiro-cli/records.ts +260 -0
- package/src/adapters/kiro-cli/writer.ts +305 -0
- package/src/adapters/kiro-ide/index.ts +8 -0
- package/src/adapters/kiro-ide/writer.ts +52 -0
- package/src/adapters/letta/index.ts +8 -0
- package/src/adapters/letta/reader.ts +177 -0
- package/src/adapters/letta/records.ts +119 -0
- package/src/adapters/letta/writer.ts +413 -0
- package/src/adapters/mastracode/index.ts +8 -0
- package/src/adapters/mastracode/reader.ts +323 -0
- package/src/adapters/mastracode/records.ts +336 -0
- package/src/adapters/mastracode/writer.ts +385 -0
- package/src/adapters/mistral-vibe/index.ts +9 -0
- package/src/adapters/mistral-vibe/reader.ts +62 -0
- package/src/adapters/mistral-vibe/records.ts +104 -0
- package/src/adapters/mistral-vibe/writer.ts +359 -0
- package/src/adapters/muse/index.ts +10 -0
- package/src/adapters/muse/reader.ts +332 -0
- package/src/adapters/muse/records.ts +104 -0
- package/src/adapters/muse/writer.ts +387 -0
- package/src/adapters/omp/index.ts +13 -0
- package/src/adapters/omp/reader.ts +314 -0
- package/src/adapters/omp/records.ts +153 -0
- package/src/adapters/omp/writer.ts +216 -0
- package/src/adapters/opencode/ids.ts +52 -0
- package/src/adapters/opencode/index.ts +31 -0
- package/src/adapters/opencode/note.ts +8 -0
- package/src/adapters/opencode/reader.ts +452 -0
- package/src/adapters/opencode/tools.ts +174 -0
- package/src/adapters/opencode/v2.ts +367 -0
- package/src/adapters/opencode/writer.ts +379 -0
- package/src/adapters/openhands/index.ts +8 -0
- package/src/adapters/openhands/reader.ts +222 -0
- package/src/adapters/openhands/records.ts +136 -0
- package/src/adapters/openhands/writer.ts +404 -0
- package/src/adapters/pearai/index.ts +10 -0
- package/src/adapters/pi/index.ts +28 -0
- package/src/adapters/pi/reader.ts +261 -0
- package/src/adapters/pi/records.ts +99 -0
- package/src/adapters/pi/writer.ts +362 -0
- package/src/adapters/qoder-cli/index.ts +8 -0
- package/src/adapters/qoder-cli/reader.ts +83 -0
- package/src/adapters/qoder-cli/records.ts +45 -0
- package/src/adapters/qoder-cli/writer.ts +72 -0
- package/src/adapters/qwen-code/index.ts +84 -0
- package/src/adapters/qwen-code/records.ts +194 -0
- package/src/adapters/qwen-code/writer.ts +224 -0
- package/src/adapters/tabnine-cli/index.ts +8 -0
- package/src/adapters/tabnine-cli/reader.ts +106 -0
- package/src/adapters/tabnine-cli/records.ts +33 -0
- package/src/adapters/tabnine-cli/writer.ts +156 -0
- package/src/adapters/types.ts +73 -0
- package/src/adapters/void/index.ts +9 -0
- package/src/adapters/void/reader.ts +48 -0
- package/src/adapters/void/records.ts +285 -0
- package/src/adapters/void/writer.ts +93 -0
- package/src/adapters/vscode-copilot/index.ts +9 -0
- package/src/adapters/vscode-copilot/reader.ts +212 -0
- package/src/adapters/vscode-copilot/records.ts +388 -0
- package/src/adapters/vscode-copilot/shared.ts +68 -0
- package/src/adapters/vscode-copilot/writer.ts +546 -0
- package/src/adapters/zed/index.ts +8 -0
- package/src/adapters/zed/reader.ts +198 -0
- package/src/adapters/zed/records.ts +87 -0
- package/src/adapters/zed/writer.ts +341 -0
- package/src/brief-writers.ts +71 -0
- package/src/brief.ts +238 -0
- package/src/cli.ts +351 -0
- package/src/dashboard/cli.ts +148 -0
- package/src/dashboard/conversions.ts +170 -0
- package/src/dashboard/report.ts +261 -0
- package/src/dashboard/send.ts +213 -0
- package/src/dashboard/state.ts +95 -0
- package/src/fsutil.ts +133 -0
- package/src/gateway/common.ts +232 -0
- package/src/gateway/connect.ts +847 -0
- package/src/gateway/connectors/aider.ts +356 -0
- package/src/gateway/connectors/amazon-q-cli.ts +6 -0
- package/src/gateway/connectors/amazon-q-ide.ts +6 -0
- package/src/gateway/connectors/antigravity.ts +6 -0
- package/src/gateway/connectors/auggie.ts +6 -0
- package/src/gateway/connectors/cline-sdk.ts +85 -0
- package/src/gateway/connectors/cline.ts +34 -0
- package/src/gateway/connectors/codebuff.ts +6 -0
- package/src/gateway/connectors/continue.ts +579 -0
- package/src/gateway/connectors/copilot-cli.ts +138 -0
- package/src/gateway/connectors/crush.ts +313 -0
- package/src/gateway/connectors/cursor-cli.ts +6 -0
- package/src/gateway/connectors/cursor.ts +6 -0
- package/src/gateway/connectors/devin.ts +6 -0
- package/src/gateway/connectors/factory-droid.ts +308 -0
- package/src/gateway/connectors/goose.ts +79 -0
- package/src/gateway/connectors/grok-cli.ts +82 -0
- package/src/gateway/connectors/hermes.ts +230 -0
- package/src/gateway/connectors/iflow-cli.ts +50 -0
- package/src/gateway/connectors/index.ts +86 -0
- package/src/gateway/connectors/kilo-cli.ts +434 -0
- package/src/gateway/connectors/kilo-code.ts +32 -0
- package/src/gateway/connectors/kimi-code.ts +241 -0
- package/src/gateway/connectors/kiro-cli.ts +6 -0
- package/src/gateway/connectors/kiro-ide.ts +6 -0
- package/src/gateway/connectors/letta-code.ts +170 -0
- package/src/gateway/connectors/mastracode.ts +91 -0
- package/src/gateway/connectors/mistral-vibe.ts +495 -0
- package/src/gateway/connectors/muse.ts +27 -0
- package/src/gateway/connectors/omp.ts +279 -0
- package/src/gateway/connectors/openhands.ts +296 -0
- package/src/gateway/connectors/pearai.ts +52 -0
- package/src/gateway/connectors/pi.ts +357 -0
- package/src/gateway/connectors/qoder-cli.ts +6 -0
- package/src/gateway/connectors/qwen-code.ts +411 -0
- package/src/gateway/connectors/roo.ts +43 -0
- package/src/gateway/connectors/tabnine-cli.ts +6 -0
- package/src/gateway/connectors/void.ts +6 -0
- package/src/gateway/connectors/vscode-copilot.ts +286 -0
- package/src/gateway/connectors/windsurf.ts +6 -0
- package/src/gateway/connectors/zed.ts +29 -0
- package/src/gateway/signin.ts +323 -0
- package/src/handoff.ts +104 -0
- package/src/hooks.ts +455 -0
- package/src/memory/audit.ts +514 -0
- package/src/memory/blobs.ts +94 -0
- package/src/memory/cli.ts +117 -0
- package/src/memory/distill.ts +381 -0
- package/src/memory/externalize.ts +435 -0
- package/src/memory/lookups.ts +70 -0
- package/src/memory/mcp-install.ts +401 -0
- package/src/memory/mcp-server.ts +296 -0
- package/src/memory/prepare.ts +179 -0
- package/src/memory/preview.ts +154 -0
- package/src/memory/project.ts +35 -0
- package/src/memory/redact.ts +72 -0
- package/src/memory/relevance.ts +187 -0
- package/src/memory/repomap.ts +516 -0
- package/src/memory/store.ts +270 -0
- package/src/memory/sync.ts +166 -0
- package/src/memory/tidy.ts +193 -0
- package/src/memory/types.ts +118 -0
- package/src/org/admin-ops.ts +96 -0
- package/src/org/auth.ts +112 -0
- package/src/org/backup.ts +115 -0
- package/src/org/cli.ts +438 -0
- package/src/org/client.ts +404 -0
- package/src/org/config.ts +44 -0
- package/src/org/crypto.ts +537 -0
- package/src/org/db.ts +435 -0
- package/src/org/graph.ts +318 -0
- package/src/org/oidc.ts +435 -0
- package/src/org/policy.ts +398 -0
- package/src/org/server.ts +920 -0
- package/src/org/sso.ts +316 -0
- package/src/org/tls.ts +66 -0
- package/src/org/types.ts +193 -0
- package/src/self.ts +68 -0
- package/src/session/attachments.ts +53 -0
- package/src/session/peekcache.ts +50 -0
- package/src/session/types.ts +104 -0
- package/src/session/util.ts +130 -0
- package/src/sqlite.ts +48 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { join } from "node:path"
|
|
2
|
+
import { versionOf } from "../../fsutil.ts"
|
|
3
|
+
|
|
4
|
+
// What Claude Code's transcripts look like, as far as this adapter relies on it. Checked against the
|
|
5
|
+
// transcripts on the author's Mac (versions 2.1.209 to 2.1.317, structure and counts only) and the
|
|
6
|
+
// installed binary, 2.1.316. Catalogue: docs/sessions/2026-11-08-session-formats.md, entries 1 and 2.
|
|
7
|
+
|
|
8
|
+
export const TOOL = "claude-code"
|
|
9
|
+
export const LABEL = "Claude Code"
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Versions the writer's record shape was checked against: the CLI-written transcripts on the author's Mac
|
|
13
|
+
* (2.1.296 to 2.1.316) and the version Claude Desktop bundles (2.1.317). Outside this range the writer
|
|
14
|
+
* says so and the switcher falls back to the brief.
|
|
15
|
+
*/
|
|
16
|
+
export const TESTED = { min: "2.1.296", max: "2.1.319" }
|
|
17
|
+
|
|
18
|
+
export const projectsDir = (home: string) => join(home, ".claude", "projects")
|
|
19
|
+
|
|
20
|
+
/** A main transcript is `<session uuid>.jsonl`; set-aside copies (.orphaned-, .superseded-) are not sessions. */
|
|
21
|
+
export const SESSION_FILE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.jsonl$/i
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Claude Code's project folder for a cwd, as 2.1.316 derives it: every non-alphanumeric becomes '-', and
|
|
25
|
+
* a name over 200 characters is cut to 200 and suffixed with a base-36 string hash of the full path (the
|
|
26
|
+
* `(h << 5) - h + charCode` hash Claude Code uses, so the folder matches the one it would pick itself).
|
|
27
|
+
*/
|
|
28
|
+
export function projectSlug(cwd: string): string {
|
|
29
|
+
const slug = cwd.replace(/[^a-zA-Z0-9]/g, "-")
|
|
30
|
+
if (slug.length <= 200) return slug
|
|
31
|
+
let h = 0
|
|
32
|
+
for (let i = 0; i < cwd.length; i++) h = ((h << 5) - h + cwd.charCodeAt(i)) | 0
|
|
33
|
+
return `${slug.slice(0, 200)}-${Math.abs(h).toString(36)}`
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** "2.1.316 (Claude Code)" → "2.1.316". */
|
|
37
|
+
export const semver = (v: string | undefined) => v?.match(/(\d+)\.(\d+)\.(\d+)/)?.[0]
|
|
38
|
+
|
|
39
|
+
function compare(a: string, b: string): number {
|
|
40
|
+
const x = a.split(".").map(Number)
|
|
41
|
+
const y = b.split(".").map(Number)
|
|
42
|
+
for (let i = 0; i < 3; i++) if ((x[i] ?? 0) !== (y[i] ?? 0)) return (x[i] ?? 0) - (y[i] ?? 0)
|
|
43
|
+
return 0
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function testedVersion(v: string | undefined): boolean {
|
|
47
|
+
const s = semver(v)
|
|
48
|
+
return !!s && compare(s, TESTED.min) >= 0 && compare(s, TESTED.max) <= 0
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* A release newer than the ones checked but on the same minor line (2.1.x): Claude Code ships patch releases daily
|
|
53
|
+
* and has not changed the record shapes this writer relies on between them (2.1.296 to 2.1.319). A new minor or major
|
|
54
|
+
* is still refused until it has been checked.
|
|
55
|
+
*/
|
|
56
|
+
export function newerPatch(v: string | undefined): boolean {
|
|
57
|
+
const s = semver(v)
|
|
58
|
+
if (!s) return false
|
|
59
|
+
const [maj, min] = s.split(".")
|
|
60
|
+
const [tmaj, tmin] = TESTED.max.split(".")
|
|
61
|
+
return maj === tmaj && min === tmin && compare(s, TESTED.max) > 0
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** The installed Claude Code version, or undefined when `claude` is not on the PATH. */
|
|
65
|
+
export const claudeVersion = (bin = "claude") => semver(versionOf(bin))
|
|
66
|
+
|
|
67
|
+
/** Records that make up the conversation tree (they carry uuid and parentUuid). `progress` is from older versions. */
|
|
68
|
+
export const CHAIN_TYPES = new Set(["user", "assistant", "system", "attachment", "progress"])
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Record types Claude Code writes beside the conversation: titles, request logs, modes, latches, file
|
|
72
|
+
* checkpoints, links. Skipped without counting them as lost; anything not here or in CHAIN_TYPES is
|
|
73
|
+
* counted as an unknown record type.
|
|
74
|
+
*/
|
|
75
|
+
export const HOUSEKEEPING = new Set([
|
|
76
|
+
"api-request", "api-request-blob", "api-request-shape", "last-prompt", "custom-title", "ai-title", "agent-name",
|
|
77
|
+
"summary", "atis-latch", "isolation-latch", "bridge-session", "queue-operation", "mode", "permission-mode",
|
|
78
|
+
"pr-link", "history-suppression", "file-history-snapshot", "file-history-delta", "frame-link", "cost-state",
|
|
79
|
+
"artifact-autoreact-ledger", "artifact-comment-monitor", "tools-used", "relocated",
|
|
80
|
+
])
|
|
81
|
+
|
|
82
|
+
/** Attachments that point at a file the person mentioned; the rest (reminders, environment, listings) are context, not conversation. */
|
|
83
|
+
export const FILE_ATTACHMENTS = new Set(["file", "compact_file_reference", "pdf_reference"])
|
|
84
|
+
|
|
85
|
+
/** The first message of a session the writer creates; the reader recognises it and records where the session came from. */
|
|
86
|
+
export const continuedNote = (label: string, id: string, when: string) =>
|
|
87
|
+
`[This session continues ${label} session ${id}, carried over by brain session on ${when}. The turns below are its history; reasoning and anything listed as lost did not carry over. Check files before relying on details, since they may have changed.]`
|
|
88
|
+
export const CONTINUED_NOTE = /^\[This session continues .+? session (\S+), carried over by brain session/
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
import { randomBytes, randomUUID } from "node:crypto"
|
|
2
|
+
import { mkdirSync, readFileSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs"
|
|
3
|
+
import { extname, join } from "node:path"
|
|
4
|
+
import { toolLabel } from "../../brief.ts"
|
|
5
|
+
import { exists, gitInfo, isDir } from "../../fsutil.ts"
|
|
6
|
+
import type { Session, Turn, Usage } from "../../session/types.ts"
|
|
7
|
+
import { countTurns, firstUserText, unpaired } from "../../session/util.ts"
|
|
8
|
+
import type { Readiness, WriteOptions, WriteResult, Writer } from "../types.ts"
|
|
9
|
+
import { claudeCodeReader } from "./reader.ts"
|
|
10
|
+
import { LABEL, TESTED, TOOL, claudeVersion, continuedNote, newerPatch, projectSlug, projectsDir, testedVersion } from "./records.ts"
|
|
11
|
+
|
|
12
|
+
// Continues a portable session in Claude Code as a new session: a transcript written where Claude Code
|
|
13
|
+
// keeps its own (~/.claude/projects/<slug of cwd>/<new uuid>.jsonl), which `claude --resume <uuid>` opens.
|
|
14
|
+
//
|
|
15
|
+
// The records copy the shape of transcripts Claude Code 2.1.296-2.1.317 writes from the CLI: user and
|
|
16
|
+
// assistant records on one uuid/parentUuid chain, with sessionId, cwd, gitBranch, version, entrypoint
|
|
17
|
+
// "cli" and userType "external" (so the session shows in the resume picker like any CLI session). Each
|
|
18
|
+
// assistant response is split one content block per record under a shared message.id, as Claude Code
|
|
19
|
+
// does, and each tool result is its own user record pointing back at its call (sourceToolAssistantUUID).
|
|
20
|
+
// The chain is kept linear (results follow the last call of their response), a shape Claude Code also
|
|
21
|
+
// writes and that needs no re-attaching of parallel results when it loads.
|
|
22
|
+
//
|
|
23
|
+
// A compaction is written as Claude Code writes its own: a compact_boundary record that starts a new chain
|
|
24
|
+
// (parentUuid null, logicalParentUuid pointing back) and the summary as the next record. Claude Code loads
|
|
25
|
+
// a session from its last boundary on, so a long session continues from its summary instead of sending
|
|
26
|
+
// the model every turn the summary replaced; those turns stay in the file.
|
|
27
|
+
//
|
|
28
|
+
// Reasoning is dropped: Claude's thinking replays only with its signature, which cannot be made for text
|
|
29
|
+
// from elsewhere. Tool call ids are remapped to toolu_ ids, every call gets a result (a missing one is
|
|
30
|
+
// closed as an error), and the file is read back before the session is offered.
|
|
31
|
+
|
|
32
|
+
const ALNUM = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
|
|
33
|
+
const randomId = (prefix: string, n: number) => prefix + [...randomBytes(n)].map((b) => ALNUM[b % ALNUM.length]).join("")
|
|
34
|
+
|
|
35
|
+
const shellQuote = (s: string) => (/^[\w./@%+=:,-]+$/.test(s) ? s : `'${s.replace(/'/g, `'\\''`)}'`)
|
|
36
|
+
|
|
37
|
+
/** Claude Code records the resolved folder (/private/tmp, not /tmp), and names the project folder after it. */
|
|
38
|
+
function realCwd(cwd: string): string {
|
|
39
|
+
try {
|
|
40
|
+
return realpathSync(cwd)
|
|
41
|
+
} catch {
|
|
42
|
+
return cwd
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Tool names the API accepts: letters, digits, _ and -, at most 128. */
|
|
47
|
+
const toolName = (name: string) => name.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 128) || "tool"
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* What Codex's unified exec wrote for a command, as Claude Code's Bash result reads: the output itself, and for a
|
|
51
|
+
* failure its "Exit code N" first line. Anything that is not that format is left as it is.
|
|
52
|
+
*/
|
|
53
|
+
function fromUnifiedExec(text: string): string {
|
|
54
|
+
const m = text.match(/^(?:Chunk ID: [^\n]*\n)?(?:Wall time: [^\n]*\n)?Process exited with code (-?\d+)\n(?:Original token count: [^\n]*\n)?Output:\n([\s\S]*)$/)
|
|
55
|
+
if (!m) return text
|
|
56
|
+
return m[1] === "0" ? m[2]! : `Exit code ${m[1]}\n${m[2]}`
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** tool_use input must be an object; other tools sometimes pass a raw string (apply_patch) or an array. */
|
|
60
|
+
const toolInput = (input: unknown): Record<string, unknown> =>
|
|
61
|
+
input && typeof input === "object" && !Array.isArray(input) ? (input as Record<string, unknown>) : { input: input ?? null }
|
|
62
|
+
|
|
63
|
+
const IMAGE_TYPES: Record<string, string> = { ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".gif": "image/gif", ".webp": "image/webp" }
|
|
64
|
+
const IMAGE_LIMIT = 5 * 1024 * 1024
|
|
65
|
+
|
|
66
|
+
type Block = Record<string, unknown>
|
|
67
|
+
type Call = Extract<Turn, { kind: "tool_call" }>
|
|
68
|
+
type Result = Extract<Turn, { kind: "tool_result" }>
|
|
69
|
+
|
|
70
|
+
/** The conversation as API-shaped messages, before ids, chain and timestamps are assigned. */
|
|
71
|
+
type Msg =
|
|
72
|
+
/** `meta`: written as Claude Code's isMeta record, which the model reads but which is not the person's words. */
|
|
73
|
+
| { role: "user"; at?: string; content: string | Block[]; meta?: boolean }
|
|
74
|
+
| { role: "assistant"; at?: string; model: string; usage?: Usage; blocks: Block[]; calls: Call[] }
|
|
75
|
+
| { role: "compaction"; at?: string; summary: string }
|
|
76
|
+
|
|
77
|
+
export interface TranscriptPlan {
|
|
78
|
+
/** Every record of the new transcript, in order. */
|
|
79
|
+
lines: Array<Record<string, unknown>>
|
|
80
|
+
/** What the new session cannot keep. */
|
|
81
|
+
lost: string[]
|
|
82
|
+
/** What a read-back must find. */
|
|
83
|
+
expect: { calls: number }
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** The records of a new Claude Code session `sessionId` continuing `session`. Pure apart from reading attached images. */
|
|
87
|
+
export function planTranscript(
|
|
88
|
+
session: Session,
|
|
89
|
+
opts: { sessionId: string; cwd: string; version: string; gitBranch: string; now: Date },
|
|
90
|
+
): TranscriptPlan {
|
|
91
|
+
const drops = new Map<string, number>()
|
|
92
|
+
const drop = (what: string, n = 1) => drops.set(what, (drops.get(what) ?? 0) + n)
|
|
93
|
+
const label = toolLabel(session.header.tool)
|
|
94
|
+
const results = new Map<string, Result>()
|
|
95
|
+
const callIds = new Set<string>()
|
|
96
|
+
for (const t of session.turns) {
|
|
97
|
+
if (t.kind === "tool_result" && !results.has(t.callId)) results.set(t.callId, t)
|
|
98
|
+
if (t.kind === "tool_call") callIds.add(t.id)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// The note saying where the session came from is meta: readers (and so the brief) must not take it for the first request.
|
|
102
|
+
// Shell calls from tools that run them under another name (Codex's exec_command takes `cmd`) come back as Bash.
|
|
103
|
+
const shellMapped = new Set<string>()
|
|
104
|
+
const msgs: Msg[] = [{ role: "user", content: continuedNote(label, session.header.id, opts.now.toISOString().slice(0, 10)), meta: true }]
|
|
105
|
+
let cur: Extract<Msg, { role: "assistant" }> | undefined
|
|
106
|
+
let model = session.header.models[session.header.models.length - 1] ?? "unknown"
|
|
107
|
+
const flush = () => {
|
|
108
|
+
if (cur?.blocks.length) msgs.push(cur)
|
|
109
|
+
cur = undefined
|
|
110
|
+
}
|
|
111
|
+
const assistant = (at?: string) => (cur ??= { role: "assistant", ...(at ? { at } : {}), model, blocks: [], calls: [] })
|
|
112
|
+
const seen = new Set<string>()
|
|
113
|
+
|
|
114
|
+
for (const t of session.turns) {
|
|
115
|
+
switch (t.kind) {
|
|
116
|
+
case "user": {
|
|
117
|
+
flush()
|
|
118
|
+
const blocks: Block[] = []
|
|
119
|
+
let text = t.text
|
|
120
|
+
for (const a of t.attachments ?? []) {
|
|
121
|
+
if (!a.path) continue // bytes the reader could not keep; it already counted them
|
|
122
|
+
if (a.kind === "file") {
|
|
123
|
+
text += `\n\n[Attached file: ${a.path}]`
|
|
124
|
+
continue
|
|
125
|
+
}
|
|
126
|
+
const mime = a.mime ?? IMAGE_TYPES[extname(a.path).toLowerCase()]
|
|
127
|
+
let size = Number.POSITIVE_INFINITY
|
|
128
|
+
try {
|
|
129
|
+
size = statSync(a.path).size
|
|
130
|
+
} catch {}
|
|
131
|
+
if (mime && Object.values(IMAGE_TYPES).includes(mime) && size <= IMAGE_LIMIT) {
|
|
132
|
+
blocks.push({ type: "image", source: { type: "base64", media_type: mime, data: readFileSync(a.path).toString("base64") } })
|
|
133
|
+
} else drop("images that could not be attached")
|
|
134
|
+
}
|
|
135
|
+
if (!text.trim() && !blocks.length) break
|
|
136
|
+
msgs.push({ role: "user", ...(t.at ? { at: t.at } : {}), content: blocks.length ? [{ type: "text", text: text.trim() ? text : "(image)" }, ...blocks] : text })
|
|
137
|
+
break
|
|
138
|
+
}
|
|
139
|
+
case "compaction":
|
|
140
|
+
flush()
|
|
141
|
+
msgs.push({ role: "compaction", ...(t.at ? { at: t.at } : {}), summary: `[Summary of the earlier conversation in ${label}, which replaced the turns before it]\n\n${t.summary}` })
|
|
142
|
+
break
|
|
143
|
+
case "assistant":
|
|
144
|
+
// Text after a tool call is a new response: it was written after the call's result came back.
|
|
145
|
+
if (cur?.calls.length) flush()
|
|
146
|
+
if (t.model) model = t.model
|
|
147
|
+
assistant(t.at)
|
|
148
|
+
if (t.model && !cur!.blocks.length) cur!.model = t.model
|
|
149
|
+
if (t.text.trim()) cur!.blocks.push({ type: "text", text: t.text })
|
|
150
|
+
if (t.usage && !cur!.usage) cur!.usage = t.usage
|
|
151
|
+
break
|
|
152
|
+
case "tool_call":
|
|
153
|
+
if (seen.has(t.id)) break
|
|
154
|
+
seen.add(t.id)
|
|
155
|
+
{
|
|
156
|
+
const raw = t.input && typeof t.input === "object" && !Array.isArray(t.input) ? (t.input as Record<string, unknown>) : undefined
|
|
157
|
+
const asBash = t.toolKind === "shell" && t.name !== "Bash" && typeof raw?.cmd === "string"
|
|
158
|
+
if (asBash) shellMapped.add(t.id)
|
|
159
|
+
assistant(t.at).blocks.push({
|
|
160
|
+
type: "tool_use",
|
|
161
|
+
id: "",
|
|
162
|
+
name: asBash ? "Bash" : toolName(t.name),
|
|
163
|
+
input: asBash ? { command: raw!.cmd } : toolInput(t.input),
|
|
164
|
+
caller: { type: "direct" },
|
|
165
|
+
})
|
|
166
|
+
}
|
|
167
|
+
cur!.calls.push(t)
|
|
168
|
+
break
|
|
169
|
+
case "tool_result":
|
|
170
|
+
// Written right after its call's response; a result whose call never came is dropped. A result
|
|
171
|
+
// closes the response it answers, so a call made after it is a new response, not a parallel one.
|
|
172
|
+
if (!callIds.has(t.callId)) drop("tool results without a call")
|
|
173
|
+
else if (cur?.calls.some((c) => c.id === t.callId)) flush()
|
|
174
|
+
break
|
|
175
|
+
case "reasoning":
|
|
176
|
+
drop("reasoning (Claude Code replays thinking only with its signature)")
|
|
177
|
+
break
|
|
178
|
+
case "file_edit":
|
|
179
|
+
if (!t.callId || !callIds.has(t.callId)) drop("file edits outside a tool call")
|
|
180
|
+
break
|
|
181
|
+
case "subagent":
|
|
182
|
+
drop("subagent transcripts (their results stay in the parent's tool results)")
|
|
183
|
+
break
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
flush()
|
|
187
|
+
|
|
188
|
+
// Records, on one chain, with timestamps that never go backwards.
|
|
189
|
+
const lines: Array<Record<string, unknown>> = []
|
|
190
|
+
const known = session.turns.map((t) => (t.at ? Date.parse(t.at) : Number.NaN)).find(Number.isFinite)
|
|
191
|
+
let clock = (known ?? opts.now.getTime()) - 1
|
|
192
|
+
const stamp = (at?: string) => {
|
|
193
|
+
const t = at ? Date.parse(at) : Number.NaN
|
|
194
|
+
clock = Number.isFinite(t) && t > clock ? t : clock + 1
|
|
195
|
+
return new Date(clock).toISOString()
|
|
196
|
+
}
|
|
197
|
+
let parent: string | null = null
|
|
198
|
+
let promptId = randomUUID()
|
|
199
|
+
// Characters of conversation since the last compaction, for the boundary's token estimate.
|
|
200
|
+
let chars = 0
|
|
201
|
+
const common = (at?: string) => ({ timestamp: stamp(at), userType: "external", entrypoint: "cli", cwd: opts.cwd, sessionId: opts.sessionId, version: opts.version, gitBranch: opts.gitBranch })
|
|
202
|
+
const push = (fields: Record<string, unknown>, at?: string) => {
|
|
203
|
+
const uuid = randomUUID()
|
|
204
|
+
lines.push({ parentUuid: parent, isSidechain: false, ...fields, uuid, ...common(at) })
|
|
205
|
+
parent = uuid
|
|
206
|
+
const message = fields.message as { content?: unknown } | undefined
|
|
207
|
+
if (message) chars += JSON.stringify(message.content ?? "").length
|
|
208
|
+
return uuid
|
|
209
|
+
}
|
|
210
|
+
let calls = 0
|
|
211
|
+
let lastPrompt = ""
|
|
212
|
+
for (const m of msgs) {
|
|
213
|
+
if (m.role === "compaction") {
|
|
214
|
+
const before = parent
|
|
215
|
+
const preTokens = Math.round(chars / 4)
|
|
216
|
+
parent = null
|
|
217
|
+
push({ logicalParentUuid: before, type: "system", subtype: "compact_boundary", content: "Conversation compacted", isMeta: false, level: "info", compactMetadata: { trigger: "auto", preTokens, postTokens: Math.round(m.summary.length / 4) } }, m.at)
|
|
218
|
+
chars = 0
|
|
219
|
+
promptId = randomUUID()
|
|
220
|
+
push({ promptId, type: "user", message: { role: "user", content: m.summary }, isVisibleInTranscriptOnly: true, isCompactSummary: true }, m.at)
|
|
221
|
+
continue
|
|
222
|
+
}
|
|
223
|
+
if (m.role === "user") {
|
|
224
|
+
if (m.meta) {
|
|
225
|
+
push({ type: "user", message: { role: "user", content: m.content }, isMeta: true }, m.at)
|
|
226
|
+
continue
|
|
227
|
+
}
|
|
228
|
+
promptId = randomUUID()
|
|
229
|
+
if (typeof m.content === "string") lastPrompt = m.content
|
|
230
|
+
push({ promptId, type: "user", message: { role: "user", content: m.content } }, m.at)
|
|
231
|
+
continue
|
|
232
|
+
}
|
|
233
|
+
const id = randomId("msg_01", 22)
|
|
234
|
+
const requestId = randomId("req_011C", 20)
|
|
235
|
+
const usage = { input_tokens: m.usage?.input ?? 0, cache_creation_input_tokens: m.usage?.cacheWrite ?? 0, cache_read_input_tokens: m.usage?.cacheRead ?? 0, output_tokens: m.usage?.output ?? 0, service_tier: "standard" }
|
|
236
|
+
const stop = m.calls.length ? "tool_use" : "end_turn"
|
|
237
|
+
const byCall = new Map<Call, { toolu: string; record: string }>()
|
|
238
|
+
let k = 0
|
|
239
|
+
m.blocks.forEach((b, i) => {
|
|
240
|
+
if (b.type === "tool_use") {
|
|
241
|
+
const call = m.calls[k++]!
|
|
242
|
+
b.id = randomId("toolu_01", 22)
|
|
243
|
+
const record = push({ message: { model: m.model, id, type: "message", role: "assistant", content: [b], stop_reason: stop, stop_sequence: null, usage }, requestId, type: "assistant", apiBlockIndex: i }, m.at)
|
|
244
|
+
byCall.set(call, { toolu: b.id as string, record })
|
|
245
|
+
calls++
|
|
246
|
+
} else push({ message: { model: m.model, id, type: "message", role: "assistant", content: [b], stop_reason: stop, stop_sequence: null, usage }, requestId, type: "assistant", apiBlockIndex: i }, m.at)
|
|
247
|
+
})
|
|
248
|
+
for (const call of m.calls) {
|
|
249
|
+
const { toolu, record } = byCall.get(call)!
|
|
250
|
+
const r = results.get(call.id)
|
|
251
|
+
if (!r) drop("tool calls without a result (closed as errors)")
|
|
252
|
+
const ok = r?.ok ?? false
|
|
253
|
+
let output = r?.output ?? "No result was recorded for this call; the session moved before it finished."
|
|
254
|
+
if (r && shellMapped.has(call.id)) output = fromUnifiedExec(output)
|
|
255
|
+
if (r?.fullPath) output += `\n\n[Full output: ${r.fullPath}]`
|
|
256
|
+
if (!output.trim()) output = "(no output)"
|
|
257
|
+
// toolUseResult is what Claude Code's UI renders from; it checks the shape first, so a plain string is
|
|
258
|
+
// safe for any tool. Bash's own shape is simple enough to rebuild.
|
|
259
|
+
const toolUseResult = !ok ? `Error: ${output}` : call.name === "Bash" || shellMapped.has(call.id) ? { stdout: output, stderr: "", interrupted: false, isImage: false, noOutputExpected: false } : output
|
|
260
|
+
push(
|
|
261
|
+
{
|
|
262
|
+
promptId,
|
|
263
|
+
type: "user",
|
|
264
|
+
message: { role: "user", content: [{ tool_use_id: toolu, type: "tool_result", content: output, ...(ok ? {} : { is_error: true }) }] },
|
|
265
|
+
toolUseResult,
|
|
266
|
+
sourceToolAssistantUUID: record,
|
|
267
|
+
},
|
|
268
|
+
r?.at ?? m.at,
|
|
269
|
+
)
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const title = session.header.title ?? firstUserText(session)?.replace(/\s+/g, " ").trim().slice(0, 60) ?? "Session"
|
|
274
|
+
lines.push({ type: "custom-title", customTitle: `${title} (from ${label})`, sessionId: opts.sessionId })
|
|
275
|
+
lines.push({ type: "last-prompt", lastPrompt: lastPrompt.slice(0, 500), leafUuid: parent, sessionId: opts.sessionId })
|
|
276
|
+
return { lines, lost: [...drops].map(([what, n]) => `${what} (${n})`), expect: { calls } }
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** The Claude Code writer; `version` and `bin` are for tests, which must not depend on the installed binary. */
|
|
280
|
+
export function claudeCodeWriter(opts: { bin?: string; version?: string } = {}): Writer {
|
|
281
|
+
const installed = () => opts.version ?? claudeVersion(opts.bin)
|
|
282
|
+
return {
|
|
283
|
+
tool: TOOL,
|
|
284
|
+
label: LABEL,
|
|
285
|
+
way: "native",
|
|
286
|
+
|
|
287
|
+
async ready(w: WriteOptions): Promise<Readiness> {
|
|
288
|
+
const v = installed()
|
|
289
|
+
if (!v) return { ok: false, why: "Claude Code is not installed (`claude --version` did not run)" }
|
|
290
|
+
if (!testedVersion(v) && !newerPatch(v)) return { ok: false, why: `Claude Code ${v} is not a version this writer was checked against (${TESTED.min} to ${TESTED.max})` }
|
|
291
|
+
if (!isDir(w.cwd)) return { ok: false, why: `${w.cwd} is not a folder; Claude Code resumes a session in the folder it belongs to` }
|
|
292
|
+
return { ok: true }
|
|
293
|
+
},
|
|
294
|
+
|
|
295
|
+
async write(session: Session, w: WriteOptions): Promise<WriteResult> {
|
|
296
|
+
const cwd = realCwd(w.cwd)
|
|
297
|
+
const git = gitInfo(cwd)
|
|
298
|
+
const version = installed() ?? TESTED.max
|
|
299
|
+
let sessionId = randomUUID()
|
|
300
|
+
const dir = join(projectsDir(w.home), projectSlug(cwd))
|
|
301
|
+
while (exists(join(dir, `${sessionId}.jsonl`))) sessionId = randomUUID()
|
|
302
|
+
const path = join(dir, `${sessionId}.jsonl`)
|
|
303
|
+
const plan = planTranscript(session, { sessionId, cwd, version, gitBranch: git.gitBranch ?? (git.gitCommit ? "HEAD" : ""), now: new Date() })
|
|
304
|
+
if (!w.dryRun) {
|
|
305
|
+
mkdirSync(dir, { recursive: true })
|
|
306
|
+
// "wx": a new file only; an existing session is never touched.
|
|
307
|
+
writeFileSync(path, `${plan.lines.map((l) => JSON.stringify(l)).join("\n")}\n`, { flag: "wx" })
|
|
308
|
+
const back = await claudeCodeReader.read({ tool: TOOL, sourceId: sessionId, path }, w.home)
|
|
309
|
+
const pairs = unpaired(back)
|
|
310
|
+
const got = countTurns(back).tool_call
|
|
311
|
+
if (got !== plan.expect.calls || pairs.calls.length || pairs.results.length) {
|
|
312
|
+
rmSync(path, { force: true })
|
|
313
|
+
throw new Error(`the new Claude Code transcript did not read back intact (${got} of ${plan.expect.calls} tool calls, ${pairs.calls.length + pairs.results.length} unpaired); nothing was kept`)
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return {
|
|
317
|
+
tool: TOOL,
|
|
318
|
+
way: "native",
|
|
319
|
+
sessionId,
|
|
320
|
+
written: [path],
|
|
321
|
+
open: `cd ${shellQuote(cwd)} && claude --resume ${sessionId}`,
|
|
322
|
+
lost: newerPatch(version) ? [...plan.lost, `Claude Code ${version} is newer than the versions this writer was checked against (${TESTED.min} to ${TESTED.max}); the record shapes it relies on have not changed between patch releases`] : plan.lost,
|
|
323
|
+
dryRun: !!w.dryRun,
|
|
324
|
+
}
|
|
325
|
+
},
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
export const claudeCodeNativeWriter = claudeCodeWriter()
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Reader, Writer } from "../types.ts"
|
|
2
|
+
import { taskReader } from "./reader.ts"
|
|
3
|
+
import { taskWriters } from "./writer.ts"
|
|
4
|
+
|
|
5
|
+
// Cline's classic task folders, and Roo Code's, which share the layout (catalogue entries 37 and 39). Both
|
|
6
|
+
// are read from every VS Code-family editor, and continued as a new native task in the editor named by the
|
|
7
|
+
// writer: `cline` and `roo` write to VS Code, `cline-cursor`, `roo-cursor` (and -windsurf) to the others.
|
|
8
|
+
export const readers: Reader[] = [
|
|
9
|
+
taskReader({ tool: "cline", label: "Cline", extension: "saoudrizwan.claude-dev" }),
|
|
10
|
+
taskReader({ tool: "roo", label: "Roo Code", extension: "rooveterinaryinc.roo-cline" }),
|
|
11
|
+
]
|
|
12
|
+
export const writers: Writer[] = taskWriters
|