@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,323 @@
1
+ import type { Database } from "bun:sqlite"
2
+ import { exists, gitInfo } from "../../fsutil.ts"
3
+ import { keepImage, parseDataUrl } from "../../session/attachments.ts"
4
+ import { withSnapshot } from "../../sqlite.ts"
5
+ import type { Attachment, Session, Turn } from "../../session/types.ts"
6
+ import { asText, clip, header, iso, lossCounter, noteModel } from "../../session/util.ts"
7
+ import { CONTINUED_NOTE } from "../opencode/note.ts"
8
+ import type { Reader, SessionRef } from "../types.ts"
9
+ import { LABEL, TOOL, mastraToolKind, metadataOf, storePaths, type Part } from "./records.ts"
10
+
11
+ // Reads MastraCode threads from its one store (records.ts), always from a copy taken with its write-ahead
12
+ // log, so a running MastraCode is never disturbed and the newest committed turns are included.
13
+ // Semantics ported from session-migrate's mastracode.py (MIT): a "signal" row of type user is the person's
14
+ // prompt; an assistant row holds every step of a reply, with each tool call and its result folded into one
15
+ // tool-invocation part (unfolded here into a call and a result); readable reasoning is kept; image file parts
16
+ // become attachments; the thread's active observational memory is its compaction. MastraCode's runtime parts
17
+ // (workspace and sandbox metadata) are counted as lost; step boundaries are structure, not content.
18
+
19
+ /** The stores on this machine: the platform's path, and the others when present (a home copied between systems). */
20
+ const stores = (home: string) => [...new Set(Object.values(storePaths(home)))].filter(exists)
21
+
22
+ export function listThreads(d: Database, path: string): SessionRef[] {
23
+ const rows = d
24
+ .query(
25
+ `SELECT t.id, t.resourceId, t.title, t.metadata, t.updatedAt FROM mastra_threads t
26
+ WHERE EXISTS (SELECT 1 FROM mastra_messages m WHERE m.thread_id = t.id)`,
27
+ )
28
+ .all() as Array<Record<string, any>>
29
+ return rows.map((r) => {
30
+ const cwd = metadataOf(r.metadata).projectPath
31
+ const updatedAt = iso(r.updatedAt)
32
+ return {
33
+ tool: TOOL,
34
+ sourceId: String(r.id),
35
+ path,
36
+ ...(r.title ? { title: String(r.title) } : {}),
37
+ ...(typeof cwd === "string" && cwd ? { cwd } : {}),
38
+ ...(updatedAt ? { updatedAt } : {}),
39
+ extra: { resourceId: String(r.resourceId ?? "") },
40
+ }
41
+ })
42
+ }
43
+
44
+ export const mastracodeReader: Reader = {
45
+ tool: TOOL,
46
+ label: LABEL,
47
+ async list(home) {
48
+ const refs: SessionRef[] = []
49
+ for (const db of stores(home)) {
50
+ try {
51
+ refs.push(...withSnapshot(db, (d) => listThreads(d, db)))
52
+ } catch {
53
+ // A store that cannot be copied or opened lists nothing; read() says why for a thread in it.
54
+ }
55
+ }
56
+ return refs.sort((a, b) => (b.updatedAt ?? "").localeCompare(a.updatedAt ?? ""))
57
+ },
58
+ async read(ref, home) {
59
+ if (!exists(ref.path)) throw new Error(`MastraCode store not found: ${ref.path}`)
60
+ return withSnapshot(ref.path, (d) => readMastra(d, ref.sourceId, ref.path, home))
61
+ },
62
+ }
63
+
64
+ const json = (v: unknown): any => {
65
+ try {
66
+ return JSON.parse(v instanceof Uint8Array ? new TextDecoder().decode(v) : String(v))
67
+ } catch {
68
+ return undefined
69
+ }
70
+ }
71
+
72
+ /** The prefix brain's writer puts before a compaction summary it stores as a user row. */
73
+ export const SUMMARY_PREFIX = /^\[Summary of the earlier conversation in [^\]\n]*\]\n\n/
74
+
75
+ /** A tool result's text: strings as they are, text blocks joined (MCP and AI SDK shapes), else JSON. */
76
+ function resultText(v: unknown): { text: string; images: number } {
77
+ if (typeof v === "string") return { text: v, images: 0 }
78
+ const blocks = Array.isArray(v) ? v : v && typeof v === "object" && Array.isArray((v as any).content) ? (v as any).content : undefined
79
+ // session-migrate's wrapper for a result whose text and structured content differ.
80
+ if (v && typeof v === "object" && (v as any).schema === "session-migrate.mastracode-tool-result.v1" && typeof (v as any).text === "string") return { text: (v as any).text, images: 0 }
81
+ if (v && typeof v === "object" && !Array.isArray(v) && (v as any).type === "text" && typeof (v as any).value === "string") return { text: (v as any).value, images: 0 }
82
+ if (!blocks) return { text: asText(v), images: 0 }
83
+ const parts: string[] = []
84
+ let images = 0
85
+ for (const b of blocks as any[]) {
86
+ if (typeof b === "string") parts.push(b)
87
+ else if (typeof b?.text === "string") parts.push(b.text)
88
+ else if (b?.type === "image" || b?.type === "media") images++
89
+ else parts.push(asText(b))
90
+ }
91
+ return { text: parts.join("\n"), images }
92
+ }
93
+
94
+ const resultFailed = (v: unknown) => !!v && typeof v === "object" && !Array.isArray(v) && (v as any).isError === true
95
+
96
+ /** One thread from an open MastraCode store. */
97
+ export function readMastra(d: Database, id: string, path: string, home: string): Session {
98
+ const thread = d.query("SELECT id, resourceId, title, metadata, createdAt, updatedAt FROM mastra_threads WHERE id = ?").get(id) as Record<string, any> | null
99
+ if (!thread) throw new Error(`no MastraCode thread ${id} in ${path}`)
100
+ const meta = metadataOf(thread.metadata)
101
+ const cwd = typeof meta.projectPath === "string" && meta.projectPath ? (meta.projectPath as string) : undefined
102
+ const h = header(TOOL, id, {
103
+ sourcePath: path,
104
+ ...(thread.title ? { title: String(thread.title) } : {}),
105
+ project: cwd ? { cwd, ...gitInfo(cwd, home) } : {},
106
+ ...(iso(thread.createdAt) ? { startedAt: iso(thread.createdAt)! } : {}),
107
+ ...(iso(thread.updatedAt) ? { updatedAt: iso(thread.updatedAt)! } : {}),
108
+ })
109
+ if (typeof meta.brain?.continues === "string") h.parent = { id: meta.brain.continues, kind: "continued" }
110
+ const threadModel = typeof meta.currentModelId === "string" && meta.currentModelId ? (meta.currentModelId as string) : undefined
111
+ const lost = lossCounter()
112
+ const turns: Turn[] = []
113
+ const callIds = new Set<string>()
114
+
115
+ // The thread's observational memory, MastraCode's own compaction: what it currently uses in place of older turns.
116
+ try {
117
+ const om = d.query("SELECT activeObservations FROM mastra_observational_memory WHERE threadId = ? ORDER BY updatedAt DESC LIMIT 1").get(id) as { activeObservations?: unknown } | null
118
+ const summary = typeof om?.activeObservations === "string" ? om.activeObservations.trim() : ""
119
+ if (summary) turns.push({ kind: "compaction", ...(h.startedAt ? { at: h.startedAt } : {}), summary })
120
+ } catch {
121
+ // No observational memory table (it is created when MastraCode first observes) or another layout.
122
+ }
123
+
124
+ const rows = d.query("SELECT id, content, role, type, createdAt FROM mastra_messages WHERE thread_id = ? ORDER BY createdAt, id").all(id) as Array<Record<string, any>>
125
+ for (const row of rows) {
126
+ const rowAt = iso(row.createdAt)
127
+ let content = json(row.content)
128
+ // Older Mastra rows hold the message's content directly: a string, or a list of AI SDK content parts.
129
+ if (typeof content === "string") content = { format: 2, parts: [{ type: "text", text: content }] }
130
+ else if (Array.isArray(content)) content = { format: 2, parts: content }
131
+ if (!content || typeof content !== "object" || !Array.isArray(content.parts)) {
132
+ lost.add("unreadable messages")
133
+ continue
134
+ }
135
+ const cmeta = (content.metadata && typeof content.metadata === "object" ? content.metadata : {}) as Record<string, any>
136
+ if (row.id === "om-continuation" || cmeta.sessionMigrate?.internal === true) {
137
+ lost.add("observational-memory continuation messages")
138
+ continue
139
+ }
140
+ const role = String(row.role)
141
+ if (role === "system") {
142
+ lost.add("system messages")
143
+ continue
144
+ }
145
+ if (role === "signal") {
146
+ const kind = String(cmeta.signal?.type ?? row.type ?? "")
147
+ if (kind !== "user") {
148
+ lost.add(`signals the tool injected (${kind || "untyped"})`)
149
+ continue
150
+ }
151
+ } else if (role !== "user" && role !== "assistant" && role !== "tool") {
152
+ lost.add(`unknown message roles: ${role}`)
153
+ continue
154
+ }
155
+ const parts = content.parts as Part[]
156
+ const textOf = (p: Part) => (typeof p.text === "string" ? p.text : "")
157
+
158
+ // A compaction summary stored as a tagged user row (by brain, or by session-migrate).
159
+ if (cmeta.brain?.kind === "compaction" || cmeta.sessionMigrate?.kind === "compaction") {
160
+ const summary = parts.filter((p) => p?.type === "text").map(textOf).join("\n\n").replace(SUMMARY_PREFIX, "")
161
+ if (summary.trim()) turns.push({ kind: "compaction", ...(rowAt ? { at: rowAt } : {}), summary })
162
+ continue
163
+ }
164
+
165
+ const user = role === "user" || role === "signal"
166
+ const model =
167
+ (typeof cmeta.modelId === "string" && cmeta.modelId) ||
168
+ (parts.find((p) => p?.type === "step-start" && typeof p.model === "string")?.model as string | undefined) ||
169
+ (user ? undefined : threadModel) ||
170
+ undefined
171
+ // A part's own time when it has one (tool invocations have none: they take the time of the part before).
172
+ let lastAt = rowAt
173
+ const stampOf = (p: Part) => {
174
+ const at = typeof p.createdAt === "number" ? iso(p.createdAt) : undefined
175
+ if (at) lastAt = at
176
+ return lastAt
177
+ }
178
+
179
+ // The person's prompt: its texts joined, with the images pasted into it.
180
+ let userText: string[] = []
181
+ let attachments: Attachment[] = []
182
+ let userAt: string | undefined
183
+ const flushUser = () => {
184
+ const text = userText.join("\n\n")
185
+ const note = text.match(CONTINUED_NOTE)
186
+ if (note) h.parent ??= { id: note[1]!, kind: "continued" }
187
+ else if (text.trim() || attachments.length) turns.push({ kind: "user", ...(userAt ? { at: userAt } : {}), text, ...(attachments.length ? { attachments } : {}) })
188
+ userText = []
189
+ attachments = []
190
+ userAt = undefined
191
+ }
192
+
193
+ const call = (p: Part, callId: string, name: string, input: unknown) => {
194
+ let cid = callId
195
+ for (let n = 2; callIds.has(cid); n++) cid = `${callId}#${n}`
196
+ callIds.add(cid)
197
+ const at = stampOf(p)
198
+ turns.push({ kind: "tool_call", ...(at ? { at } : {}), id: cid, name, toolKind: mastraToolKind(name), input: input && typeof input === "object" ? input : {} })
199
+ return cid
200
+ }
201
+ const result = (p: Part, callId: string, ok: boolean, value: unknown) => {
202
+ const { text, images } = resultText(value)
203
+ if (images) lost.add("images in tool results", images)
204
+ const out = clip(text)
205
+ const at = stampOf(p)
206
+ turns.push({ kind: "tool_result", ...(at ? { at } : {}), callId, ok, output: out.text, ...(out.truncated ? { truncated: true } : {}) })
207
+ }
208
+
209
+ for (const p of parts) {
210
+ const type = String(p?.type ?? "")
211
+ if (type === "text") {
212
+ const text = textOf(p)
213
+ if (user) {
214
+ userAt ??= stampOf(p)
215
+ userText.push(text)
216
+ } else if (text.trim()) {
217
+ noteModel(h, model)
218
+ const at = stampOf(p)
219
+ turns.push({ kind: "assistant", ...(at ? { at } : {}), text, ...(model ? { model } : {}) })
220
+ }
221
+ continue
222
+ }
223
+ if (type === "file" || type === "image") {
224
+ const mime = String(p.mimeType ?? p.mediaType ?? "")
225
+ const raw = type === "image" ? p.image : (p.data ?? p.url)
226
+ const data = typeof raw === "string" ? (parseDataUrl(raw)?.base64 ?? (raw.startsWith("data:") || /^https?:/.test(raw) ? undefined : raw)) : undefined
227
+ if (!mime.startsWith("image/")) {
228
+ lost.add("file attachments")
229
+ continue
230
+ }
231
+ const kept = data ? keepImage(data, mime) : undefined
232
+ if (!kept) lost.add("images")
233
+ else if (!user) lost.add("images in replies")
234
+ else {
235
+ userAt ??= stampOf(p)
236
+ attachments.push(kept)
237
+ }
238
+ continue
239
+ }
240
+ if (type === "data-user-message" && p.data && typeof p.data === "object") {
241
+ const text = typeof p.data.message === "string" ? p.data.message : typeof p.data.content === "string" ? p.data.content : ""
242
+ if (text) {
243
+ if (!user) flushUser()
244
+ userAt ??= stampOf(p)
245
+ userText.push(text)
246
+ if (!user) flushUser()
247
+ continue
248
+ }
249
+ }
250
+ if (type === "reasoning") {
251
+ const details = Array.isArray(p.details) ? (p.details as any[]) : []
252
+ const text =
253
+ (typeof p.reasoning === "string" && p.reasoning) ||
254
+ (typeof p.text === "string" && p.text) ||
255
+ details
256
+ .filter((x) => x?.type === "text" && typeof x.text === "string")
257
+ .map((x) => x.text as string)
258
+ .join("\n\n")
259
+ const signed = details.some((x) => x?.signature) || /signature|encrypted/i.test(JSON.stringify(p.providerMetadata ?? {}))
260
+ if (text.trim()) {
261
+ const at = stampOf(p)
262
+ turns.push({ kind: "reasoning", ...(at ? { at } : {}), text })
263
+ if (signed) lost.add("reasoning signatures")
264
+ } else if (details.some((x) => x?.type === "redacted")) lost.add("redacted reasoning")
265
+ else if (signed) lost.add("signed reasoning")
266
+ continue
267
+ }
268
+ // A tool call with its result folded in (AI SDK v4 UI part), as MastraCode stores it.
269
+ if (type === "tool-invocation" && p.toolInvocation && typeof p.toolInvocation === "object") {
270
+ const inv = p.toolInvocation as Record<string, any>
271
+ const name = typeof inv.toolName === "string" && inv.toolName ? inv.toolName : "unknown"
272
+ if (typeof inv.toolCallId !== "string" || !inv.toolCallId) {
273
+ lost.add("tool calls without an id")
274
+ continue
275
+ }
276
+ if (user) flushUser()
277
+ const cid = call(p, inv.toolCallId, name, inv.args)
278
+ if (inv.state === "result" && "result" in inv) result(p, cid, inv.isError !== true && !resultFailed(inv.result), inv.result)
279
+ else if (inv.state === "output-error") result(p, cid, false, typeof inv.errorText === "string" ? inv.errorText : asText(inv.errorText))
280
+ continue
281
+ }
282
+ // AI SDK v5 tool parts ("tool-<name>", "dynamic-tool") and model-message tool-call / tool-result parts.
283
+ if ((type.startsWith("tool-") && typeof p.toolCallId === "string" && type !== "tool-result" && type !== "tool-call") || type === "dynamic-tool") {
284
+ const name = type === "dynamic-tool" ? String(p.toolName ?? "unknown") : type.slice(5)
285
+ if (user) flushUser()
286
+ const cid = call(p, String(p.toolCallId), name, p.input)
287
+ if (p.state === "output-available") result(p, cid, !resultFailed(p.output), p.output)
288
+ else if (p.state === "output-error") result(p, cid, false, typeof p.errorText === "string" ? p.errorText : asText(p.errorText))
289
+ continue
290
+ }
291
+ if (type === "tool-call" && typeof p.toolCallId === "string") {
292
+ if (user) flushUser()
293
+ call(p, p.toolCallId, String(p.toolName ?? "unknown"), p.args ?? p.input)
294
+ continue
295
+ }
296
+ if (type === "tool-result" && typeof p.toolCallId === "string") {
297
+ if (user) flushUser()
298
+ const value = "result" in p ? p.result : p.output
299
+ result(p, p.toolCallId, p.isError !== true && !resultFailed(value), value)
300
+ continue
301
+ }
302
+ // Step boundaries carry the model, which is already read; no content.
303
+ if (type === "step-start") continue
304
+ if (type.startsWith("data-")) {
305
+ lost.add(`runtime parts: ${type}`)
306
+ continue
307
+ }
308
+ if (type === "source" || type === "source-url" || type === "source-document") {
309
+ lost.add("cited sources")
310
+ continue
311
+ }
312
+ lost.add(`unknown part types: ${type || "untyped"}`)
313
+ }
314
+ if (user) flushUser()
315
+ else if (!parts.some((p) => p?.type === "text") && typeof content.content === "string" && content.content.trim()) {
316
+ // A reply that kept its text only beside the parts.
317
+ noteModel(h, model)
318
+ turns.push({ kind: "assistant", ...(rowAt ? { at: rowAt } : {}), text: content.content, ...(model ? { model } : {}) })
319
+ }
320
+ }
321
+ lost.flush(h)
322
+ return { header: h, turns }
323
+ }
@@ -0,0 +1,336 @@
1
+ import { createHash } from "node:crypto"
2
+ import { readFileSync, realpathSync } from "node:fs"
3
+ import { basename, dirname, join, resolve } from "node:path"
4
+ import { appSupport, exists, isDir, isRunning, readJson, versionOf } from "../../fsutil.ts"
5
+ import type { ToolKind } from "../../session/types.ts"
6
+ import { toolKindOf } from "../../session/util.ts"
7
+
8
+ // Mastra Code (the `mastracode` CLI, npm `mastracode`): where its store is and what its rows look like.
9
+ // Ported from session-migrate (xhluca/session-migrate, MIT licence): docs/mastracode-format.md and
10
+ // src/session_migrate/formats/mastracode.py, which were checked against mastracode@0.37.1 (source commit
11
+ // 003e7574, @mastra/core 1.63.2, @mastra/libsql 1.22.2, @mastra/memory 1.28.1), and against the store the
12
+ // exact 0.37.1 CLI wrote in test/fixtures/native-corpus/mastracode.
13
+ //
14
+ // Every local conversation lives in one LibSQL (SQLite) file, mastra.db: a thread is a mastra_threads row
15
+ // (id a UUID, resourceId the project identity it is scoped to, title, metadata as SQLite JSONB holding
16
+ // projectPath and the thread's model), a message a mastra_messages row whose content is JSON
17
+ // {format: 2, parts: [...]}: AI SDK UI parts (text, reasoning, tool-invocation with the call and its result
18
+ // folded together, file) plus MastraCode's own runtime parts (step-start, data-*). What the person typed is a
19
+ // "signal" row of type "user"; the model's reply to one prompt, every step of it, is one "assistant" row of
20
+ // type "v2". Observational memory (its own compaction) is in mastra_observational_memory.
21
+ //
22
+ // Only the local file store is read and written. MastraCode can also use a remote LibSQL (MASTRA_DB_URL) or
23
+ // PostgreSQL store; those are never touched. The path overrides MASTRA_DB_PATH, MASTRA_APP_DATA_DIR and
24
+ // XDG_DATA_HOME are not followed (no other adapter follows its tool's overrides); the writer refuses when one
25
+ // would make MastraCode read another store than the one written.
26
+
27
+ export const TOOL = "mastracode"
28
+ export const LABEL = "Mastra Code"
29
+ export const TESTED_VERSIONS = ["0.37.1"]
30
+
31
+ /** The store on each platform, as MastraCode's app-data path puts it (without the env overrides). */
32
+ export const storePaths = (home: string) => ({
33
+ darwin: join(appSupport(home), "mastracode", "mastra.db"),
34
+ linux: join(home, ".local", "share", "mastracode", "mastra.db"),
35
+ win32: join(home, "AppData", "Roaming", "mastracode", "mastra.db"),
36
+ })
37
+
38
+ /** The store MastraCode uses on this platform. */
39
+ export function storePath(home: string, platform: string = process.platform): string {
40
+ const p = storePaths(home)
41
+ return platform === "darwin" ? p.darwin : platform === "win32" ? p.win32 : p.linux
42
+ }
43
+
44
+ /** The schema a 0.37.1 store's two conversation tables have (MastraCode creates the rest itself on launch). */
45
+ export const SCHEMA = `
46
+ CREATE TABLE IF NOT EXISTS "mastra_threads" ("id" TEXT NOT NULL PRIMARY KEY,"resourceId" TEXT NOT NULL,"title" TEXT NOT NULL,"metadata" TEXT,"createdAt" TEXT NOT NULL,"updatedAt" TEXT NOT NULL);
47
+ CREATE TABLE IF NOT EXISTS "mastra_messages" ("id" TEXT NOT NULL PRIMARY KEY,"thread_id" TEXT NOT NULL,"content" TEXT NOT NULL,"role" TEXT NOT NULL,"type" TEXT NOT NULL,"createdAt" TEXT NOT NULL,"resourceId" TEXT);
48
+ CREATE INDEX IF NOT EXISTS "mastra_messages_thread_created" ON "mastra_messages"("thread_id","createdAt","id");
49
+ `
50
+
51
+ export const COLUMNS = {
52
+ mastra_threads: ["id", "resourceId", "title", "metadata", "createdAt", "updatedAt"],
53
+ mastra_messages: ["id", "thread_id", "content", "role", "type", "createdAt", "resourceId"],
54
+ }
55
+
56
+ /** One message part as stored (AI SDK UI parts and MastraCode's own). */
57
+ export type Part = { type: string; [k: string]: any }
58
+
59
+ /** A mastra_messages row's content. */
60
+ export interface Content {
61
+ format: 2
62
+ parts: Part[]
63
+ /** The reply's text, beside its parts (assistant rows). */
64
+ content?: string
65
+ metadata?: Record<string, any>
66
+ }
67
+
68
+ /** A mastra_messages row before it is inserted. */
69
+ export interface MessageRow {
70
+ id: string
71
+ role: "signal" | "user" | "assistant"
72
+ type: "user" | "v2"
73
+ content: Content
74
+ createdAt: string
75
+ }
76
+
77
+ export interface ThreadRow {
78
+ id: string
79
+ resourceId: string
80
+ title: string
81
+ metadata: Record<string, unknown>
82
+ createdAt: string
83
+ updatedAt: string
84
+ }
85
+
86
+ /** What a MastraCode tool does: execute_command is the shell, view reads a file, and so on. */
87
+ export function mastraToolKind(name: string): ToolKind {
88
+ const short = name.replace(/^mastra_workspace_/, "")
89
+ if (short === "view") return "read"
90
+ if (short === "execute_command") return "shell"
91
+ return toolKindOf(short)
92
+ }
93
+
94
+ // --- SQLite JSONB --------------------------------------------------------------------------------------
95
+ // mastra_threads.metadata is SQLite's binary JSON, not JSON text. A small codec, so neither reading nor
96
+ // writing needs SQLite 3.45's json functions. Each element: a header byte (low nibble the type, high nibble
97
+ // the payload size, or 12-15 for a 1/2/4/8-byte big-endian size after it), then the payload.
98
+
99
+ const utf8 = new TextDecoder("utf-8", { fatal: true })
100
+ const bytes = (s: string) => new TextEncoder().encode(s)
101
+
102
+ /** The value a JSONB blob holds; throws when it is not well-formed JSONB. */
103
+ export function decodeJsonb(b: Uint8Array): unknown {
104
+ const [v, end] = jsonbElement(b, 0, 0)
105
+ if (end !== b.length) throw new Error("trailing bytes after the JSONB value")
106
+ return v
107
+ }
108
+
109
+ function jsonbElement(b: Uint8Array, at: number, depth: number): [unknown, number] {
110
+ if (depth > 96 || at >= b.length) throw new Error("malformed JSONB")
111
+ const code = b[at]! >> 4
112
+ const type = b[at]! & 0x0f
113
+ let size = code
114
+ let head = 1
115
+ if (code >= 12) {
116
+ const n = [1, 2, 4, 8][code - 12]!
117
+ if (at + 1 + n > b.length) throw new Error("truncated JSONB header")
118
+ size = 0
119
+ for (let i = 0; i < n; i++) size = size * 256 + b[at + 1 + i]!
120
+ head = 1 + n
121
+ }
122
+ const start = at + head
123
+ const end = start + size
124
+ if (end > b.length) throw new Error("truncated JSONB payload")
125
+ const text = () => utf8.decode(b.subarray(start, end))
126
+ switch (type) {
127
+ case 0:
128
+ return [null, end]
129
+ case 1:
130
+ return [true, end]
131
+ case 2:
132
+ return [false, end]
133
+ case 3: // INT
134
+ case 5: // FLOAT
135
+ case 6: // FLOAT5
136
+ return [Number(text()), end]
137
+ case 4: {
138
+ // INT5: JSON5 integers, hex included
139
+ const t = text()
140
+ const neg = t.startsWith("-")
141
+ const n = Number(t.replace(/^[+-]/, ""))
142
+ return [neg ? -n : n, end]
143
+ }
144
+ case 7: // TEXT: nothing to unescape
145
+ case 10: // TEXTRAW
146
+ return [text(), end]
147
+ case 8: // TEXTJ: JSON escapes
148
+ case 9: {
149
+ // TEXT5: JSON5 escapes, read as JSON when they are
150
+ const t = text()
151
+ try {
152
+ return [JSON.parse(`"${t}"`), end]
153
+ } catch {
154
+ return [t, end]
155
+ }
156
+ }
157
+ case 11: {
158
+ const out: unknown[] = []
159
+ let p = start
160
+ while (p < end) {
161
+ const [v, next] = jsonbElement(b, p, depth + 1)
162
+ out.push(v)
163
+ p = next
164
+ }
165
+ if (p !== end) throw new Error("malformed JSONB array")
166
+ return [out, end]
167
+ }
168
+ case 12: {
169
+ const out: Record<string, unknown> = {}
170
+ let p = start
171
+ while (p < end) {
172
+ const [k, afterKey] = jsonbElement(b, p, depth + 1)
173
+ if (typeof k !== "string") throw new Error("JSONB object key is not text")
174
+ const [v, next] = jsonbElement(b, afterKey, depth + 1)
175
+ out[k] = v
176
+ p = next
177
+ }
178
+ if (p !== end) throw new Error("malformed JSONB object")
179
+ return [out, end]
180
+ }
181
+ default:
182
+ throw new Error(`reserved JSONB element type ${type}`)
183
+ }
184
+ }
185
+
186
+ function jsonbHeader(type: number, size: number): number[] {
187
+ if (size <= 11) return [(size << 4) | type]
188
+ if (size <= 0xff) return [0xc0 | type, size]
189
+ if (size <= 0xffff) return [0xd0 | type, size >> 8, size & 0xff]
190
+ return [0xe0 | type, (size >>> 24) & 0xff, (size >>> 16) & 0xff, (size >>> 8) & 0xff, size & 0xff]
191
+ }
192
+
193
+ function jsonbParts(v: unknown, out: number[][], depth: number): void {
194
+ if (depth > 96) throw new Error("metadata nests too deeply for JSONB")
195
+ const leaf = (type: number, payload: Uint8Array): void => void out.push([...jsonbHeader(type, payload.length), ...payload])
196
+ if (v === null || v === undefined) return void out.push(jsonbHeader(0, 0))
197
+ if (v === true) return void out.push(jsonbHeader(1, 0))
198
+ if (v === false) return void out.push(jsonbHeader(2, 0))
199
+ if (typeof v === "number") {
200
+ if (!Number.isFinite(v)) return void out.push(jsonbHeader(0, 0))
201
+ return leaf(Number.isSafeInteger(v) ? 3 : 5, bytes(Number.isSafeInteger(v) ? String(v) : JSON.stringify(v)))
202
+ }
203
+ if (typeof v === "string") {
204
+ const escaped = JSON.stringify(v).slice(1, -1)
205
+ return leaf(escaped === v ? 7 : 8, bytes(escaped))
206
+ }
207
+ const children: number[][] = []
208
+ if (Array.isArray(v)) for (const item of v) jsonbParts(item, children, depth + 1)
209
+ else if (typeof v === "object")
210
+ for (const [k, item] of Object.entries(v as Record<string, unknown>)) {
211
+ if (item === undefined) continue
212
+ jsonbParts(k, children, depth + 1)
213
+ jsonbParts(item, children, depth + 1)
214
+ }
215
+ else throw new Error(`cannot store a ${typeof v} in JSONB`)
216
+ const size = children.reduce((n, c) => n + c.length, 0)
217
+ out.push(jsonbHeader(Array.isArray(v) ? 11 : 12, size))
218
+ out.push(...children)
219
+ }
220
+
221
+ /** A value as SQLite JSONB, the way MastraCode stores a thread's metadata. */
222
+ export function encodeJsonb(v: unknown): Uint8Array {
223
+ const out: number[][] = []
224
+ jsonbParts(v, out, 0)
225
+ return Uint8Array.from(out.flat())
226
+ }
227
+
228
+ /** A thread's metadata, whether it is JSONB (as MastraCode writes it), JSON text, or missing. */
229
+ export function metadataOf(v: unknown): Record<string, any> {
230
+ try {
231
+ const parsed = v instanceof Uint8Array ? decodeJsonb(v) : typeof v === "string" && v ? JSON.parse(v) : undefined
232
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? (parsed as Record<string, any>) : {}
233
+ } catch {
234
+ return {}
235
+ }
236
+ }
237
+
238
+ // --- Project identity ----------------------------------------------------------------------------------
239
+
240
+ /** The folder with symbolic links resolved, as the process working directory MastraCode sees. */
241
+ export function realDir(dir: string): string {
242
+ try {
243
+ return realpathSync(resolve(dir))
244
+ } catch {
245
+ return resolve(dir)
246
+ }
247
+ }
248
+
249
+ const normalizeGitUrl = (url: string) =>
250
+ url
251
+ .replace(/\.git$/, "")
252
+ .replace(/^git@([^:]+):/, "https://$1/")
253
+ .replace(/^ssh:\/\/git@/, "https://")
254
+ .toLowerCase()
255
+
256
+ /**
257
+ * The resource id MastraCode scopes a project's threads by (utils/project.ts, as session-migrate reproduces
258
+ * it): a slug of the repository's name and the first 12 hex digits of the SHA-256 of its normalised remote
259
+ * URL; without a remote, of the main repository's folder (a worktree) or the project folder itself. Read
260
+ * from .git without running git.
261
+ */
262
+ export function resourceIdFor(cwd: string): string {
263
+ let root = realDir(cwd)
264
+ let gitDir: string | undefined
265
+ for (let dir = root; ; dir = dirname(dir)) {
266
+ const git = join(dir, ".git")
267
+ if (exists(git)) {
268
+ try {
269
+ gitDir = isDir(git) ? git : resolve(dir, readFileSync(git, "utf8").replace(/^gitdir:\s*/, "").trim())
270
+ root = dir
271
+ } catch {}
272
+ break
273
+ }
274
+ if (dirname(dir) === dir) break
275
+ }
276
+ let url: string | undefined
277
+ let mainRepo: string | undefined
278
+ if (gitDir) {
279
+ let common = gitDir
280
+ try {
281
+ common = resolve(gitDir, readFileSync(join(gitDir, "commondir"), "utf8").trim())
282
+ } catch {}
283
+ if (common !== gitDir) mainRepo = dirname(common)
284
+ let config = ""
285
+ try {
286
+ config = readFileSync(join(common, "config"), "utf8")
287
+ } catch {}
288
+ url = config.match(/\[remote "origin"\][^[]*?url\s*=\s*(\S+)/)?.[1] ?? config.match(/\[remote "[^"]*"\][^[]*?url\s*=\s*(\S+)/)?.[1]
289
+ }
290
+ const source = url ? normalizeGitUrl(url) : (mainRepo ?? root)
291
+ const base = url ? url.replace(/\/+$/, "").split("/").pop()!.replace(/\.git$/, "") || "project" : basename(root)
292
+ const slug = base.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "")
293
+ return `${slug || "project"}-${createHash("sha256").update(source).digest("hex").slice(0, 12)}`
294
+ }
295
+
296
+ // --- The CLI -------------------------------------------------------------------------------------------
297
+
298
+ /** The mastracode binary on PATH (read now, so a PATH set after start counts). */
299
+ export const mastraBin = (): string | undefined => Bun.which("mastracode", { PATH: process.env.PATH ?? "" }) ?? undefined
300
+
301
+ /** The installed MastraCode version, from `mastracode --version`. */
302
+ export const mastraVersion = (bin = mastraBin()) => (bin ? versionOf(bin)?.match(/\d+\.\d+\.\d+/)?.[0] : undefined)
303
+
304
+ /** Why MastraCode would read another store than `store`, from the environment and its settings; undefined when it reads this one. */
305
+ export function otherStore(store: string): string | undefined {
306
+ const env = process.env
307
+ if (env.MASTRA_STORAGE_BACKEND && env.MASTRA_STORAGE_BACKEND !== "libsql") return `MASTRA_STORAGE_BACKEND=${env.MASTRA_STORAGE_BACKEND} points MastraCode at a remote store`
308
+ if (env.MASTRA_DB_URL && resolve(env.MASTRA_DB_URL.replace(/^file:(\/\/)?/, "")) !== store) return "MASTRA_DB_URL points MastraCode at another store"
309
+ if (env.MASTRA_DB_PATH && resolve(env.MASTRA_DB_PATH) !== store) return `MASTRA_DB_PATH points MastraCode at ${env.MASTRA_DB_PATH}`
310
+ if (env.MASTRA_APP_DATA_DIR && join(resolve(env.MASTRA_APP_DATA_DIR), "mastra.db") !== store) return `MASTRA_APP_DATA_DIR points MastraCode at ${env.MASTRA_APP_DATA_DIR}`
311
+ const storage = readJson<{ storage?: { backend?: unknown; libsql?: { url?: unknown } } }>(join(dirname(store), "settings.json"))?.storage
312
+ if (typeof storage?.backend === "string" && storage.backend !== "libsql") return `MastraCode's settings use the ${storage.backend} storage backend`
313
+ if (typeof storage?.libsql?.url === "string" && !storage.libsql.url.startsWith("file:")) return "MastraCode's settings use a remote LibSQL store"
314
+ return undefined
315
+ }
316
+
317
+ /** Why the store cannot be written now: a process holding it open, or MastraCode running; undefined when it is free. */
318
+ export function storeInUse(store: string): string | undefined {
319
+ const files = [store, `${store}-wal`].filter(exists)
320
+ if (files.length) {
321
+ try {
322
+ const p = Bun.spawnSync(["lsof", "-t", "--", ...files], { stdout: "pipe", stderr: "pipe", timeout: 10_000 })
323
+ const pids = p.stdout.toString().split(/\s+/).filter((pid) => pid && Number(pid) !== process.pid)
324
+ if (pids.length) return `the MastraCode store is open in another process (pid ${pids.join(", ")}); close MastraCode first`
325
+ } catch {
326
+ // No lsof: the process checks below still apply.
327
+ }
328
+ }
329
+ if (isRunning("mastracode")) return "MastraCode is running; close it first, since it holds the session store open"
330
+ try {
331
+ const p = Bun.spawnSync(["pgrep", "-f", "mastracode/dist/cli|(^|/)mastracode( |$)"], { stdout: "pipe", stderr: "pipe" })
332
+ const pids = p.stdout.toString().split(/\s+/).filter((pid) => pid && Number(pid) !== process.pid)
333
+ if (pids.length) return "MastraCode is running; close it first, since it holds the session store open"
334
+ } catch {}
335
+ return undefined
336
+ }