@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,385 @@
1
+ import { Database } from "bun:sqlite"
2
+ import { randomUUID } from "node:crypto"
3
+ import { chmodSync, copyFileSync, mkdirSync, rmSync } from "node:fs"
4
+ import { basename, dirname, join } from "node:path"
5
+ import { toolLabel } from "../../brief.ts"
6
+ import { exists, isDir } from "../../fsutil.ts"
7
+ import { imageDataUrl, parseDataUrl } from "../../session/attachments.ts"
8
+ import { withSnapshot } from "../../sqlite.ts"
9
+ import type { Session, Turn } from "../../session/types.ts"
10
+ import { countTurns, unpaired } from "../../session/util.ts"
11
+ import { continuedNote } from "../opencode/note.ts"
12
+ import { continuedTitle, sh } from "../opencode/writer.ts"
13
+ import type { Readiness, WriteOptions, WriteResult, Writer } from "../types.ts"
14
+ import { readMastra } from "./reader.ts"
15
+ import {
16
+ COLUMNS,
17
+ LABEL,
18
+ SCHEMA,
19
+ TESTED_VERSIONS,
20
+ TOOL,
21
+ type Content,
22
+ type MessageRow,
23
+ type Part,
24
+ type ThreadRow,
25
+ encodeJsonb,
26
+ mastraBin,
27
+ mastraVersion,
28
+ otherStore,
29
+ realDir,
30
+ resourceIdFor,
31
+ storeInUse,
32
+ storePath,
33
+ } from "./records.ts"
34
+
35
+ // Continues a portable session in MastraCode as a new thread of its store, with rows shaped like the ones the
36
+ // 0.37.1 CLI writes (test/fixtures/native-corpus/mastracode) and the semantics of session-migrate's
37
+ // mastracode.py (MIT): each prompt a "signal" row of type user; each reply one assistant row of type v2 with
38
+ // all its steps (a step-start part between them), every tool call folded together with its result into one
39
+ // tool-invocation part (state "result", or "output-error" for a failure, as MastraCode stores one); readable
40
+ // reasoning as a reasoning part; pasted images as file parts with their base64 bytes. A compaction is a
41
+ // tagged user row, not MastraCode's observational memory: 0.37.1 leaves historical system rows out of the
42
+ // next request, and a user row is replayed. The note saying where the session came from is a user row too.
43
+ //
44
+ // The thread is scoped to the project's resource id (records.ts) so MastraCode finds it from that folder.
45
+ // The store is SQLite that MastraCode holds open while it runs: the write needs it closed, backs the store up
46
+ // first, and inserts in one IMMEDIATE transaction that reads the thread back before committing. A missing
47
+ // store is created with just the two conversation tables, which MastraCode initialises its other tables around.
48
+
49
+ const NO_RESULT = "No result was recorded for this call; the session moved before it finished."
50
+ const SAFE_ID = /^[A-Za-z0-9_-]{1,64}$/
51
+
52
+ export interface MastraPlan {
53
+ thread: ThreadRow
54
+ messages: MessageRow[]
55
+ lost: string[]
56
+ /** What a read-back must find. */
57
+ expect: { users: number; assistants: number; calls: number; reasoning: number; compactions: number }
58
+ }
59
+
60
+ /** The rows of a new MastraCode thread continuing `session`. Pure apart from reading attached images. */
61
+ export function planMastra(session: Session, opts: { id: string; cwd: string; resourceId: string; now: Date }): MastraPlan {
62
+ const drops = new Map<string, number>()
63
+ const drop = (what: string, n = 1) => drops.set(what, (drops.get(what) ?? 0) + n)
64
+ const label = toolLabel(session.header.tool)
65
+ const turns = session.turns
66
+ const expect = { users: 0, assistants: 0, calls: 0, reasoning: 0, compactions: 0 }
67
+
68
+ // Times are ISO strings with milliseconds; MastraCode orders a thread's rows by them, so they only go forward.
69
+ const known = turns.map((t) => (t.at ? Date.parse(t.at) : Number.NaN)).find(Number.isFinite)
70
+ let clock = (Date.parse(session.header.startedAt ?? "") || known || opts.now.getTime() - turns.length - 10) - 1
71
+ const tick = (at?: string) => {
72
+ const t = at ? Date.parse(at) : Number.NaN
73
+ clock = Number.isFinite(t) && t > clock ? t : clock + 1
74
+ return clock
75
+ }
76
+
77
+ // Each call's first result, wherever it comes: MastraCode keeps the two in one part.
78
+ const results = new Map<string, Extract<Turn, { kind: "tool_result" }>>()
79
+ for (const t of turns) if (t.kind === "tool_result" && !results.has(t.callId)) results.set(t.callId, t)
80
+
81
+ // Tool call ids and names as providers take them; others are mapped once, the same way everywhere.
82
+ const ids = new Map<string, string>()
83
+ const used = new Set<string>()
84
+ let remapped = 0
85
+ const callId = (id: string) => {
86
+ let v = ids.get(id)
87
+ if (v) return v
88
+ const base = SAFE_ID.test(id) ? id : `call_${id.replace(/[^A-Za-z0-9]/g, "").slice(-40) || "x"}`
89
+ v = base
90
+ for (let n = 2; used.has(v); n++) v = `${base}_${n}`
91
+ ids.set(id, v)
92
+ used.add(v)
93
+ if (v !== id) remapped++
94
+ return v
95
+ }
96
+ let renamed = 0
97
+ const toolName = (name: string) => {
98
+ const v = name.replace(/[^A-Za-z0-9_-]/g, "_").slice(0, 64) || "tool"
99
+ if (v !== name) renamed++
100
+ return v
101
+ }
102
+
103
+ // Rows are built with numeric times, stamped at the end (when they may still shift back to now).
104
+ type Draft = { role: MessageRow["role"]; type: MessageRow["type"]; at: number; parts: Part[]; tag?: string; model?: string }
105
+ const drafts: Draft[] = []
106
+ const signal = (parts: Part[], at: number, tag?: string) => drafts.push({ role: "signal", type: "user", at, parts, ...(tag ? { tag } : {}) })
107
+
108
+ let model = (turns.find((t) => t.kind === "assistant" && t.model) as { model?: string } | undefined)?.model ?? session.header.models.at(-1)
109
+ let reply: { d: Draft; calls: number; results: boolean } | undefined
110
+ const closeReply = () => (reply = undefined)
111
+ /** The open reply, or a new one; text after a tool call, or a call after results, starts a new step in it. */
112
+ const replyFor = (at: number, kind: "text" | "call") => {
113
+ if (!reply) {
114
+ const d: Draft = { role: "assistant", type: "v2", at, parts: [], ...(model ? { model } : {}) }
115
+ drafts.push(d)
116
+ reply = { d, calls: 0, results: false }
117
+ } else if ((kind === "text" && reply.calls) || (kind === "call" && reply.results)) {
118
+ reply.d.parts.push({ type: "step-start", createdAt: at })
119
+ reply.calls = 0
120
+ reply.results = false
121
+ }
122
+ return reply
123
+ }
124
+
125
+ signal([{ type: "text", text: continuedNote(label, session.header.id, opts.now.toISOString().slice(0, 10)), createdAt: tick(turns[0]?.at) }], clock, "note")
126
+ const written = new Set<string>()
127
+ const calls = new Set(turns.filter((t) => t.kind === "tool_call").map((t) => (t as Extract<Turn, { kind: "tool_call" }>).id))
128
+
129
+ for (const t of turns) {
130
+ switch (t.kind) {
131
+ case "user": {
132
+ const files: Part[] = []
133
+ let text = t.text
134
+ for (const a of t.attachments ?? []) {
135
+ const img = a.kind === "image" ? parseDataUrl(imageDataUrl(a)) : undefined
136
+ if (img) files.push({ type: "file", data: img.base64, mimeType: img.mime })
137
+ else if (a.kind === "file" && a.path) text += `\n\n[Attached file: ${a.path}]`
138
+ else drop("attachments")
139
+ }
140
+ if (!text.trim() && !files.length) break
141
+ closeReply()
142
+ const at = tick(t.at)
143
+ signal([...(text.trim() ? [{ type: "text", text, createdAt: at }] : []), ...files.map((f) => ({ ...f, createdAt: at }))], at)
144
+ expect.users++
145
+ break
146
+ }
147
+ case "assistant": {
148
+ if (!t.text.trim()) break
149
+ // A reply belongs to one model: text from another starts a new reply.
150
+ if (t.model && t.model !== model) {
151
+ closeReply()
152
+ model = t.model
153
+ }
154
+ const at = tick(t.at)
155
+ replyFor(at, "text").d.parts.push({ type: "text", text: t.text, createdAt: at })
156
+ expect.assistants++
157
+ break
158
+ }
159
+ case "reasoning": {
160
+ if (!t.text.trim()) break
161
+ const at = tick(t.at)
162
+ replyFor(at, "text").d.parts.push({ type: "reasoning", reasoning: t.text, details: [{ type: "text", text: t.text }], createdAt: at })
163
+ expect.reasoning++
164
+ drop("reasoning: kept as text, without provider signatures")
165
+ break
166
+ }
167
+ case "tool_call": {
168
+ if (written.has(t.id)) {
169
+ drop("repeated tool call ids")
170
+ break
171
+ }
172
+ written.add(t.id)
173
+ const r = replyFor(tick(t.at), "call")
174
+ let args = t.input
175
+ if (!args || typeof args !== "object" || Array.isArray(args)) {
176
+ args = { input: args ?? null }
177
+ drop("tool inputs that were not objects, wrapped as {input}")
178
+ }
179
+ const inv: Record<string, unknown> = { toolCallId: callId(t.id), toolName: toolName(t.name), args }
180
+ const res = results.get(t.id)
181
+ if (!res) {
182
+ Object.assign(inv, { state: "output-error", errorText: NO_RESULT })
183
+ drop("calls with no result, marked as failed")
184
+ } else {
185
+ const output = res.fullPath ? `${res.output}\n\n[Full output: ${res.fullPath}]` : res.output
186
+ if (res.ok) Object.assign(inv, { state: "result", result: output })
187
+ else Object.assign(inv, { state: "output-error", errorText: output.trim() ? output : "The tool call failed." })
188
+ }
189
+ r.d.parts.push({ type: "tool-invocation", toolInvocation: inv })
190
+ r.calls++
191
+ expect.calls++
192
+ break
193
+ }
194
+ case "tool_result": {
195
+ // The result is already folded into its call; here it only marks the step as answered.
196
+ if (results.get(t.callId) !== t) drop("repeated tool results")
197
+ else if (!calls.has(t.callId)) drop("tool results without a call")
198
+ else if (reply?.calls) reply.results = true
199
+ break
200
+ }
201
+ case "compaction": {
202
+ closeReply()
203
+ const at = tick(t.at)
204
+ signal([{ type: "text", text: `[Summary of the earlier conversation in ${label}, which replaced the turns before it]\n\n${t.summary}`, createdAt: at }], at, "compaction")
205
+ expect.compactions++
206
+ break
207
+ }
208
+ case "file_edit":
209
+ if (!t.callId || !written.has(t.callId)) drop("file edits not made by a tool call")
210
+ break
211
+ case "subagent":
212
+ drop("subagent sessions (their call and result are kept)")
213
+ break
214
+ }
215
+ }
216
+
217
+ // Nothing may sort after the next row MastraCode writes, which is stamped now.
218
+ const late = clock - opts.now.getTime()
219
+ const stamp = (ms: number) => new Date(late > 0 ? ms - late : ms).toISOString()
220
+ const messages: MessageRow[] = drafts.map((d) => {
221
+ const id = randomUUID()
222
+ const createdAt = stamp(d.at)
223
+ const parts = d.parts.map((p) => (typeof p.createdAt === "number" && late > 0 ? { ...p, createdAt: p.createdAt - late } : p))
224
+ let content: Content
225
+ if (d.role === "signal") {
226
+ // As MastraCode records a prompt: the signal's own id and times beside the parts.
227
+ const metadata: Record<string, any> = { signal: { acceptedAt: createdAt, createdAt, id, tagName: "user", type: "user" } }
228
+ if (d.tag) metadata.brain = { kind: d.tag }
229
+ content = { format: 2, metadata, parts }
230
+ } else {
231
+ const last = [...parts].reverse().find((p) => p.type === "text")
232
+ content = { format: 2, parts, ...(last ? { content: last.text as string } : {}), ...(d.model ? { metadata: { modelId: d.model } } : {}) }
233
+ }
234
+ return { id, role: d.role, type: d.type, content, createdAt }
235
+ })
236
+
237
+ const lost = [...drops].map(([what, n]) => `${what} (${n})`)
238
+ if (remapped) lost.push(`tool call ids, remapped to ids providers accept (${remapped})`)
239
+ if (renamed) lost.push(`tool names changed to fit providers (${renamed})`)
240
+ if (session.turns.some((t) => t.kind === "assistant" && t.usage)) lost.push("token usage (MastraCode counts its own)")
241
+ const thread: ThreadRow = {
242
+ id: opts.id,
243
+ resourceId: opts.resourceId,
244
+ title: continuedTitle(session),
245
+ // No model: the source's model names are not MastraCode model ids, so the person's own default applies.
246
+ metadata: { projectPath: opts.cwd, brain: { continues: session.header.id } },
247
+ createdAt: messages[0]!.createdAt,
248
+ updatedAt: messages.at(-1)!.createdAt,
249
+ }
250
+ return { thread, messages, lost, expect }
251
+ }
252
+
253
+ /** Which tables and columns are missing from a store, for one this writer does not know. */
254
+ function missingColumns(db: Database): string[] {
255
+ const missing: string[] = []
256
+ for (const [table, cols] of Object.entries(COLUMNS)) {
257
+ const have = new Set((db.query(`PRAGMA table_info("${table}")`).all() as Array<{ name: string }>).map((c) => c.name))
258
+ if (!have.size) missing.push(table)
259
+ else for (const c of cols) if (!have.has(c)) missing.push(`${table}.${c}`)
260
+ }
261
+ return missing
262
+ }
263
+
264
+ /**
265
+ * Inserts the plan's thread in one IMMEDIATE transaction and reads it back before committing; any mismatch, or
266
+ * an id the store already has, leaves the store as it was. Creates a missing store (0600, in 0700 folders).
267
+ */
268
+ export function installMastra(dbPath: string, plan: MastraPlan, home: string): void {
269
+ const fresh = !exists(dbPath)
270
+ if (fresh) mkdirSync(dirname(dbPath), { recursive: true, mode: 0o700 })
271
+ const db = new Database(dbPath)
272
+ let ok = false
273
+ try {
274
+ if (fresh) {
275
+ chmodSync(dbPath, 0o600)
276
+ db.exec(SCHEMA)
277
+ }
278
+ db.exec("PRAGMA busy_timeout = 5000")
279
+ const missing = missingColumns(db)
280
+ if (missing.length) throw new Error(`the MastraCode store at ${dbPath} has no ${missing.join(", ")}; it is not a store this writer knows`)
281
+ const t = plan.thread
282
+ db.transaction(() => {
283
+ if (db.query("SELECT 1 FROM mastra_threads WHERE id = ?").get(t.id)) throw new Error(`the MastraCode store already has a thread ${t.id}; refusing to overwrite it`)
284
+ const taken = db.query("SELECT 1 FROM mastra_messages WHERE id = ?")
285
+ for (const m of plan.messages) if (taken.get(m.id)) throw new Error(`the MastraCode store already has a message ${m.id}; refusing to reuse it`)
286
+ db.prepare('INSERT INTO "mastra_threads" (id, resourceId, title, metadata, createdAt, updatedAt) VALUES (?, ?, ?, ?, ?, ?)').run(
287
+ t.id,
288
+ t.resourceId,
289
+ t.title,
290
+ encodeJsonb(t.metadata),
291
+ t.createdAt,
292
+ t.updatedAt,
293
+ )
294
+ const add = db.prepare('INSERT INTO "mastra_messages" (id, thread_id, content, role, type, createdAt, resourceId) VALUES (?, ?, ?, ?, ?, ?, ?)')
295
+ for (const m of plan.messages) add.run(m.id, t.id, JSON.stringify(m.content), m.role, m.type, m.createdAt, t.resourceId)
296
+ // Read back as the reader reads MastraCode; throwing here rolls the transaction back.
297
+ const back = readMastra(db, t.id, dbPath, home)
298
+ const n = countTurns(back)
299
+ const pairs = unpaired(back)
300
+ const e = plan.expect
301
+ if (
302
+ n.user !== e.users ||
303
+ n.assistant !== e.assistants ||
304
+ n.tool_call !== e.calls ||
305
+ n.reasoning !== e.reasoning ||
306
+ n.compaction !== e.compactions ||
307
+ pairs.calls.length ||
308
+ pairs.results.length
309
+ )
310
+ throw new Error(
311
+ `the MastraCode thread did not read back as written (users ${n.user}/${e.users}, replies ${n.assistant}/${e.assistants}, tool calls ${n.tool_call}/${e.calls}, reasoning ${n.reasoning}/${e.reasoning}, summaries ${n.compaction}/${e.compactions}, unpaired ${pairs.calls.length + pairs.results.length}); nothing was kept`,
312
+ )
313
+ }).immediate()
314
+ ok = true
315
+ } finally {
316
+ db.close()
317
+ // A store this write created and could not fill is removed again, so MastraCode creates its own.
318
+ if (fresh && !ok) for (const side of ["", "-wal", "-shm", "-journal"]) rmSync(dbPath + side, { force: true })
319
+ }
320
+ }
321
+
322
+ /** Copies the store with its write-ahead log before anything is changed. */
323
+ function backup(dbPath: string, dir: string) {
324
+ mkdirSync(dir, { recursive: true })
325
+ for (const side of ["", "-wal", "-shm"]) if (exists(dbPath + side)) copyFileSync(dbPath + side, join(dir, basename(dbPath) + side))
326
+ }
327
+
328
+ const PROMPT = "Continue where this session left off."
329
+
330
+ export const mastracodeWriter: Writer = {
331
+ tool: TOOL,
332
+ label: LABEL,
333
+ way: "native",
334
+
335
+ async ready(opts: WriteOptions): Promise<Readiness> {
336
+ const bin = mastraBin()
337
+ if (!bin) return { ok: false, why: "MastraCode is not installed (no mastracode on PATH)" }
338
+ const v = mastraVersion(bin)
339
+ if (!v) return { ok: false, why: "`mastracode --version` did not run" }
340
+ if (!TESTED_VERSIONS.includes(v)) return { ok: false, why: `MastraCode ${v} is not a version this writer was checked against (${TESTED_VERSIONS.join(", ")})` }
341
+ if (!isDir(opts.cwd)) return { ok: false, why: `the folder to continue in does not exist: ${opts.cwd}` }
342
+ const db = storePath(opts.home)
343
+ const elsewhere = otherStore(db)
344
+ if (elsewhere) return { ok: false, why: `${elsewhere}; brain writes only the local store at ${db}` }
345
+ const busy = storeInUse(db)
346
+ if (busy) return { ok: false, why: busy }
347
+ if (exists(db)) {
348
+ let missing: string[]
349
+ try {
350
+ missing = withSnapshot(db, missingColumns)
351
+ } catch (e) {
352
+ return { ok: false, why: `the MastraCode store at ${db} cannot be read: ${(e as Error).message}` }
353
+ }
354
+ if (missing.length) return { ok: false, why: `the MastraCode store at ${db} has no ${missing.join(", ")}; it is not a store this writer knows` }
355
+ }
356
+ return { ok: true }
357
+ },
358
+
359
+ async write(session: Session, opts: WriteOptions): Promise<WriteResult> {
360
+ const cwd = realDir(opts.cwd)
361
+ const db = storePath(opts.home)
362
+ const now = new Date()
363
+ const resourceId = resourceIdFor(cwd)
364
+ const plan = planMastra(session, { id: randomUUID(), cwd, resourceId, now })
365
+ const backupDir = join(opts.home, ".brain", "backups", TOOL, now.toISOString().replace(/[:.]/g, "-"))
366
+ const hadStore = exists(db)
367
+ if (!opts.dryRun) {
368
+ const busy = storeInUse(db)
369
+ if (busy) throw new Error(busy)
370
+ if (hadStore) backup(db, backupDir)
371
+ installMastra(db, plan, opts.home)
372
+ }
373
+ const id = plan.thread.id
374
+ return {
375
+ tool: TOOL,
376
+ way: "native",
377
+ sessionId: id,
378
+ written: hadStore ? [db, backupDir] : [db],
379
+ open: `cd ${sh(cwd)} && mastracode --thread ${id} --resource-id ${sh(resourceId)} --prompt ${sh(PROMPT)}`,
380
+ note: `MastraCode looks a thread up by its resource id, so run it from ${cwd} with both flags; --prompt sends one headless turn on the thread.`,
381
+ lost: plan.lost,
382
+ dryRun: !!opts.dryRun,
383
+ }
384
+ },
385
+ }
@@ -0,0 +1,9 @@
1
+ import type { Reader, Writer } from "../types.ts"
2
+ import { reader } from "./reader.ts"
3
+ import { mistralVibeImportWriter } from "./writer.ts"
4
+
5
+ // Mistral Vibe: reads its session folders, and continues any session as a session folder that
6
+ // `vibe --resume` lists and imports into Vibe's own store.
7
+ export { reader }
8
+ export const readers: Reader[] = [reader]
9
+ export const writers: Writer[] = [mistralVibeImportWriter]
@@ -0,0 +1,62 @@
1
+ import { join } from "node:path"
2
+ import { exists, gitInfo, isDir, ls, mtime, readJson, readJsonl } from "../../fsutil.ts"
3
+ import { header, lossCounter } from "../../session/util.ts"
4
+ import type { Reader, SessionRef } from "../types.ts"
5
+ import { LABEL, MESSAGES, META, messagesToTurns, metaTime, TOOL } from "./records.ts"
6
+
7
+ // Mistral Vibe's session folders, $VIBE_HOME/logs/session/session_*/ (records.ts). Sessions Vibe 3.x keeps
8
+ // only in its private Unified store are not read: that store is runtime-private (ADR 0006) and has no
9
+ // published format.
10
+
11
+ type Meta = Record<string, any>
12
+ const str = (v: unknown) => (typeof v === "string" && v.trim() ? v : undefined)
13
+
14
+ export const reader: Reader = {
15
+ tool: TOOL,
16
+ label: LABEL,
17
+
18
+ async list(home) {
19
+ const root = join(home, ".vibe", "logs", "session")
20
+ const refs: SessionRef[] = []
21
+ for (const name of ls(root)) {
22
+ const dir = join(root, name)
23
+ if (!name.startsWith("session_") || !isDir(dir) || !exists(join(dir, MESSAGES))) continue
24
+ const meta = readJson<Meta>(join(dir, META))
25
+ const sourceId = str(meta?.session_id)
26
+ if (!sourceId || meta?.total_messages === 0) continue
27
+ const title = str(meta?.title)
28
+ const cwd = str(meta?.environment?.working_directory)
29
+ const updatedAt = metaTime(meta?.end_time) ?? metaTime(meta?.start_time) ?? mtime(join(dir, MESSAGES))
30
+ refs.push({ tool: TOOL, sourceId, path: join(dir, MESSAGES), ...(title ? { title } : {}), ...(cwd ? { cwd } : {}), ...(updatedAt ? { updatedAt } : {}), extra: { dir } })
31
+ }
32
+ return refs.sort((a, b) => (b.updatedAt ?? "").localeCompare(a.updatedAt ?? ""))
33
+ },
34
+
35
+ async read(ref, home) {
36
+ if (!exists(ref.path)) throw new Error(`${ref.path} does not exist`)
37
+ const dir = typeof ref.extra?.dir === "string" ? ref.extra.dir : join(ref.path, "..")
38
+ const meta = readJson<Meta>(join(dir, META))
39
+ const { rows, bad } = readJsonl(ref.path)
40
+ if (!rows.length) throw new Error(`${ref.path} has no Mistral Vibe messages`)
41
+ const lost = lossCounter()
42
+ if (bad) lost.add("unreadable lines", bad)
43
+ const cwd = str(meta?.environment?.working_directory) ?? ref.cwd
44
+ const title = str(meta?.title)
45
+ const startedAt = metaTime(meta?.start_time)
46
+ const updatedAt = metaTime(meta?.end_time) ?? mtime(ref.path)
47
+ const model = str(meta?.config?.model_name) ?? str(meta?.config?.active_model)
48
+ const h = header(TOOL, str(meta?.session_id) ?? ref.sourceId, {
49
+ sourcePath: ref.path,
50
+ ...(title ? { title } : {}),
51
+ project: { ...(cwd ? { cwd } : {}), ...gitInfo(cwd, home), ...(str(meta?.git_branch) ? { gitBranch: meta!.git_branch } : {}), ...(str(meta?.git_commit) ? { gitCommit: meta!.git_commit } : {}) },
52
+ ...(startedAt ? { startedAt } : {}),
53
+ ...(updatedAt ? { updatedAt } : {}),
54
+ ...(model ? { models: [model] } : {}),
55
+ })
56
+ const turns = messagesToTurns(rows, h, lost, dir)
57
+ const children = Array.isArray(meta?.child_sessions) ? meta!.child_sessions.length : 0
58
+ if (children) lost.add("subagent sessions (their results stay in the parent's tool results)", children)
59
+ lost.flush(h)
60
+ return { header: h, turns }
61
+ },
62
+ }
@@ -0,0 +1,104 @@
1
+ import { existsSync } from "node:fs"
2
+ import { isAbsolute, join } from "node:path"
3
+ import type { Attachment, SessionHeader, Turn } from "../../session/types.ts"
4
+ import { clip, iso, toolKindOf, type lossCounter } from "../../session/util.ts"
5
+
6
+ // Facts about Mistral Vibe's session folders, shared by its reader and writer. They come from Vibe 3.1.0's
7
+ // source (mistralai/mistral-vibe: vibe/core/session/session_loader.py and session_index.py, which list and
8
+ // load them; vibe/core/session/session_interop.py and vibe/app_server/_legacy_import.py, which import one
9
+ // into the Unified store on resume; vibe/core/types.py for LLMMessage and SessionMetadata).
10
+ //
11
+ // A session folder is $VIBE_HOME/logs/session/session_<YYYYmmdd_HHMMSS>_<id[:8]>/ (VIBE_HOME defaults to
12
+ // ~/.vibe) with meta.json (session id, times, working directory, title) and messages.jsonl, one LLMMessage
13
+ // per line in OpenAI shape: role, content, reasoning_content, tool_calls[{id, function: {name, arguments}}],
14
+ // tool_call_id, name. A failed tool result is wrapped in <tool_error>…</tool_error>. Vibe 2.x wrote these;
15
+ // Vibe 3.x keeps sessions in a private Unified store, still lists these folders in its resume picker, and
16
+ // imports one into a new Unified session when it is resumed.
17
+
18
+ export const TOOL = "mistral-vibe"
19
+ export const LABEL = "Mistral Vibe"
20
+
21
+ export const META = "meta.json"
22
+ export const MESSAGES = "messages.jsonl"
23
+
24
+ type Lost = ReturnType<typeof lossCounter>
25
+ type Json = Record<string, any>
26
+ const isObj = (v: unknown): v is Json => typeof v === "object" && v !== null && !Array.isArray(v)
27
+
28
+ /** Vibe's tags around a tool result that did not succeed (vibe/core/utils/tags.py). */
29
+ const FAILED = /^\s*<(tool_error|user_cancellation)>([\s\S]*?)<\/\1>\s*$/
30
+
31
+ /** Content as Vibe stores it: a string, or a list of {type: "text", text} parts. */
32
+ export function contentText(v: unknown): string {
33
+ if (typeof v === "string") return v
34
+ if (Array.isArray(v)) return v.map((p) => (isObj(p) && typeof p.text === "string" ? p.text : "")).join("\n")
35
+ return ""
36
+ }
37
+
38
+ function parseArgs(a: unknown): unknown {
39
+ if (typeof a !== "string") return a ?? {}
40
+ try {
41
+ return JSON.parse(a)
42
+ } catch {
43
+ return { arguments: a }
44
+ }
45
+ }
46
+
47
+ /** The turns of a messages.jsonl, in order. Injected context is skipped; a compaction boundary is a compaction. */
48
+ export function messagesToTurns(rows: Json[], h: SessionHeader, lost: Lost, dir?: string): Turn[] {
49
+ const turns: Turn[] = []
50
+ const calls = new Set<string>()
51
+ const answered = new Set<string>()
52
+ for (const m of rows) {
53
+ if (!isObj(m)) continue
54
+ const text = contentText(m.content)
55
+ if (m.role === "user") {
56
+ if (m.context_boundary === "compaction") {
57
+ if (text.trim()) turns.push({ kind: "compaction", summary: text })
58
+ continue
59
+ }
60
+ if (m.injected === true) continue
61
+ // Vibe 2.24.3 snapshots an @mentioned image into <session>/attachments/ and records {source: {kind: "file", path}, alias, mime_type}.
62
+ const attachments: Attachment[] = []
63
+ for (const im of Array.isArray(m.images) ? m.images : []) {
64
+ const path = isObj(im) && isObj(im.source) && im.source.kind === "file" && typeof im.source.path === "string" ? (isAbsolute(im.source.path) ? im.source.path : dir ? join(dir, im.source.path) : undefined) : undefined
65
+ if (path && existsSync(path)) attachments.push({ kind: "image", path, ...(isObj(im) && typeof im.alias === "string" ? { name: im.alias } : {}), ...(isObj(im) && typeof im.mime_type === "string" ? { mime: im.mime_type } : {}) })
66
+ else lost.add("images")
67
+ }
68
+ if (text.trim() || attachments.length) turns.push({ kind: "user", text, ...(attachments.length ? { attachments } : {}) })
69
+ } else if (m.role === "assistant") {
70
+ const reasoning = contentText(m.reasoning_content)
71
+ if (reasoning.trim()) turns.push({ kind: "reasoning", text: reasoning })
72
+ if (Array.isArray(m.reasoning_payloads) && m.reasoning_payloads.length) lost.add("signed or encrypted reasoning")
73
+ if (text.trim()) turns.push({ kind: "assistant", text })
74
+ for (const c of Array.isArray(m.tool_calls) ? m.tool_calls : []) {
75
+ if (!isObj(c) || typeof c.id !== "string" || !c.id || calls.has(c.id)) continue
76
+ const fn = isObj(c.function) ? c.function : {}
77
+ const name = typeof fn.name === "string" && fn.name ? fn.name : "unknown"
78
+ calls.add(c.id)
79
+ turns.push({ kind: "tool_call", id: c.id, name, toolKind: toolKindOf(name), input: parseArgs(fn.arguments) })
80
+ }
81
+ } else if (m.role === "tool" && typeof m.tool_call_id === "string") {
82
+ if (!calls.has(m.tool_call_id) || answered.has(m.tool_call_id)) continue
83
+ answered.add(m.tool_call_id)
84
+ const failed = text.match(FAILED)
85
+ const c = clip(failed ? failed[2]! : text)
86
+ turns.push({ kind: "tool_result", callId: m.tool_call_id, ok: !failed && m.tool_result?.cancelled !== true, output: c.text, ...(c.truncated ? { truncated: true } : {}) })
87
+ }
88
+ }
89
+ // Calls never answered get a result saying so, right after the call, so every call stays paired.
90
+ const open = [...calls].filter((id) => !answered.has(id))
91
+ if (open.length) lost.add("tool results not recorded", open.length)
92
+ for (const id of open) {
93
+ const i = turns.findIndex((t) => t.kind === "tool_call" && t.id === id)
94
+ turns.splice(i + 1, 0, { kind: "tool_result", callId: id, ok: false, output: "(no result recorded)" })
95
+ }
96
+ if (!h.title) {
97
+ const first = turns.find((t) => t.kind === "user")
98
+ if (first?.kind === "user") h.title = first.text.replace(/\s+/g, " ").trim().slice(0, 120) || undefined
99
+ }
100
+ return turns
101
+ }
102
+
103
+ /** A meta.json time as ISO, when it parses. */
104
+ export const metaTime = (v: unknown) => iso(v)