@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,367 @@
1
+ import type { Database } from "bun:sqlite"
2
+ import { mkdirSync, writeFileSync } from "node:fs"
3
+ import { dirname, join } from "node:path"
4
+ import { toolLabel } from "../../brief.ts"
5
+ import { gitInfo, isDir, versionOf } from "../../fsutil.ts"
6
+ import type { Session, Turn } from "../../session/types.ts"
7
+ import { asText, clip, header, iso, lossCounter, noteModel, toolKindOf } from "../../session/util.ts"
8
+ import type { Readiness, SessionRef, WriteResult, Writer } from "../types.ts"
9
+ import { idMinter, mintId, random62 } from "./ids.ts"
10
+ import { CONTINUED_NOTE, continuedNote } from "./note.ts"
11
+ import { OPENCODE_V2, mapCall } from "./tools.ts"
12
+ import { addUsage, continuedTitle, modelRef, resolveBin, sh, versionNumber, zeroTokens } from "./writer.ts"
13
+
14
+ // OpenCode 2.x (npm @opencode/cli, anomalyco/opencode packages/core). Its SQLite store keeps a session in
15
+ // session_v2 and its messages in session_message, one row per message with the message's JSON (less id and
16
+ // type) in `data`; an assistant step's content (text, reasoning and tool items, each tool holding its own
17
+ // result) is inline in `data` or, since 2026-10-29, in session_part rows by ordinal. The store is event
18
+ // sourced and owned by a background service, so a continued session goes through the service's own
19
+ // importer (`opencode session import`, POST /api/experimental/session/import), which publishes the
20
+ // session.created event and writes the rows itself (core/src/session/transfer.ts).
21
+ //
22
+ // Verified live (2026-11-08) with @opencode/cli 2.2.6 in a temp home with no network: `session import
23
+ // --standalone`, then `session list` and `session export`. Message shapes from packages/schema/src/
24
+ // session-message.ts at v2.2.6; how they reach the model from core/src/session/runner/to-llm-message.ts.
25
+
26
+ export const TESTED_V2 = ["2.2.6"]
27
+
28
+ /** A session as `session export` writes it and `session import` takes it. */
29
+ export interface Transfer {
30
+ info: Record<string, any>
31
+ messages: Array<Record<string, any>>
32
+ }
33
+
34
+ /**
35
+ * The portable session as a 2.x transfer: the continuation note as a synthetic message (sent to the model,
36
+ * shown only as its short description), one user message per user turn, one assistant message per model step
37
+ * with its tool items holding their results, and a compaction as a completed summary compaction, from which
38
+ * the loader starts the model's history.
39
+ */
40
+ export function buildTransferV2(s: Session, o: { cwd: string; now?: number }): { data: Transfer; sessionId: string; lost: string[] } {
41
+ const now = o.now ?? Date.now()
42
+ const next = idMinter(now)
43
+ const sessionId = mintId("ses", now, 1, true)
44
+ const messages: Transfer["messages"] = []
45
+ const lost = new Map<string, number>()
46
+ const drop = (what: string, n = 1) => lost.set(what, (lost.get(what) ?? 0) + n)
47
+ const mapped = { n: 0 }
48
+ const label = toolLabel(s.header.tool)
49
+
50
+ let clock = Date.parse(s.header.startedAt ?? "") || now - s.turns.length * 1000
51
+ const tick = (at?: string) => {
52
+ const t = at ? Date.parse(at) : NaN
53
+ clock = Number.isFinite(t) && t > clock ? t : clock + 1
54
+ return clock
55
+ }
56
+ const firstClock = clock
57
+
58
+ let model = modelRef(s.header.models.at(-1))
59
+ let step: { msg: Record<string, any>; tools: boolean; results: boolean; tokens: ReturnType<typeof zeroTokens> } | undefined
60
+ const calls = new Map<string, Record<string, any>>()
61
+ const answered = new Set<string>()
62
+
63
+ const push = (msg: Record<string, any>) => {
64
+ closeStep()
65
+ messages.push(msg)
66
+ }
67
+ const openStep = (at?: string) => {
68
+ if (step && !step.results) return step
69
+ closeStep()
70
+ const created = tick(at)
71
+ step = {
72
+ msg: { id: next("msg"), type: "assistant", agent: "build", model: { id: model.modelID, providerID: model.providerID }, content: [], time: { created, completed: created } },
73
+ tools: false,
74
+ results: false,
75
+ tokens: zeroTokens(),
76
+ }
77
+ messages.push(step.msg)
78
+ return step
79
+ }
80
+ function closeStep() {
81
+ if (!step) return
82
+ step.msg.finish = step.tools ? "tool-calls" : "stop"
83
+ step.msg.time.completed = clock
84
+ if (Object.values(step.tokens).some((v) => typeof v === "number" && v > 0) || step.tokens.cache.read || step.tokens.cache.write) step.msg.tokens = step.tokens
85
+ step = undefined
86
+ }
87
+
88
+ push({
89
+ id: next("msg"),
90
+ type: "synthetic",
91
+ text: continuedNote(label, s.header.id, new Date(now).toISOString().slice(0, 10)),
92
+ description: `Continued from ${label}`,
93
+ time: { created: tick() },
94
+ })
95
+
96
+ for (const t of s.turns as Turn[]) {
97
+ switch (t.kind) {
98
+ case "user": {
99
+ const n = t.attachments?.length ?? 0
100
+ if (n) drop("attachments", n)
101
+ const text = t.text.trim() ? t.text : n ? `(${n} attachment${n > 1 ? "s" : ""} not carried over)` : ""
102
+ if (text) push({ id: next("msg"), type: "user", text, time: { created: tick(t.at) } })
103
+ break
104
+ }
105
+ case "assistant": {
106
+ if (!t.text.trim()) break
107
+ if (t.model) model = modelRef(t.model)
108
+ const st = openStep(t.at)
109
+ tick(t.at)
110
+ st.msg.content.push({ type: "text", text: t.text })
111
+ addUsage(st.tokens, t.usage)
112
+ break
113
+ }
114
+ case "reasoning": {
115
+ const st = openStep(t.at)
116
+ const at = tick(t.at)
117
+ st.msg.content.push({ type: "reasoning", text: t.text, time: { created: at, completed: at } })
118
+ // Without native data, 2.x hands readable reasoning to the next model's adapter as text.
119
+ drop("reasoning: kept as text, without provider signatures")
120
+ break
121
+ }
122
+ case "tool_call": {
123
+ const st = openStep(t.at)
124
+ const at = tick(t.at)
125
+ const call = mapCall(OPENCODE_V2, t.name, t.toolKind, t.input, mapped)
126
+ const item = {
127
+ type: "tool",
128
+ id: `call_${random62(24)}`,
129
+ name: call.tool,
130
+ state: { status: "error", input: call.input, error: { type: "tool.execution", message: "No result was recorded for this call." } },
131
+ time: { created: at, ran: at, completed: at },
132
+ }
133
+ st.msg.content.push(item)
134
+ st.tools = true
135
+ calls.set(t.id, item)
136
+ break
137
+ }
138
+ case "tool_result": {
139
+ const item = calls.get(t.callId)
140
+ if (!item) {
141
+ drop("tool results without a call")
142
+ break
143
+ }
144
+ answered.add(t.callId)
145
+ item.time.completed = Math.max(item.time.created, tick(t.at))
146
+ const input = item.state.input
147
+ item.state = t.ok
148
+ ? { status: "completed", input, content: [{ type: "text", text: t.output }], ...(t.truncated ? { metadata: { truncated: true } } : {}) }
149
+ : { status: "error", input, error: { type: "tool.execution", message: t.output || "The call failed." } }
150
+ if (step) step.results = true
151
+ break
152
+ }
153
+ case "compaction":
154
+ push({ id: next("msg"), type: "compaction", status: "completed", reason: "auto", summary: t.summary, time: { created: tick(t.at) } })
155
+ break
156
+ case "file_edit":
157
+ if (!t.callId) drop("file edits not made by a tool call")
158
+ break
159
+ case "subagent":
160
+ drop("subagent sessions (their call and result are kept)")
161
+ break
162
+ }
163
+ }
164
+ closeStep()
165
+
166
+ const unanswered = calls.size - answered.size
167
+ if (unanswered > 0) drop("calls with no result, marked as failed", unanswered)
168
+ if (calls.size) drop("tool-call ids replaced", calls.size)
169
+ if (mapped.n) drop("tool calls renamed to OpenCode's own tools", mapped.n)
170
+
171
+ const info = {
172
+ id: sessionId,
173
+ // The importer resolves the project from the folder; the field is required, so it carries the default.
174
+ projectID: "global",
175
+ cost: 0,
176
+ tokens: zeroTokens(),
177
+ time: { created: firstClock, updated: Math.max(now, clock) },
178
+ title: continuedTitle(s),
179
+ location: { directory: o.cwd },
180
+ }
181
+ return { data: { info, messages }, sessionId, lost: [...lost].map(([what, n]) => `${what} (${n})`) }
182
+ }
183
+
184
+ /** OpenCode 2.x through `opencode session import`; the OpenCode writer dispatches here for a 2.x install. */
185
+ export const v2Writer: Writer = {
186
+ tool: "opencode",
187
+ label: "OpenCode",
188
+ way: "import",
189
+ async ready(opts): Promise<Readiness> {
190
+ const raw = versionOf(resolveBin({ bin: "opencode", installDir: ".opencode" }, opts.home).path)
191
+ const v = versionNumber(raw)
192
+ if (!raw) return { ok: false, why: "OpenCode is not installed (no opencode on PATH or in ~/.opencode/bin)" }
193
+ if (!v || !TESTED_V2.includes(v)) return { ok: false, why: `OpenCode ${v ?? raw} has not been tested; the 2.x import format was checked on ${TESTED_V2.join(", ")}` }
194
+ if (!isDir(opts.cwd)) return { ok: false, why: `the folder to continue in does not exist: ${opts.cwd}` }
195
+ return { ok: true }
196
+ },
197
+ async write(session, opts): Promise<WriteResult> {
198
+ const bin = resolveBin({ bin: "opencode", installDir: ".opencode" }, opts.home)
199
+ const { data, sessionId, lost } = buildTransferV2(session, { cwd: opts.cwd })
200
+ const file = join(opts.home, ".brain", "exports", `opencode-${sessionId}.json`)
201
+ if (!opts.dryRun) {
202
+ mkdirSync(dirname(file), { recursive: true })
203
+ writeFileSync(file, `${JSON.stringify(data, null, 2)}\n`)
204
+ }
205
+ return {
206
+ tool: "opencode",
207
+ way: "import",
208
+ sessionId,
209
+ written: [file],
210
+ // The background service files the session under the project of the directory it is given.
211
+ open: `cd ${sh(opts.cwd)} && ${sh(bin.command)} session import ${sh(file)} && ${sh(bin.command)} --session ${sessionId}`,
212
+ lost,
213
+ dryRun: !!opts.dryRun,
214
+ }
215
+ },
216
+ }
217
+
218
+ // ---- Reading 2.x stores ----
219
+
220
+ const hasTable = (d: Database, name: string) => !!d.query("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?").get(name)
221
+
222
+ const json = (s: unknown) => {
223
+ try {
224
+ return JSON.parse(String(s)) as Record<string, any>
225
+ } catch {
226
+ return undefined
227
+ }
228
+ }
229
+
230
+ /** Top-level sessions in a 2.x database (the reader opens a snapshot of it); none in a 1.x one. */
231
+ export function listV2Db(d: Database, path: string, tool: string): SessionRef[] {
232
+ if (!hasTable(d, "session_v2")) return []
233
+ const rows = d.query("SELECT id, title, directory, time_updated, project_id FROM session_v2 WHERE parent_id IS NULL AND time_archived IS NULL").all() as Array<Record<string, any>>
234
+ return rows.map((r) => ({
235
+ tool,
236
+ sourceId: String(r.id),
237
+ path,
238
+ ...(r.title ? { title: String(r.title) } : {}),
239
+ ...(r.directory ? { cwd: String(r.directory) } : {}),
240
+ ...(iso(r.time_updated) ? { updatedAt: iso(r.time_updated)! } : {}),
241
+ extra: { source: "db2", projectID: r.project_id },
242
+ }))
243
+ }
244
+
245
+ /** One session from a 2.x database, with its subagent sessions two levels down. */
246
+ export function readV2Db(d: Database, path: string, tool: string, id: string, depth = 0): Session {
247
+ if (!hasTable(d, "session_v2")) throw new Error(`no session_v2 table in ${path}`)
248
+ const row = d.query("SELECT * FROM session_v2 WHERE id = ?").get(id) as Record<string, any> | null
249
+ if (!row) throw new Error(`session ${id} not found`)
250
+ const cwd = row.directory ? String(row.directory) : undefined
251
+ const h = header(tool, id, {
252
+ ...(row.version ? { toolVersion: String(row.version) } : {}),
253
+ sourcePath: path,
254
+ ...(row.title ? { title: String(row.title) } : {}),
255
+ project: { ...(cwd ? { cwd } : {}), ...gitInfo(cwd) },
256
+ ...(iso(row.time_created) ? { startedAt: iso(row.time_created)! } : {}),
257
+ ...(iso(row.time_updated) ? { updatedAt: iso(row.time_updated)! } : {}),
258
+ ...(row.parent_id ? { parent: { id: `${tool}:${row.parent_id}`, kind: "subagent" as const } } : {}),
259
+ })
260
+ const lost = lossCounter()
261
+ const parts = new Map<string, Array<Record<string, any>>>()
262
+ if (hasTable(d, "session_part"))
263
+ for (const p of d.query("SELECT message_id, type, data FROM session_part WHERE session_id = ? ORDER BY message_id, ordinal").all(id) as Array<Record<string, any>>) {
264
+ const data = json(p.data)
265
+ if (!data) {
266
+ lost.add("unreadable message or part records")
267
+ continue
268
+ }
269
+ const list = parts.get(p.message_id) ?? []
270
+ list.push({ ...data, type: p.type })
271
+ parts.set(p.message_id, list)
272
+ }
273
+ const turns: Turn[] = []
274
+ const callIds = new Set<string>()
275
+ for (const m of d.query("SELECT id, type, time_created, data FROM session_message WHERE session_id = ? ORDER BY seq").all(id) as Array<Record<string, any>>) {
276
+ const data = json(m.data)
277
+ if (!data) {
278
+ lost.add("unreadable message or part records")
279
+ continue
280
+ }
281
+ const at = iso(m.time_created)
282
+ switch (m.type) {
283
+ case "user": {
284
+ const files = Array.isArray(data.files) ? data.files.length : 0
285
+ if (files) lost.add("attachment data", files)
286
+ if (typeof data.text === "string" && (data.text.trim() || files))
287
+ turns.push({ kind: "user", ...(at ? { at } : {}), text: data.text, ...(files ? { attachments: data.files.map((f: any) => ({ kind: String(f.mime ?? "").startsWith("image/") ? "image" : "file", ...(f.mime ? { mime: f.mime } : {}), ...(f.name ? { name: f.name } : {}) })) } : {}) })
288
+ break
289
+ }
290
+ case "synthetic": {
291
+ const continued = typeof data.text === "string" ? data.text.match(CONTINUED_NOTE)?.[1] : undefined
292
+ if (continued) h.parent ??= { id: continued, kind: "continued" }
293
+ else lost.add("messages the tool injected as the user")
294
+ break
295
+ }
296
+ case "assistant": {
297
+ const model = data.model?.id ? String(data.model.id) : undefined
298
+ const content: Array<Record<string, any>> = parts.get(m.id) ?? (Array.isArray(data.content) ? data.content : [])
299
+ if (model && content.some((c) => c.type === "text" || c.type === "tool")) noteModel(h, model)
300
+ if (data.error) lost.add("error notices on replies")
301
+ let usage = data.tokens ? { input: data.tokens.input, output: data.tokens.output, cacheRead: data.tokens.cache?.read, cacheWrite: data.tokens.cache?.write } : undefined
302
+ for (const c of content) {
303
+ if (c.type === "text" && typeof c.text === "string" && c.text.trim()) {
304
+ turns.push({ kind: "assistant", ...(at ? { at } : {}), text: c.text, ...(model ? { model } : {}), ...(usage && Object.values(usage).some(Boolean) ? { usage } : {}) })
305
+ usage = undefined
306
+ } else if (c.type === "reasoning") {
307
+ const signed = !!c.native && Object.keys(c.native).length > 0
308
+ if (typeof c.text === "string" && c.text.trim()) {
309
+ turns.push({ kind: "reasoning", ...(at ? { at } : {}), text: c.text })
310
+ if (signed) lost.add("reasoning signatures")
311
+ } else if (signed) lost.add("signed reasoning")
312
+ } else if (c.type === "tool") toolTurns(c, at, turns, callIds, lost)
313
+ }
314
+ break
315
+ }
316
+ case "compaction":
317
+ if (data.status === "completed" && typeof data.summary === "string") turns.push({ kind: "compaction", ...(at ? { at } : {}), summary: data.summary })
318
+ break
319
+ case "shell": {
320
+ // A command the person ran from the prompt: a call and its result, as the model was shown it.
321
+ const callId = `shell-${m.id}`
322
+ turns.push({ kind: "tool_call", ...(at ? { at } : {}), id: callId, name: "shell", toolKind: "shell", input: { command: data.command } })
323
+ if (data.output) turns.push({ kind: "tool_result", callId, ok: data.exit === undefined || data.exit === 0, output: clip(asText(data.output.output)).text })
324
+ break
325
+ }
326
+ case "system":
327
+ case "skill":
328
+ lost.add("system and skill notes")
329
+ break
330
+ }
331
+ }
332
+ lost.flush(h)
333
+ const s: Session = { header: h, turns }
334
+ if (depth < 2) {
335
+ const children: Session[] = []
336
+ for (const c of d.query("SELECT id FROM session_v2 WHERE parent_id = ?").all(id) as Array<{ id: string }>) {
337
+ try {
338
+ children.push(readV2Db(d, path, tool, c.id, depth + 1))
339
+ } catch {
340
+ h.lost.push(`unreadable subagent session (${c.id})`)
341
+ }
342
+ }
343
+ if (children.length) s.children = children
344
+ }
345
+ return s
346
+ }
347
+
348
+ function toolTurns(c: Record<string, any>, at: string | undefined, turns: Turn[], callIds: Set<string>, lost: ReturnType<typeof lossCounter>) {
349
+ const st = (c.state ?? {}) as Record<string, any>
350
+ const name = String(c.name ?? "unknown")
351
+ let id = String(c.id ?? "")
352
+ for (let n = 2; !id || callIds.has(id); n++) id = `${String(c.id ?? "call")}#${n}`
353
+ callIds.add(id)
354
+ const start = iso(c.time?.created) ?? at
355
+ const end = iso(c.time?.completed) ?? start
356
+ if (c.native && Object.keys(c.native).length) lost.add("tool-call signatures")
357
+ const input = st.status === "streaming" ? asText(st.input) : (st.input ?? {})
358
+ turns.push({ kind: "tool_call", ...(start ? { at: start } : {}), id, name, toolKind: toolKindOf(name), input })
359
+ if (st.status !== "completed" && st.status !== "error") return
360
+ const items: Array<Record<string, any>> = Array.isArray(st.content) ? st.content : []
361
+ const files = items.filter((x) => x.type === "file").length
362
+ if (files) lost.add("attachments in tool results", files)
363
+ const text = items.filter((x) => x.type === "text").map((x) => String(x.text)).join("\n")
364
+ const raw = st.status === "completed" ? text : [st.error?.message, text].filter(Boolean).join("\n")
365
+ const out = clip(raw)
366
+ turns.push({ kind: "tool_result", ...(end ? { at: end } : {}), callId: id, ok: st.status === "completed", output: out.text, ...(out.truncated || st.metadata?.truncated === true ? { truncated: true } : {}) })
367
+ }