@100xprompt/brain 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (213) hide show
  1. package/LICENSE +48 -0
  2. package/README.md +172 -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 +326 -0
  170. package/src/fsutil.ts +133 -0
  171. package/src/gateway/connect.ts +845 -0
  172. package/src/gateway/signin.ts +323 -0
  173. package/src/handoff.ts +104 -0
  174. package/src/hooks.ts +455 -0
  175. package/src/memory/audit.ts +514 -0
  176. package/src/memory/blobs.ts +94 -0
  177. package/src/memory/cli.ts +117 -0
  178. package/src/memory/distill.ts +381 -0
  179. package/src/memory/externalize.ts +435 -0
  180. package/src/memory/lookups.ts +70 -0
  181. package/src/memory/mcp-install.ts +401 -0
  182. package/src/memory/mcp-server.ts +296 -0
  183. package/src/memory/prepare.ts +179 -0
  184. package/src/memory/preview.ts +154 -0
  185. package/src/memory/project.ts +35 -0
  186. package/src/memory/redact.ts +72 -0
  187. package/src/memory/relevance.ts +187 -0
  188. package/src/memory/repomap.ts +516 -0
  189. package/src/memory/store.ts +270 -0
  190. package/src/memory/sync.ts +166 -0
  191. package/src/memory/tidy.ts +193 -0
  192. package/src/memory/types.ts +118 -0
  193. package/src/org/admin-ops.ts +96 -0
  194. package/src/org/auth.ts +112 -0
  195. package/src/org/backup.ts +115 -0
  196. package/src/org/cli.ts +438 -0
  197. package/src/org/client.ts +404 -0
  198. package/src/org/config.ts +44 -0
  199. package/src/org/crypto.ts +537 -0
  200. package/src/org/db.ts +435 -0
  201. package/src/org/graph.ts +318 -0
  202. package/src/org/oidc.ts +435 -0
  203. package/src/org/policy.ts +398 -0
  204. package/src/org/server.ts +920 -0
  205. package/src/org/sso.ts +316 -0
  206. package/src/org/tls.ts +66 -0
  207. package/src/org/types.ts +193 -0
  208. package/src/self.ts +68 -0
  209. package/src/session/attachments.ts +53 -0
  210. package/src/session/peekcache.ts +50 -0
  211. package/src/session/types.ts +104 -0
  212. package/src/session/util.ts +130 -0
  213. package/src/sqlite.ts +48 -0
@@ -0,0 +1,198 @@
1
+ import type { Database } from "bun:sqlite"
2
+ import { exists, gitInfo } from "../../fsutil.ts"
3
+ import type { Attachment, Session, Turn } from "../../session/types.ts"
4
+ import { asText, clip, header, iso, lossCounter, noteModel } from "../../session/util.ts"
5
+ import { withSnapshot } from "../../sqlite.ts"
6
+ import type { Reader, SessionRef } from "../types.ts"
7
+ import { CONTINUED_NOTE, LABEL, THREAD_VERSION, TOOL, threadsDb, zedToolKind } from "./records.ts"
8
+
9
+ // Zed's native agent threads (records.ts has the layout). Root threads are listed; a subagent thread
10
+ // (parent_id set) belongs to its parent and is not listed on its own. Threads of external ACP agents
11
+ // (Claude Code, Gemini CLI, ...) are not here: Zed keeps only their metadata and the agent keeps the content.
12
+
13
+ type Json = Record<string, any>
14
+
15
+ /** Columns a threads table has; older Zed versions lack parent_id, folder_paths and created_at. */
16
+ const columns = (db: Database) => new Set((db.query("PRAGMA table_info(threads)").all() as Array<{ name: string }>).map((c) => c.name))
17
+
18
+ /** The first of a thread's folders (folder_paths is newline-joined), the project it ran in. */
19
+ const firstFolder = (paths: unknown) => (typeof paths === "string" && paths ? paths.split("\n")[0] : undefined)
20
+
21
+ export const zedReader: Reader = {
22
+ tool: TOOL,
23
+ label: LABEL,
24
+ async list(home) {
25
+ const path = threadsDb(home)
26
+ if (!exists(path)) return []
27
+ const rows = withSnapshot(path, (db) => {
28
+ const have = columns(db)
29
+ if (!have.has("id")) return []
30
+ const pick = (c: string) => (have.has(c) ? c : `NULL AS ${c}`)
31
+ return db
32
+ .query(`SELECT id, ${pick("parent_id")}, ${pick("folder_paths")}, summary, updated_at, ${pick("created_at")} FROM threads ORDER BY updated_at DESC`)
33
+ .all() as Json[]
34
+ })
35
+ const refs: SessionRef[] = []
36
+ for (const r of rows) {
37
+ if (r.parent_id) continue
38
+ const cwd = firstFolder(r.folder_paths)
39
+ refs.push({
40
+ tool: TOOL,
41
+ sourceId: String(r.id),
42
+ path,
43
+ ...(r.summary ? { title: String(r.summary) } : {}),
44
+ ...(cwd ? { cwd } : {}),
45
+ ...(iso(r.updated_at) ? { updatedAt: iso(r.updated_at)! } : {}),
46
+ })
47
+ }
48
+ return refs
49
+ },
50
+ async read(ref, home) {
51
+ const path = exists(ref.path) ? ref.path : threadsDb(home)
52
+ const row = withSnapshot(path, (db) => {
53
+ const have = columns(db)
54
+ const pick = (c: string) => (have.has(c) ? c : `NULL AS ${c}`)
55
+ return db.query(`SELECT data_type, data, ${pick("folder_paths")}, summary, updated_at, ${pick("created_at")} FROM threads WHERE id = ?`).get(ref.sourceId) as Json | null
56
+ })
57
+ if (!row) throw new Error(`thread ${ref.sourceId} is not in ${path}`)
58
+ const bytes = row.data instanceof Uint8Array ? row.data : Buffer.from(String(row.data ?? ""))
59
+ let text: string
60
+ if (row.data_type === "zstd") text = Buffer.from(Bun.zstdDecompressSync(bytes)).toString("utf8")
61
+ else if (row.data_type === "json") text = Buffer.from(bytes).toString("utf8")
62
+ else throw new Error(`thread ${ref.sourceId} has an unknown data type: ${String(row.data_type)}`)
63
+ return parseThread(JSON.parse(text) as Json, {
64
+ id: ref.sourceId,
65
+ path,
66
+ cwd: firstFolder(row.folder_paths),
67
+ createdAt: iso(row.created_at),
68
+ updatedAt: iso(row.updated_at),
69
+ home,
70
+ })
71
+ },
72
+ }
73
+
74
+ /** A thread's tool input as the value it holds: {type, value} is how Zed writes it, anything else is raw JSON. */
75
+ function toolInput(input: unknown): unknown {
76
+ if (input && typeof input === "object" && !Array.isArray(input)) {
77
+ const o = input as Json
78
+ if (Object.keys(o).length === 2 && (o.type === "json" || o.type === "text") && "value" in o) return o.value
79
+ }
80
+ return input
81
+ }
82
+
83
+ /** A tool result's text: its Text parts (strings, {Text}, {type: "text"} and {text} all occur), images counted. */
84
+ function resultText(content: unknown, images: () => void): string {
85
+ const parts = Array.isArray(content) ? content : [content]
86
+ const out: string[] = []
87
+ for (const p of parts) {
88
+ if (typeof p === "string") out.push(p)
89
+ else if (p && typeof p === "object") {
90
+ const o = p as Json
91
+ const text = o.Text ?? o.text ?? (o.type === "text" ? o.text : undefined)
92
+ if (typeof text === "string") out.push(text)
93
+ else images()
94
+ }
95
+ }
96
+ return out.join("")
97
+ }
98
+
99
+ /** A mention's name, as Zed shows it on the chip (MentionUri::name, simplified). */
100
+ function mentionName(uri: Json): { kind: string; name: string; path?: string } {
101
+ const [kind, v] = Object.entries(uri ?? {})[0] ?? ["Unknown", {}]
102
+ const o = (v ?? {}) as Json
103
+ const path = typeof o.abs_path === "string" ? o.abs_path : undefined
104
+ const name = typeof o.name === "string" ? o.name : path ?? (typeof o.url === "string" ? o.url : kind)
105
+ return { kind, name, ...(path ? { path } : {}) }
106
+ }
107
+
108
+ /** A DbThread (version 0.3.0) as a portable session. */
109
+ export function parseThread(
110
+ thread: Json,
111
+ ctx: { id: string; path: string; cwd?: string; createdAt?: string; updatedAt?: string; home: string },
112
+ ): Session {
113
+ if (thread.version !== THREAD_VERSION)
114
+ throw new Error(`thread ${ctx.id} is in Zed's format ${String(thread.version ?? "agent 1")}, not ${THREAD_VERSION}; Zed upgrades it when the thread is next opened`)
115
+ const h = header(TOOL, ctx.id, {
116
+ sourcePath: ctx.path,
117
+ ...(thread.title ? { title: String(thread.title) } : {}),
118
+ project: ctx.cwd ? { cwd: ctx.cwd, ...gitInfo(ctx.cwd, ctx.home) } : {},
119
+ ...(ctx.createdAt ? { startedAt: ctx.createdAt } : {}),
120
+ ...((iso(thread.updated_at) ?? ctx.updatedAt) ? { updatedAt: iso(thread.updated_at) ?? ctx.updatedAt } : {}),
121
+ })
122
+ const model = thread.model && typeof thread.model === "object" ? `${thread.model.provider}/${thread.model.model}` : undefined
123
+ noteModel(h, model)
124
+ const lost = lossCounter()
125
+ const turns: Turn[] = []
126
+ // Zed stores no per-message times: every turn carries the thread's last update, the best it says.
127
+ const at = h.updatedAt
128
+
129
+ for (const m of Array.isArray(thread.messages) ? thread.messages : []) {
130
+ if (m === "Resume") continue // Zed's own "Continue where you left off", not the person's words
131
+ if (!m || typeof m !== "object") {
132
+ lost.add("unknown message kinds")
133
+ continue
134
+ }
135
+ if (m.User) {
136
+ const parts: string[] = []
137
+ const attachments: Attachment[] = []
138
+ for (const c of Array.isArray(m.User.content) ? m.User.content : []) {
139
+ if (typeof c?.Text === "string") parts.push(c.Text)
140
+ else if (c?.Mention) {
141
+ const content = typeof c.Mention.content === "string" ? c.Mention.content : ""
142
+ const continued = content.match(CONTINUED_NOTE)
143
+ if (continued?.[1] && !h.parent) {
144
+ h.parent = { id: continued[1], kind: "continued" }
145
+ continue
146
+ }
147
+ const { kind, name, path } = mentionName(c.Mention.uri)
148
+ if (path) attachments.push({ kind: "file", path, name })
149
+ parts.push(content ? `[@${name} (${kind})]\n${clip(content).text}` : `[@${name}]`)
150
+ } else if (c?.Image) lost.add("images")
151
+ else lost.add("unknown user content")
152
+ }
153
+ const text = parts.join("\n\n")
154
+ if (text.trim() || attachments.length) turns.push({ kind: "user", ...(at ? { at } : {}), text, ...(attachments.length ? { attachments } : {}) })
155
+ continue
156
+ }
157
+ if (m.Agent) {
158
+ const results = (m.Agent.tool_results ?? {}) as Json
159
+ for (const c of Array.isArray(m.Agent.content) ? m.Agent.content : []) {
160
+ if (typeof c?.Text === "string") {
161
+ if (c.Text.trim()) turns.push({ kind: "assistant", ...(at ? { at } : {}), text: c.Text, ...(model ? { model } : {}) })
162
+ } else if (c?.Thinking) {
163
+ if (typeof c.Thinking.text === "string" && c.Thinking.text.trim()) turns.push({ kind: "reasoning", ...(at ? { at } : {}), text: c.Thinking.text })
164
+ else if (c.Thinking.signature) lost.add("signed reasoning")
165
+ } else if (c?.RedactedThinking !== undefined) lost.add("redacted reasoning")
166
+ else if (c?.ToolUse) {
167
+ const u = c.ToolUse as Json
168
+ const name = String(u.name ?? "tool")
169
+ turns.push({ kind: "tool_call", ...(at ? { at } : {}), id: String(u.id), name, toolKind: zedToolKind(name), input: toolInput(u.input) })
170
+ } else lost.add("unknown agent content")
171
+ }
172
+ // Results sit beside the calls in the same message, keyed by tool use id, in the order they came back.
173
+ for (const r of Object.values(results) as Json[]) {
174
+ let images = 0
175
+ const out = clip(resultText(r.content, () => images++))
176
+ if (images) lost.add("images in tool results", images)
177
+ turns.push({
178
+ kind: "tool_result",
179
+ ...(at ? { at } : {}),
180
+ callId: String(r.tool_use_id),
181
+ ok: r.is_error !== true,
182
+ output: out.text || (r.output !== undefined && r.output !== null ? clip(asText(r.output)).text : ""),
183
+ ...(out.truncated ? { truncated: true } : {}),
184
+ })
185
+ }
186
+ if (m.Agent.reasoning_details) lost.add("provider reasoning details")
187
+ continue
188
+ }
189
+ if (m.Compaction) {
190
+ if (typeof m.Compaction.Summary === "string") turns.push({ kind: "compaction", ...(at ? { at } : {}), summary: m.Compaction.Summary })
191
+ else lost.add("provider-native compaction")
192
+ continue
193
+ }
194
+ lost.add("unknown message kinds")
195
+ }
196
+ lost.flush(h)
197
+ return { header: h, turns }
198
+ }
@@ -0,0 +1,87 @@
1
+ import { join } from "node:path"
2
+ import { appSupport, exists, versionOf } from "../../fsutil.ts"
3
+ import type { ToolKind } from "../../session/types.ts"
4
+ import { toolKindOf } from "../../session/util.ts"
5
+
6
+ // What Zed's native agent threads look like, as far as this adapter relies on it. Checked against Zed's
7
+ // source at v1.26.2, v1.27.0 and main (2026-11-07): crates/agent/src/db.rs (ThreadsDatabase, DbThread),
8
+ // crates/agent/src/thread.rs (Message and its content), crates/language_model_core (tool use and result),
9
+ // crates/acp_thread/src/mention.rs (MentionUri) and crates/agent_ui/src/thread_metadata_store.rs (the sidebar).
10
+ // Catalogue: docs/sessions/2026-11-08-session-formats.md, entries 49 and 70.
11
+ //
12
+ // One row per thread in <data dir>/threads/threads.db, table `threads`: id (an ACP session id, a v4 uuid
13
+ // for native threads), parent_id (subagent threads), folder_paths and folder_paths_order (the project's
14
+ // folders, newline- and comma-joined), summary (the title), updated_at and created_at (RFC 3339), data_type
15
+ // ("zstd" or "json") and data: the DbThread as JSON with a `version` field, zstd level 3.
16
+
17
+ export const TOOL = "zed"
18
+ export const LABEL = "Zed"
19
+
20
+ /** DbThread::VERSION. Any other version is Zed's legacy "agent 1" format, which Zed upgrades on load. */
21
+ export const THREAD_VERSION = "0.3.0"
22
+
23
+ /** Zed releases whose thread format was checked in their source; the writer was run against 1.27.0. */
24
+ export const TESTED_VERSIONS = ["1.26.2", "1.27.0"]
25
+
26
+ /** Zed's data dir: ~/Library/Application Support/Zed on macOS, ~/.local/share/zed elsewhere (paths::data_dir). */
27
+ export const dataDir = (home: string) =>
28
+ process.platform === "darwin" ? join(appSupport(home), "Zed") : join(home, ".local", "share", "zed")
29
+
30
+ export const threadsDb = (home: string) => join(dataDir(home), "threads", "threads.db")
31
+
32
+ /** The statements Zed runs when it opens threads.db (ThreadsDatabase::new), so a new file matches its own. */
33
+ export const SCHEMA = [
34
+ "CREATE TABLE IF NOT EXISTS threads (id TEXT PRIMARY KEY, summary TEXT NOT NULL, updated_at TEXT NOT NULL, data_type TEXT NOT NULL, data BLOB NOT NULL)",
35
+ "ALTER TABLE threads ADD COLUMN parent_id TEXT",
36
+ "ALTER TABLE threads ADD COLUMN folder_paths TEXT",
37
+ "ALTER TABLE threads ADD COLUMN folder_paths_order TEXT",
38
+ "ALTER TABLE threads ADD COLUMN created_at TEXT",
39
+ "CREATE INDEX IF NOT EXISTS threads_list_metadata ON threads (updated_at, created_at, id, parent_id, folder_paths, folder_paths_order, summary)",
40
+ ]
41
+
42
+ /** Process names of a running Zed (the macOS app's binary is `zed`; Linux packages also ship `zeditor`). */
43
+ export const PROCESSES = ["zed", "Zed", "zeditor", "zed-editor"]
44
+
45
+ /** Zed's own tools (crates/agent/src/tools), where the name alone does not say what they do. */
46
+ const KINDS: Record<string, ToolKind> = {
47
+ terminal: "shell",
48
+ read_file: "read",
49
+ edit_file: "edit",
50
+ streaming_edit_file: "edit",
51
+ grep: "search",
52
+ find_path: "search",
53
+ list_directory: "list",
54
+ fetch: "web",
55
+ web_search: "web",
56
+ spawn_agent: "task",
57
+ create_directory: "write",
58
+ copy_path: "write",
59
+ move_path: "write",
60
+ delete_path: "edit",
61
+ }
62
+
63
+ export const zedToolKind = (name: string): ToolKind => KINDS[name] ?? toolKindOf(name)
64
+
65
+ const semver = (v: string | undefined) => v?.match(/(\d+\.\d+\.\d+)/)?.[1]
66
+
67
+ /**
68
+ * The installed Zed's version: the app bundle's CFBundleShortVersionString on macOS (what `zed --version`
69
+ * prints too), else the `zed` CLI. Undefined when Zed is not installed.
70
+ */
71
+ export function zedVersion(home: string): string | undefined {
72
+ if (process.platform === "darwin") {
73
+ for (const app of ["/Applications/Zed.app", join(home, "Applications", "Zed.app")]) {
74
+ const plist = join(app, "Contents", "Info.plist")
75
+ if (!exists(plist)) continue
76
+ const p = Bun.spawnSync(["plutil", "-extract", "CFBundleShortVersionString", "raw", "-o", "-", plist], { stdout: "pipe", stderr: "pipe" })
77
+ const v = p.exitCode === 0 ? semver(p.stdout.toString()) : undefined
78
+ if (v) return v
79
+ }
80
+ }
81
+ return semver(versionOf("zed")) ?? semver(versionOf("zeditor"))
82
+ }
83
+
84
+ /** The first message's note, recognised by the reader so it is not taken for the person's request. */
85
+ export const continuedNote = (label: string, id: string, when: string) =>
86
+ `This thread continues ${label} session ${id}, carried over by brain session on ${when}. The messages that follow are its history; reasoning and anything listed as lost did not carry over. Check files before relying on details, since they may have changed.`
87
+ export const CONTINUED_NOTE = /^This thread continues .+? session (\S+), carried over by brain session/
@@ -0,0 +1,341 @@
1
+ import { Database } from "bun:sqlite"
2
+ import { copyFileSync, mkdirSync, readFileSync, realpathSync, statSync } from "node:fs"
3
+ import { dirname, extname, join } from "node:path"
4
+ import { toolLabel } from "../../brief.ts"
5
+ import { exists, isDir, isRunning } from "../../fsutil.ts"
6
+ import type { Session, Turn } 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 { zedReader } from "./reader.ts"
10
+ import { LABEL, PROCESSES, SCHEMA, TESTED_VERSIONS, THREAD_VERSION, TOOL, continuedNote, threadsDb, zedVersion } from "./records.ts"
11
+
12
+ // Continues a portable session in Zed as a new native agent thread: one row in threads.db, written the way
13
+ // ThreadsDatabase::save_thread_sync writes it (a DbThread as JSON with its version, zstd level 3, the folder
14
+ // list and RFC 3339 times), so Thread History lists it and opening it loads it. On its next launch Zed adds
15
+ // the thread to the sidebar itself (migrate_thread_metadata in agent_ui's thread_metadata_store.rs), and keeps
16
+ // it unarchived because it is the newest of its batch for its project; the sidebar's own database is not touched.
17
+ //
18
+ // Messages are regrouped Zed's way: one Agent message per model response, holding its text, readable
19
+ // reasoning (Thinking without a signature, which Zed sends as text or leaves out per provider) and tool
20
+ // uses, with each use's result in the same message's tool_results. Zed sends a tool use to the model only
21
+ // when it has a result, so a call without one is closed as an error. Results keep `output` empty, so on
22
+ // replay Zed shows their text instead of asking each tool to rebuild its own view. Compaction is Zed's own
23
+ // Message::Compaction(Summary): Zed sends the model only what follows the last one.
24
+ //
25
+ // Zed has no hidden message. The note saying where the thread came from is a thread mention on the first
26
+ // request: a chip in the panel, and an attached <threads> context block for the model, not the person's words.
27
+ //
28
+ // The database is written only while Zed is closed, after a copy of it is kept under ~/.brain/backups/zed/.
29
+
30
+ const shellQuote = (s: string) => (/^[\w./@%+=:,-]+$/.test(s) ? s : `'${s.replace(/'/g, `'\\''`)}'`)
31
+
32
+ function realCwd(cwd: string): string {
33
+ try {
34
+ return realpathSync(cwd)
35
+ } catch {
36
+ return cwd
37
+ }
38
+ }
39
+
40
+ /** Tool names the providers Zed talks to accept. */
41
+ const toolName = (name: string) => name.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 64) || "tool"
42
+
43
+ /** Tool use ids the same way: Anthropic's ^[a-zA-Z0-9_-]+$. */
44
+ const useId = (id: string) => id.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 64) || "tool_use"
45
+
46
+ /** Tool input as an object, which every provider expects; a raw string (apply_patch) or array is wrapped. */
47
+ const objectInput = (input: unknown): Record<string, unknown> =>
48
+ input && typeof input === "object" && !Array.isArray(input) ? (input as Record<string, unknown>) : { input: input ?? null }
49
+
50
+ const PNG_LIMIT = 5 * 1024 * 1024
51
+
52
+ type Json = Record<string, any>
53
+ type Call = Extract<Turn, { kind: "tool_call" }>
54
+ interface AgentMessage {
55
+ content: Json[]
56
+ tool_results: Record<string, Json>
57
+ reasoning_details: null
58
+ }
59
+
60
+ export interface ThreadPlan {
61
+ /** The DbThread JSON as Zed serialises it, with its version. */
62
+ thread: Json
63
+ title: string
64
+ lost: string[]
65
+ /** What a read-back must find. */
66
+ expect: { users: number; assistants: number; calls: number }
67
+ }
68
+
69
+ /** The DbThread for `session` as a new Zed thread. Pure apart from reading attached PNG images. */
70
+ export function planThread(session: Session, opts: { now: Date }): ThreadPlan {
71
+ const drops = new Map<string, number>()
72
+ const drop = (what: string, n = 1) => drops.set(what, (drops.get(what) ?? 0) + n)
73
+ const label = toolLabel(session.header.tool)
74
+ const results = new Map<string, Extract<Turn, { kind: "tool_result" }>>()
75
+ for (const t of session.turns) if (t.kind === "tool_result" && !results.has(t.callId)) results.set(t.callId, t)
76
+
77
+ const messages: Json[] = []
78
+ const expect = { users: 0, assistants: 0, calls: 0 }
79
+ const note = {
80
+ Mention: {
81
+ uri: { Thread: { id: session.header.id, name: `${label} session ${session.header.sourceId}` } },
82
+ content: continuedNote(label, session.header.id, opts.now.toISOString().slice(0, 10)),
83
+ },
84
+ }
85
+ let noted = false
86
+
87
+ let cur: AgentMessage | undefined
88
+ const flush = () => {
89
+ if (cur?.content.length) messages.push({ Agent: cur })
90
+ cur = undefined
91
+ }
92
+ const agent = () => (cur ??= { content: [], tool_results: {}, reasoning_details: null })
93
+ const hasUse = () => !!cur?.content.some((c) => "ToolUse" in c)
94
+ // Which message holds each call, so a late result still lands beside its call; ids remapped once.
95
+ const owner = new Map<string, AgentMessage>()
96
+ const ids = new Map<string, string>()
97
+ const used = new Set<string>()
98
+ let remapped = 0
99
+ const idFor = (id: string) => {
100
+ let v = ids.get(id)
101
+ if (v) return v
102
+ v = useId(id)
103
+ for (let n = 2; used.has(v); n++) v = `${useId(id)}_${n}`
104
+ ids.set(id, v)
105
+ used.add(v)
106
+ if (v !== id) remapped++
107
+ return v
108
+ }
109
+ const result = (msg: AgentMessage, call: Call, ok: boolean, output: string) => {
110
+ msg.tool_results[idFor(call.id)] = {
111
+ tool_use_id: idFor(call.id),
112
+ tool_name: toolName(call.name),
113
+ is_error: !ok,
114
+ content: [{ Text: output.trim() ? output : "(no output)" }],
115
+ output: null,
116
+ }
117
+ }
118
+ const calls = new Map<string, Call>()
119
+
120
+ for (const t of session.turns) {
121
+ switch (t.kind) {
122
+ case "user": {
123
+ flush()
124
+ const content: Json[] = []
125
+ let text = t.text
126
+ for (const a of t.attachments ?? []) {
127
+ if (!a.path) continue // bytes the reader could not keep; it already counted them
128
+ if (a.kind === "file") {
129
+ text += `\n\n[Attached file: ${a.path}]`
130
+ continue
131
+ }
132
+ // Zed's LanguageModelImage holds a base64 PNG and nothing else.
133
+ let size = Number.POSITIVE_INFINITY
134
+ try {
135
+ size = statSync(a.path).size
136
+ } catch {}
137
+ if ((a.mime === "image/png" || extname(a.path).toLowerCase() === ".png") && size <= PNG_LIMIT)
138
+ content.push({ Image: { source: readFileSync(a.path).toString("base64") } })
139
+ else drop("images Zed cannot hold (it keeps PNG only)")
140
+ }
141
+ if (!text.trim() && !content.length) break
142
+ if (text.trim()) content.unshift({ Text: text })
143
+ if (!noted) {
144
+ content.unshift(note)
145
+ noted = true
146
+ }
147
+ messages.push({ User: { id: crypto.randomUUID(), content } })
148
+ if (text.trim()) expect.users++
149
+ break
150
+ }
151
+ case "assistant":
152
+ // Text after a tool use is a new response: it was written after the use's result came back.
153
+ if (hasUse()) flush()
154
+ if (!t.text.trim()) break
155
+ agent().content.push({ Text: t.text })
156
+ expect.assistants++
157
+ break
158
+ case "reasoning":
159
+ if (hasUse()) flush()
160
+ if (t.text.trim()) agent().content.push({ Thinking: { text: t.text, signature: null } })
161
+ break
162
+ case "tool_call": {
163
+ if (calls.has(t.id)) {
164
+ drop("repeated tool call ids")
165
+ break
166
+ }
167
+ const input = objectInput(t.input)
168
+ const name = toolName(t.name)
169
+ if (name !== t.name) drop("tool names changed to fit Zed")
170
+ const msg = agent()
171
+ msg.content.push({
172
+ ToolUse: { id: idFor(t.id), name, raw_input: JSON.stringify(input), input: { type: "json", value: input }, is_input_complete: true, thought_signature: null },
173
+ })
174
+ owner.set(t.id, msg)
175
+ calls.set(t.id, t)
176
+ expect.calls++
177
+ break
178
+ }
179
+ case "tool_result": {
180
+ const msg = owner.get(t.callId)
181
+ const call = calls.get(t.callId)
182
+ if (!msg || !call) {
183
+ drop("tool results without a call")
184
+ break
185
+ }
186
+ if (msg.tool_results[idFor(t.callId)]) {
187
+ drop("repeated tool results")
188
+ break
189
+ }
190
+ result(msg, call, t.ok, t.fullPath ? `${t.output}\n\n[Full output: ${t.fullPath}]` : t.output)
191
+ break
192
+ }
193
+ case "compaction":
194
+ flush()
195
+ messages.push({ Compaction: { Summary: `[Summary of the earlier conversation in ${label}, which replaced the turns before it]\n\n${t.summary}` } })
196
+ break
197
+ case "file_edit":
198
+ if (!t.callId || !calls.has(t.callId)) drop("file edits outside a tool call")
199
+ break
200
+ case "subagent":
201
+ drop("subagent sessions (their results stay in the parent's tool results)")
202
+ break
203
+ }
204
+ }
205
+ flush()
206
+ // Zed leaves a tool use without a result out of what it sends, so every call is answered.
207
+ for (const [id, call] of calls) {
208
+ const msg = owner.get(id)!
209
+ if (!msg.tool_results[idFor(id)]) {
210
+ result(msg, call, false, "No result was recorded for this call; the session moved on before it finished.")
211
+ drop("tool calls without a result (closed as errors)")
212
+ }
213
+ }
214
+ if (!messages.length) throw new Error("nothing Zed can show: the session has no messages or tool calls")
215
+ // A session that opens with the model (a summary, a reply) still says where it came from.
216
+ if (!noted) messages.unshift({ User: { id: crypto.randomUUID(), content: [note] } })
217
+
218
+ const firstText = firstUserText(session)?.replace(/\s+/g, " ").trim().slice(0, 60)
219
+ const title = `${session.header.title ?? firstText ?? "Session"} (from ${label})`
220
+ const thread: Json = {
221
+ title,
222
+ messages,
223
+ updated_at: opts.now.toISOString(),
224
+ detailed_summary: null,
225
+ initial_project_snapshot: null,
226
+ cumulative_token_usage: {},
227
+ request_token_usage: {},
228
+ // No model: a foreign model id would leave the thread's model unresolved; Zed picks the person's default.
229
+ model: null,
230
+ profile: null,
231
+ subagent_context: null,
232
+ speed: null,
233
+ thinking_enabled: false,
234
+ thinking_effort: null,
235
+ draft_prompt: null,
236
+ ui_scroll_position: null,
237
+ sandboxed_terminal_temp_dir: null,
238
+ sandbox_grants: { write_paths: [], network_hosts: [], network_any_host: false, allow_fs_write_all: false, unsandboxed: false, sandbox_fallback: false },
239
+ version: THREAD_VERSION,
240
+ }
241
+ const lost = [...drops].map(([what, n]) => `${what} (${n})`)
242
+ if (remapped) lost.push(`tool call ids, changed to fit Zed (${remapped})`)
243
+ return { thread, title, lost, expect }
244
+ }
245
+
246
+ /** chrono's to_rfc3339 for a UTC time, which is how Zed fills the updated_at and created_at columns. */
247
+ const rfc3339 = (d: Date) => d.toISOString().replace("Z", "+00:00")
248
+
249
+ /** A filesystem-safe timestamp for a backup folder. */
250
+ const stamp = (d: Date) => d.toISOString().replace(/[:.]/g, "-")
251
+
252
+ /** Copies the database and its -wal and -shm files; returns the folder. */
253
+ export function backupDb(home: string, path: string, now: Date): string {
254
+ const dir = join(home, ".brain", "backups", TOOL, stamp(now))
255
+ mkdirSync(dir, { recursive: true })
256
+ for (const side of ["", "-wal", "-shm", "-journal"]) if (exists(path + side)) copyFileSync(path + side, join(dir, `threads.db${side}`))
257
+ return dir
258
+ }
259
+
260
+ /** The Zed writer; `version` and `running` are for tests, which must not depend on what is installed or open. */
261
+ export function zedWriter(opts: { version?: string; running?: () => boolean } = {}): Writer {
262
+ const installed = (home: string) => opts.version ?? zedVersion(home)
263
+ const running = opts.running ?? (() => PROCESSES.some(isRunning))
264
+ return {
265
+ tool: TOOL,
266
+ label: LABEL,
267
+ way: "native",
268
+
269
+ async ready(w: WriteOptions): Promise<Readiness> {
270
+ const v = installed(w.home)
271
+ if (!v) return { ok: false, why: "Zed is not installed (no Zed.app and no `zed` on the PATH)" }
272
+ if (!TESTED_VERSIONS.includes(v)) return { ok: false, why: `Zed ${v} is not a version this writer was checked against (${TESTED_VERSIONS.join(", ")})` }
273
+ if (running()) return { ok: false, why: "Zed is running; quit it first, since it keeps threads.db open and caches its thread list" }
274
+ if (!isDir(w.cwd)) return { ok: false, why: `${w.cwd} is not a folder; Zed files the thread under the project it belongs to` }
275
+ return { ok: true }
276
+ },
277
+
278
+ async write(session: Session, w: WriteOptions): Promise<WriteResult> {
279
+ const now = new Date()
280
+ const cwd = realCwd(w.cwd)
281
+ const id = crypto.randomUUID()
282
+ const path = threadsDb(w.home)
283
+ const plan = planThread(session, { now })
284
+ const written = [path]
285
+ if (!w.dryRun) {
286
+ if (running()) throw new Error("Zed is running; quit it before writing its threads database")
287
+ if (exists(path)) written.push(backupDb(w.home, path, now))
288
+ else mkdirSync(dirname(path), { recursive: true })
289
+ const json = JSON.stringify(plan.thread)
290
+ const data = Bun.zstdCompressSync(Buffer.from(json), { level: 3 })
291
+ const db = new Database(path)
292
+ try {
293
+ db.exec("PRAGMA busy_timeout = 5000")
294
+ for (const sql of SCHEMA) {
295
+ try {
296
+ db.exec(sql)
297
+ } catch {} // the column is already there, as Zed itself tolerates
298
+ }
299
+ // A plain INSERT: a new thread never replaces a row, even in the impossible case of an id clash.
300
+ db.query(
301
+ "INSERT INTO threads (id, parent_id, folder_paths, folder_paths_order, summary, updated_at, data_type, data, created_at) VALUES (?, NULL, ?, ?, ?, ?, 'zstd', ?, ?)",
302
+ ).run(id, cwd, "0", plan.title, rfc3339(now), data, rfc3339(now))
303
+ } finally {
304
+ db.close()
305
+ }
306
+ // Read it back as Zed's own rows are read; a row that does not round-trip is removed.
307
+ let problem: string | undefined
308
+ try {
309
+ const back = await zedReader.read({ tool: TOOL, sourceId: id, path }, w.home)
310
+ const n = countTurns(back)
311
+ const pairs = unpaired(back)
312
+ if (n.user !== plan.expect.users || n.assistant !== plan.expect.assistants || n.tool_call !== plan.expect.calls || pairs.calls.length || pairs.results.length)
313
+ problem = `users ${n.user}/${plan.expect.users}, replies ${n.assistant}/${plan.expect.assistants}, tool calls ${n.tool_call}/${plan.expect.calls}, unpaired ${pairs.calls.length + pairs.results.length}`
314
+ } catch (e) {
315
+ problem = e instanceof Error ? e.message : String(e)
316
+ }
317
+ if (problem) {
318
+ const db2 = new Database(path)
319
+ try {
320
+ db2.query("DELETE FROM threads WHERE id = ?").run(id)
321
+ } finally {
322
+ db2.close()
323
+ }
324
+ throw new Error(`the new Zed thread did not read back as written (${problem}); it was removed`)
325
+ }
326
+ }
327
+ return {
328
+ tool: TOOL,
329
+ way: "native",
330
+ sessionId: id,
331
+ written,
332
+ open: `zed ${shellQuote(cwd)}`,
333
+ note: `In the Agent Panel, open the thread "${plan.title}" from the sidebar or Thread History.`,
334
+ lost: plan.lost,
335
+ dryRun: !!w.dryRun,
336
+ }
337
+ },
338
+ }
339
+ }
340
+
341
+ export const zedNativeWriter = zedWriter()