@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.
Files changed (260) hide show
  1. package/LICENSE +48 -0
  2. package/README.md +235 -2
  3. package/package.json +48 -4
  4. package/src/adapters/aider/index.ts +8 -0
  5. package/src/adapters/aider/reader.ts +129 -0
  6. package/src/adapters/aider/records.ts +220 -0
  7. package/src/adapters/aider/writer.ts +269 -0
  8. package/src/adapters/amazon-q-cli/index.ts +8 -0
  9. package/src/adapters/amazon-q-cli/reader.ts +84 -0
  10. package/src/adapters/amazon-q-cli/records.ts +191 -0
  11. package/src/adapters/amazon-q-cli/writer.ts +370 -0
  12. package/src/adapters/amazon-q-ide/index.ts +9 -0
  13. package/src/adapters/amazon-q-ide/reader.ts +131 -0
  14. package/src/adapters/amazon-q-ide/records.ts +141 -0
  15. package/src/adapters/amazon-q-ide/writer.ts +325 -0
  16. package/src/adapters/antigravity/index.ts +9 -0
  17. package/src/adapters/antigravity/proto.ts +194 -0
  18. package/src/adapters/antigravity/reader.ts +424 -0
  19. package/src/adapters/antigravity/records.ts +236 -0
  20. package/src/adapters/antigravity/writer.ts +463 -0
  21. package/src/adapters/auggie/index.ts +36 -0
  22. package/src/adapters/claude-code/index.ts +8 -0
  23. package/src/adapters/claude-code/reader.ts +639 -0
  24. package/src/adapters/claude-code/records.ts +88 -0
  25. package/src/adapters/claude-code/writer.ts +329 -0
  26. package/src/adapters/cline/index.ts +12 -0
  27. package/src/adapters/cline/reader.ts +432 -0
  28. package/src/adapters/cline/records.ts +151 -0
  29. package/src/adapters/cline/vscdb.ts +56 -0
  30. package/src/adapters/cline/writer.ts +656 -0
  31. package/src/adapters/cline/xml.ts +177 -0
  32. package/src/adapters/cline4/index.ts +9 -0
  33. package/src/adapters/cline4/reader.ts +179 -0
  34. package/src/adapters/cline4/records.ts +92 -0
  35. package/src/adapters/cline4/writer.ts +361 -0
  36. package/src/adapters/codebuff/index.ts +8 -0
  37. package/src/adapters/codebuff/project.ts +207 -0
  38. package/src/adapters/codebuff/reader.ts +215 -0
  39. package/src/adapters/codebuff/records.ts +173 -0
  40. package/src/adapters/codebuff/writer.ts +383 -0
  41. package/src/adapters/codex/index.ts +8 -0
  42. package/src/adapters/codex/native.ts +239 -0
  43. package/src/adapters/codex/reader.ts +747 -0
  44. package/src/adapters/codex/records.ts +114 -0
  45. package/src/adapters/codex/writer.ts +479 -0
  46. package/src/adapters/continue/index.ts +9 -0
  47. package/src/adapters/continue/reader.ts +68 -0
  48. package/src/adapters/continue/records.ts +418 -0
  49. package/src/adapters/continue/writer.ts +162 -0
  50. package/src/adapters/copilot-cli/index.ts +9 -0
  51. package/src/adapters/copilot-cli/reader.ts +186 -0
  52. package/src/adapters/copilot-cli/records.ts +241 -0
  53. package/src/adapters/copilot-cli/writer.ts +353 -0
  54. package/src/adapters/crush/index.ts +8 -0
  55. package/src/adapters/crush/reader.ts +180 -0
  56. package/src/adapters/crush/records.ts +169 -0
  57. package/src/adapters/crush/writer.ts +333 -0
  58. package/src/adapters/cursor-cli/index.ts +405 -0
  59. package/src/adapters/cursor-ide/agentkv.ts +95 -0
  60. package/src/adapters/cursor-ide/index.ts +501 -0
  61. package/src/adapters/cursor-ide/transcripts.ts +122 -0
  62. package/src/adapters/cursor-ide/workspaces.ts +119 -0
  63. package/src/adapters/cursor-ide/writer.ts +170 -0
  64. package/src/adapters/devin/index.ts +8 -0
  65. package/src/adapters/devin/reader.ts +305 -0
  66. package/src/adapters/devin/records.ts +277 -0
  67. package/src/adapters/devin/writer.ts +523 -0
  68. package/src/adapters/factory-droid/index.ts +12 -0
  69. package/src/adapters/factory-droid/reader.ts +184 -0
  70. package/src/adapters/factory-droid/records.ts +79 -0
  71. package/src/adapters/factory-droid/writer.ts +350 -0
  72. package/src/adapters/gemini-cli/exchanges.ts +228 -0
  73. package/src/adapters/gemini-cli/index.ts +396 -0
  74. package/src/adapters/gemini-cli/records.ts +521 -0
  75. package/src/adapters/gemini-cli/writer.ts +354 -0
  76. package/src/adapters/goose/index.ts +8 -0
  77. package/src/adapters/goose/reader.ts +138 -0
  78. package/src/adapters/goose/records.ts +199 -0
  79. package/src/adapters/goose/writer.ts +340 -0
  80. package/src/adapters/grok-cli/image.ts +44 -0
  81. package/src/adapters/grok-cli/index.ts +8 -0
  82. package/src/adapters/grok-cli/reader.ts +302 -0
  83. package/src/adapters/grok-cli/records.ts +172 -0
  84. package/src/adapters/grok-cli/writer.ts +329 -0
  85. package/src/adapters/hermes/index.ts +8 -0
  86. package/src/adapters/hermes/reader.ts +249 -0
  87. package/src/adapters/hermes/records.ts +285 -0
  88. package/src/adapters/hermes/writer.ts +452 -0
  89. package/src/adapters/iflow-cli/index.ts +72 -0
  90. package/src/adapters/iflow-cli/records.ts +215 -0
  91. package/src/adapters/iflow-cli/writer.ts +204 -0
  92. package/src/adapters/index.ts +112 -0
  93. package/src/adapters/kilo-cli/index.ts +21 -0
  94. package/src/adapters/kilo-code/index.ts +11 -0
  95. package/src/adapters/kilo-code/writer.ts +92 -0
  96. package/src/adapters/kimi/index.ts +9 -0
  97. package/src/adapters/kimi/reader.ts +92 -0
  98. package/src/adapters/kimi/records.ts +221 -0
  99. package/src/adapters/kimi/writer.ts +367 -0
  100. package/src/adapters/kiro-cli/index.ts +8 -0
  101. package/src/adapters/kiro-cli/reader.ts +87 -0
  102. package/src/adapters/kiro-cli/records.ts +260 -0
  103. package/src/adapters/kiro-cli/writer.ts +305 -0
  104. package/src/adapters/kiro-ide/index.ts +8 -0
  105. package/src/adapters/kiro-ide/writer.ts +52 -0
  106. package/src/adapters/letta/index.ts +8 -0
  107. package/src/adapters/letta/reader.ts +177 -0
  108. package/src/adapters/letta/records.ts +119 -0
  109. package/src/adapters/letta/writer.ts +413 -0
  110. package/src/adapters/mastracode/index.ts +8 -0
  111. package/src/adapters/mastracode/reader.ts +323 -0
  112. package/src/adapters/mastracode/records.ts +336 -0
  113. package/src/adapters/mastracode/writer.ts +385 -0
  114. package/src/adapters/mistral-vibe/index.ts +9 -0
  115. package/src/adapters/mistral-vibe/reader.ts +62 -0
  116. package/src/adapters/mistral-vibe/records.ts +104 -0
  117. package/src/adapters/mistral-vibe/writer.ts +359 -0
  118. package/src/adapters/muse/index.ts +10 -0
  119. package/src/adapters/muse/reader.ts +332 -0
  120. package/src/adapters/muse/records.ts +104 -0
  121. package/src/adapters/muse/writer.ts +387 -0
  122. package/src/adapters/omp/index.ts +13 -0
  123. package/src/adapters/omp/reader.ts +314 -0
  124. package/src/adapters/omp/records.ts +153 -0
  125. package/src/adapters/omp/writer.ts +216 -0
  126. package/src/adapters/opencode/ids.ts +52 -0
  127. package/src/adapters/opencode/index.ts +31 -0
  128. package/src/adapters/opencode/note.ts +8 -0
  129. package/src/adapters/opencode/reader.ts +452 -0
  130. package/src/adapters/opencode/tools.ts +174 -0
  131. package/src/adapters/opencode/v2.ts +367 -0
  132. package/src/adapters/opencode/writer.ts +379 -0
  133. package/src/adapters/openhands/index.ts +8 -0
  134. package/src/adapters/openhands/reader.ts +222 -0
  135. package/src/adapters/openhands/records.ts +136 -0
  136. package/src/adapters/openhands/writer.ts +404 -0
  137. package/src/adapters/pearai/index.ts +10 -0
  138. package/src/adapters/pi/index.ts +28 -0
  139. package/src/adapters/pi/reader.ts +261 -0
  140. package/src/adapters/pi/records.ts +99 -0
  141. package/src/adapters/pi/writer.ts +362 -0
  142. package/src/adapters/qoder-cli/index.ts +8 -0
  143. package/src/adapters/qoder-cli/reader.ts +83 -0
  144. package/src/adapters/qoder-cli/records.ts +45 -0
  145. package/src/adapters/qoder-cli/writer.ts +72 -0
  146. package/src/adapters/qwen-code/index.ts +84 -0
  147. package/src/adapters/qwen-code/records.ts +194 -0
  148. package/src/adapters/qwen-code/writer.ts +224 -0
  149. package/src/adapters/tabnine-cli/index.ts +8 -0
  150. package/src/adapters/tabnine-cli/reader.ts +106 -0
  151. package/src/adapters/tabnine-cli/records.ts +33 -0
  152. package/src/adapters/tabnine-cli/writer.ts +156 -0
  153. package/src/adapters/types.ts +73 -0
  154. package/src/adapters/void/index.ts +9 -0
  155. package/src/adapters/void/reader.ts +48 -0
  156. package/src/adapters/void/records.ts +285 -0
  157. package/src/adapters/void/writer.ts +93 -0
  158. package/src/adapters/vscode-copilot/index.ts +9 -0
  159. package/src/adapters/vscode-copilot/reader.ts +212 -0
  160. package/src/adapters/vscode-copilot/records.ts +388 -0
  161. package/src/adapters/vscode-copilot/shared.ts +68 -0
  162. package/src/adapters/vscode-copilot/writer.ts +546 -0
  163. package/src/adapters/zed/index.ts +8 -0
  164. package/src/adapters/zed/reader.ts +198 -0
  165. package/src/adapters/zed/records.ts +87 -0
  166. package/src/adapters/zed/writer.ts +341 -0
  167. package/src/brief-writers.ts +71 -0
  168. package/src/brief.ts +238 -0
  169. package/src/cli.ts +351 -0
  170. package/src/dashboard/cli.ts +148 -0
  171. package/src/dashboard/conversions.ts +170 -0
  172. package/src/dashboard/report.ts +261 -0
  173. package/src/dashboard/send.ts +213 -0
  174. package/src/dashboard/state.ts +95 -0
  175. package/src/fsutil.ts +133 -0
  176. package/src/gateway/common.ts +232 -0
  177. package/src/gateway/connect.ts +847 -0
  178. package/src/gateway/connectors/aider.ts +356 -0
  179. package/src/gateway/connectors/amazon-q-cli.ts +6 -0
  180. package/src/gateway/connectors/amazon-q-ide.ts +6 -0
  181. package/src/gateway/connectors/antigravity.ts +6 -0
  182. package/src/gateway/connectors/auggie.ts +6 -0
  183. package/src/gateway/connectors/cline-sdk.ts +85 -0
  184. package/src/gateway/connectors/cline.ts +34 -0
  185. package/src/gateway/connectors/codebuff.ts +6 -0
  186. package/src/gateway/connectors/continue.ts +579 -0
  187. package/src/gateway/connectors/copilot-cli.ts +138 -0
  188. package/src/gateway/connectors/crush.ts +313 -0
  189. package/src/gateway/connectors/cursor-cli.ts +6 -0
  190. package/src/gateway/connectors/cursor.ts +6 -0
  191. package/src/gateway/connectors/devin.ts +6 -0
  192. package/src/gateway/connectors/factory-droid.ts +308 -0
  193. package/src/gateway/connectors/goose.ts +79 -0
  194. package/src/gateway/connectors/grok-cli.ts +82 -0
  195. package/src/gateway/connectors/hermes.ts +230 -0
  196. package/src/gateway/connectors/iflow-cli.ts +50 -0
  197. package/src/gateway/connectors/index.ts +86 -0
  198. package/src/gateway/connectors/kilo-cli.ts +434 -0
  199. package/src/gateway/connectors/kilo-code.ts +32 -0
  200. package/src/gateway/connectors/kimi-code.ts +241 -0
  201. package/src/gateway/connectors/kiro-cli.ts +6 -0
  202. package/src/gateway/connectors/kiro-ide.ts +6 -0
  203. package/src/gateway/connectors/letta-code.ts +170 -0
  204. package/src/gateway/connectors/mastracode.ts +91 -0
  205. package/src/gateway/connectors/mistral-vibe.ts +495 -0
  206. package/src/gateway/connectors/muse.ts +27 -0
  207. package/src/gateway/connectors/omp.ts +279 -0
  208. package/src/gateway/connectors/openhands.ts +296 -0
  209. package/src/gateway/connectors/pearai.ts +52 -0
  210. package/src/gateway/connectors/pi.ts +357 -0
  211. package/src/gateway/connectors/qoder-cli.ts +6 -0
  212. package/src/gateway/connectors/qwen-code.ts +411 -0
  213. package/src/gateway/connectors/roo.ts +43 -0
  214. package/src/gateway/connectors/tabnine-cli.ts +6 -0
  215. package/src/gateway/connectors/void.ts +6 -0
  216. package/src/gateway/connectors/vscode-copilot.ts +286 -0
  217. package/src/gateway/connectors/windsurf.ts +6 -0
  218. package/src/gateway/connectors/zed.ts +29 -0
  219. package/src/gateway/signin.ts +323 -0
  220. package/src/handoff.ts +104 -0
  221. package/src/hooks.ts +455 -0
  222. package/src/memory/audit.ts +514 -0
  223. package/src/memory/blobs.ts +94 -0
  224. package/src/memory/cli.ts +117 -0
  225. package/src/memory/distill.ts +381 -0
  226. package/src/memory/externalize.ts +435 -0
  227. package/src/memory/lookups.ts +70 -0
  228. package/src/memory/mcp-install.ts +401 -0
  229. package/src/memory/mcp-server.ts +296 -0
  230. package/src/memory/prepare.ts +179 -0
  231. package/src/memory/preview.ts +154 -0
  232. package/src/memory/project.ts +35 -0
  233. package/src/memory/redact.ts +72 -0
  234. package/src/memory/relevance.ts +187 -0
  235. package/src/memory/repomap.ts +516 -0
  236. package/src/memory/store.ts +270 -0
  237. package/src/memory/sync.ts +166 -0
  238. package/src/memory/tidy.ts +193 -0
  239. package/src/memory/types.ts +118 -0
  240. package/src/org/admin-ops.ts +96 -0
  241. package/src/org/auth.ts +112 -0
  242. package/src/org/backup.ts +115 -0
  243. package/src/org/cli.ts +438 -0
  244. package/src/org/client.ts +404 -0
  245. package/src/org/config.ts +44 -0
  246. package/src/org/crypto.ts +537 -0
  247. package/src/org/db.ts +435 -0
  248. package/src/org/graph.ts +318 -0
  249. package/src/org/oidc.ts +435 -0
  250. package/src/org/policy.ts +398 -0
  251. package/src/org/server.ts +920 -0
  252. package/src/org/sso.ts +316 -0
  253. package/src/org/tls.ts +66 -0
  254. package/src/org/types.ts +193 -0
  255. package/src/self.ts +68 -0
  256. package/src/session/attachments.ts +53 -0
  257. package/src/session/peekcache.ts +50 -0
  258. package/src/session/types.ts +104 -0
  259. package/src/session/util.ts +130 -0
  260. 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