@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,359 @@
1
+ import { createHash, randomUUID } from "node:crypto"
2
+ import { mkdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs"
3
+ import { userInfo } from "node:os"
4
+ import { extname, join } from "node:path"
5
+ import { toolLabel } from "../../brief.ts"
6
+ import { gitInfo, isDir, ls, versionOf } from "../../fsutil.ts"
7
+ import type { Session, Turn } from "../../session/types.ts"
8
+ import { countTurns, firstUserText, unpaired } from "../../session/util.ts"
9
+ import type { Readiness, WriteOptions, WriteResult, Writer } from "../types.ts"
10
+ import { reader } from "./reader.ts"
11
+ import { LABEL, MESSAGES, META, TOOL } from "./records.ts"
12
+
13
+ // Continues a portable session in Mistral Vibe through its own importer. Vibe 3.x keeps sessions in a
14
+ // private Unified store that is not a supported write target (ADR 0006); what it does accept is a session
15
+ // folder in the format Vibe 2.x wrote ($VIBE_HOME/logs/session/session_<time>_<id8>/: meta.json and
16
+ // messages.jsonl). Vibe 3.1.0 lists such a folder in `vibe --resume`, previews it, and on resume imports
17
+ // its history into a new Unified session (session_interop.export_legacy_committed_history), keeping each
18
+ // message, each tool call with its arguments and each result, failures included, and readable reasoning.
19
+ //
20
+ // Calls become Vibe's own tools where their arguments fit (read_file, write_file, edit, bash, grep). The
21
+ // note saying where the session came from is an injected user message right after the first request;
22
+ // Vibe's legacy history skips injected messages, though its importer turns the note into a plain one.
23
+ // The importer does not carry a compaction boundary into the Unified store either, so the session starts
24
+ // from the last compaction summary, written as Vibe writes one (context_boundary "compaction").
25
+
26
+ /** Vibe versions whose resume importer this writer was checked against. */
27
+ export const TESTED_VERSIONS = ["3.1.0"]
28
+
29
+ /** False for 3.1.0: see imagesOf in planMessages. */
30
+ export const VIBE_RESUMES_IMPORTED_IMAGES = false
31
+
32
+ /** The installed version (`vibe --version` prints "vibe 3.1.0"), or undefined when it is missing. */
33
+ export function vibeVersion(bin = "vibe"): string | undefined {
34
+ return versionOf(bin)?.match(/(\d+\.\d+\.\d+(?:[-.\w]*)?)/)?.[1]
35
+ }
36
+
37
+ const continuedNote = (label: string, id: string, when: string) =>
38
+ `<system-reminder>\nThis session continues ${label} session ${id}, carried over by brain session on ${when}. The messages below are its history; anything listed as lost did not carry over. Check files before relying on details, since they may have changed.\n</system-reminder>`
39
+
40
+ const shellQuote = (s: string) => (/^[\w./@%+=:,-]+$/.test(s) ? s : `'${s.replace(/'/g, `'\\''`)}'`)
41
+
42
+ /** Vibe matches a session to the folder it is resumed from by the resolved path. */
43
+ function realCwd(cwd: string): string {
44
+ try {
45
+ return realpathSync(cwd)
46
+ } catch {
47
+ return cwd
48
+ }
49
+ }
50
+
51
+ type Call = Extract<Turn, { kind: "tool_call" }>
52
+ type Json = Record<string, unknown>
53
+
54
+ // Argument names other tools use for the same thing.
55
+ const FILE = ["file_path", "path", "filePath", "absolute_path", "target_file", "file", "filename"]
56
+ const OLD = ["old_string", "oldString", "old_str", "old_text"]
57
+ const NEW = ["new_string", "newString", "new_str", "new_text"]
58
+ const CONTENT = ["content", "contents", "file_text", "text"]
59
+ const COMMAND = ["command", "cmd", "commandLine"]
60
+ const PATTERN = ["pattern", "regex", "query"]
61
+ const DIR = ["path", "dir_path", "directory", "target_directory"]
62
+
63
+ /** The call as Vibe's own tool when its arguments fit one; otherwise undefined and it keeps its own. */
64
+ export function vibeTool(c: Call): { name: string; args: Json } | undefined {
65
+ const input = c.input && typeof c.input === "object" && !Array.isArray(c.input) ? (c.input as Json) : {}
66
+ const str = (keys: string[]) => {
67
+ const v = keys.map((k) => input[k]).find((x) => typeof x === "string" || (Array.isArray(x) && x.every((y) => typeof y === "string")))
68
+ return Array.isArray(v) ? v.join(" ") : (v as string | undefined)
69
+ }
70
+ const path = str(FILE)
71
+ switch (c.toolKind) {
72
+ case "read":
73
+ return path ? { name: "read_file", args: { file_path: path } } : undefined
74
+ case "edit": {
75
+ const [o, n] = [str(OLD), str(NEW)]
76
+ return path && o && n !== undefined ? { name: "edit", args: { file_path: path, old_string: o, new_string: n } } : undefined
77
+ }
78
+ case "write": {
79
+ const content = str(CONTENT)
80
+ return path && content !== undefined ? { name: "write_file", args: { file_path: path, content } } : undefined
81
+ }
82
+ case "shell": {
83
+ const command = str(COMMAND)
84
+ return command ? { name: "bash", args: { command } } : undefined
85
+ }
86
+ case "search": {
87
+ // Vibe has grep and no glob tool; a glob search keeps its own name.
88
+ const pattern = str(PATTERN)
89
+ const dir = str(DIR)
90
+ return pattern && !/glob/i.test(c.name) ? { name: "grep", args: { pattern, ...(dir ? { path: dir } : {}) } } : undefined
91
+ }
92
+ default:
93
+ return undefined
94
+ }
95
+ }
96
+
97
+ export interface MessagesPlan {
98
+ messages: Json[]
99
+ lost: string[]
100
+ /** What a read-back must find. */
101
+ expect: { users: number; assistants: number; calls: number }
102
+ /** Image bytes to write under the session folder, by relative path (attachments/<sha1>.<ext>). */
103
+ assets: Map<string, Buffer>
104
+ }
105
+
106
+ /** The messages.jsonl lines of a new Vibe session folder continuing `session`. Pure. */
107
+ export function planMessages(session: Session, opts: { now: Date }): MessagesPlan {
108
+ const drops = new Map<string, number>()
109
+ const drop = (what: string, n = 1) => drops.set(what, (drops.get(what) ?? 0) + n)
110
+ const label = toolLabel(session.header.tool)
111
+
112
+ // Vibe's importer copies every message into the model's context, so the session starts from the last summary.
113
+ let turns = session.turns
114
+ let last = -1
115
+ turns.forEach((t, i) => {
116
+ if (t.kind === "compaction") last = i
117
+ })
118
+ if (last >= 0) {
119
+ const before = turns.slice(0, last).filter((t) => t.kind !== "compaction").length
120
+ if (before) drop("turns before the last compaction (the summary stands in for them)", before)
121
+ turns = turns.slice(last)
122
+ }
123
+
124
+ const note: Json = { role: "user", content: continuedNote(label, session.header.id, opts.now.toISOString().slice(0, 10)), injected: true, message_id: randomUUID() }
125
+ // The note follows the first request: Vibe's import drops `injected`, and the first user message becomes
126
+ // the imported session's preview, which should be the person's words.
127
+ const firstUser = turns.findIndex((t) => t.kind === "user" && t.text.trim())
128
+ const messages: Json[] = firstUser < 0 ? [note] : []
129
+ const expect = { users: 0, assistants: 0, calls: 0 }
130
+ const assets = new Map<string, Buffer>()
131
+ // An image whose bytes are still on disk is snapshotted the way Vibe 2.24.3 does for an @mentioned image:
132
+ // attachments/<sha1 of the bytes>.<ext> and {source: {kind: "file", path}, alias, mime_type} on the message.
133
+ const imagesOf = (list: NonNullable<Extract<Turn, { kind: "user" }>["attachments"]>) => {
134
+ const out: Json[] = []
135
+ for (const a of list) {
136
+ // Vibe 3.1.0 imports a message with an image into its own store as {type: "image", data, mimeType}, then refuses
137
+ // to resume it ("image.attachment: Field required"): the whole session becomes unresumable. Found by the wire
138
+ // harness with the real tool, for file and inline sources and for blob resources alike. Until a Vibe version
139
+ // resumes its own imported images, an image is dropped and listed as lost.
140
+ if (!VIBE_RESUMES_IMPORTED_IMAGES) {
141
+ drop("images (Vibe 3.1.0 cannot resume an imported session that holds one)")
142
+ continue
143
+ }
144
+ let bytes: Buffer | undefined
145
+ if (a.kind === "image" && a.path) {
146
+ try {
147
+ bytes = readFileSync(a.path)
148
+ } catch {
149
+ bytes = undefined
150
+ }
151
+ }
152
+ if (!bytes || !a.path) {
153
+ drop("attachments")
154
+ continue
155
+ }
156
+ const rel = `attachments/${createHash("sha1").update(bytes).digest("hex")}${extname(a.path).toLowerCase() || ".png"}`
157
+ assets.set(rel, bytes)
158
+ out.push({ source: { kind: "file", path: rel }, alias: a.name ?? a.path.split("/").pop(), mime_type: a.mime ?? "image/png" })
159
+ }
160
+ return out
161
+ }
162
+ // Call ids stay as they were when an OpenAI-style API takes them, else get a call_ id; each is used once.
163
+ const ids = new Map<string, string>()
164
+ const used = new Set<string>()
165
+ const idFor = (id: string) => {
166
+ let v = ids.get(id)
167
+ if (v) return v
168
+ const base = /^[A-Za-z0-9_-]{1,64}$/.test(id) ? id : `call_${id.replace(/[^A-Za-z0-9]/g, "").slice(-40) || "x"}`
169
+ v = base
170
+ for (let n = 2; used.has(v); n++) v = `${base}_${n}`
171
+ ids.set(id, v)
172
+ used.add(v)
173
+ return v
174
+ }
175
+ const results = new Map<string, Extract<Turn, { kind: "tool_result" }>>()
176
+ for (const t of turns) if (t.kind === "tool_result" && !results.has(t.callId)) results.set(t.callId, t)
177
+
178
+ let cur: { reasoning: string[]; text: string; calls: Call[] } | undefined
179
+ let pendingReasoning: string[] = []
180
+ const answered = new Set<string>()
181
+ const nameOf = (c: Call) => vibeTool(c)?.name ?? (c.name.replace(/[^A-Za-z0-9_-]/g, "_").slice(0, 64) || "tool")
182
+ const flush = () => {
183
+ if (!cur) return
184
+ const reasoning = cur.reasoning.join("\n\n")
185
+ const toolCalls = cur.calls.map((c, index) => ({ id: idFor(c.id), index, function: { name: nameOf(c), arguments: JSON.stringify(vibeTool(c)?.args ?? c.input ?? {}) }, type: "function" }))
186
+ messages.push({ role: "assistant", content: cur.text, ...(reasoning ? { reasoning_content: reasoning } : {}), ...(toolCalls.length ? { tool_calls: toolCalls } : {}), message_id: randomUUID() })
187
+ if (cur.text.trim()) expect.assistants++
188
+ // A call the session never answered is closed as a failure, so every request has its result.
189
+ for (const c of cur.calls) {
190
+ if (results.has(c.id)) continue
191
+ messages.push({ role: "tool", content: "<tool_error>No result was recorded for this call; the session moved on before it finished.</tool_error>", tool_call_id: idFor(c.id), name: nameOf(c) })
192
+ answered.add(c.id)
193
+ drop("tool calls without a result (closed as failures)")
194
+ }
195
+ cur = undefined
196
+ }
197
+
198
+ for (const [i, t] of turns.entries()) {
199
+ switch (t.kind) {
200
+ case "user":
201
+ flush()
202
+ if (pendingReasoning.length) drop("reasoning without a reply", pendingReasoning.length)
203
+ pendingReasoning = []
204
+ const images = imagesOf(t.attachments ?? [])
205
+ if (!t.text.trim()) break
206
+ messages.push({ role: "user", content: t.text, ...(images.length ? { images } : {}), message_id: randomUUID() })
207
+ if (i === firstUser) messages.push(note)
208
+ expect.users++
209
+ break
210
+ case "assistant":
211
+ if (cur?.calls.length || (cur?.text.trim() && t.text.trim())) flush()
212
+ cur ??= { reasoning: pendingReasoning, text: "", calls: [] }
213
+ pendingReasoning = []
214
+ cur.text = t.text
215
+ break
216
+ case "reasoning":
217
+ if (cur?.calls.length || cur?.text.trim()) flush()
218
+ pendingReasoning.push(t.text)
219
+ break
220
+ case "tool_call":
221
+ if (ids.has(t.id)) {
222
+ drop("repeated tool call ids")
223
+ break
224
+ }
225
+ idFor(t.id)
226
+ cur ??= { reasoning: pendingReasoning, text: "", calls: [] }
227
+ pendingReasoning = []
228
+ cur.calls.push(t)
229
+ expect.calls++
230
+ break
231
+ case "tool_result": {
232
+ if (!ids.has(t.callId)) {
233
+ drop("tool results without a call")
234
+ break
235
+ }
236
+ if (answered.has(t.callId) || results.get(t.callId) !== t) {
237
+ drop("repeated tool results")
238
+ break
239
+ }
240
+ const call = cur?.calls.find((c) => c.id === t.callId)
241
+ if (call) flush()
242
+ let text = t.output
243
+ if (t.fullPath) text += `\n\n[Full output: ${t.fullPath}]`
244
+ const name = call ? nameOf(call) : undefined
245
+ messages.push({ role: "tool", content: t.ok ? text : `<tool_error>${text || "The tool failed."}</tool_error>`, tool_call_id: idFor(t.callId), ...(name ? { name } : {}) })
246
+ answered.add(t.callId)
247
+ break
248
+ }
249
+ case "compaction":
250
+ flush()
251
+ messages.push({ role: "user", content: `[Summary of the earlier conversation in ${label}, which replaced the turns before it]\n\n${t.summary}`, context_boundary: "compaction", message_id: randomUUID() })
252
+ break
253
+ case "file_edit":
254
+ if (!t.callId || !ids.has(t.callId)) drop("file edits outside a tool call")
255
+ break
256
+ case "subagent":
257
+ drop("subagent transcripts (their results stay in the parent's tool results)")
258
+ break
259
+ }
260
+ }
261
+ flush()
262
+ if (pendingReasoning.length) drop("reasoning without a reply", pendingReasoning.length)
263
+ // A tool message names its tool; one answered after the assistant message was flushed gets it here.
264
+ const names = new Map<string, string>()
265
+ for (const m of messages) for (const c of (m.tool_calls as Array<{ id: string; function: { name: string } }> | undefined) ?? []) names.set(c.id, c.function.name)
266
+ for (const m of messages) if (m.role === "tool" && !m.name) m.name = names.get(m.tool_call_id as string) ?? "tool"
267
+ return { messages, lost: [...drops].map(([what, n]) => `${what} (${n})`), expect, assets }
268
+ }
269
+
270
+ /** `2026-11-08T10:00:00.000Z` → `20261108_100000`, the folder-name stamp Vibe uses (local time). */
271
+ function stamp(d: Date): string {
272
+ const p = (n: number) => String(n).padStart(2, "0")
273
+ return `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}_${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}`
274
+ }
275
+
276
+ /** The Mistral Vibe writer; `version` is for tests, which must not depend on an installed binary. */
277
+ export function mistralVibeWriter(opts: { version?: string } = {}): Writer {
278
+ const installed = () => opts.version ?? vibeVersion()
279
+ return {
280
+ tool: TOOL,
281
+ label: LABEL,
282
+ way: "import",
283
+
284
+ async ready(w: WriteOptions): Promise<Readiness> {
285
+ const v = installed()
286
+ if (!v) return { ok: false, why: "Mistral Vibe is not installed (`vibe --version` did not run)" }
287
+ if (!TESTED_VERSIONS.includes(v)) return { ok: false, why: `Mistral Vibe ${v} is not a version this writer was checked against (${TESTED_VERSIONS.join(", ")})` }
288
+ if (!isDir(w.cwd)) return { ok: false, why: `${w.cwd} is not a folder; Vibe offers a session from its working directory` }
289
+ return { ok: true }
290
+ },
291
+
292
+ async write(session: Session, w: WriteOptions): Promise<WriteResult> {
293
+ const cwd = realCwd(w.cwd)
294
+ const root = join(w.home, ".vibe", "logs", "session")
295
+ const now = new Date()
296
+ let sessionId = randomUUID()
297
+ // Vibe finds a folder by the first 8 characters of the id, so no other folder may share them.
298
+ const taken = (id: string) => ls(root).some((n) => n.startsWith("session_") && n.endsWith(`_${id.slice(0, 8)}`))
299
+ while (taken(sessionId)) sessionId = randomUUID()
300
+ const dir = join(root, `session_${stamp(now)}_${sessionId.slice(0, 8)}`)
301
+ const plan = planMessages(session, { now })
302
+ const git = gitInfo(cwd, w.home)
303
+ const title = `${session.header.title ?? firstUserText(session)?.replace(/\s+/g, " ").trim().slice(0, 60) ?? "Session"} (from ${toolLabel(session.header.tool)})`
304
+ const meta = {
305
+ session_id: sessionId,
306
+ parent_session_id: null,
307
+ start_time: now.toISOString(),
308
+ end_time: now.toISOString(),
309
+ git_commit: git.gitCommit ?? null,
310
+ git_branch: git.gitBranch ?? null,
311
+ environment: { working_directory: cwd },
312
+ origin_directory: cwd,
313
+ username: userInfo().username,
314
+ title,
315
+ // "manual" keeps Vibe from replacing the title with a generated one.
316
+ title_source: "manual",
317
+ total_messages: plan.messages.length,
318
+ import_provenance: { source: session.header.tool, session_id: session.header.sourceId, by: "brain session" },
319
+ }
320
+ const paths = { meta: join(dir, META), messages: join(dir, MESSAGES) }
321
+ if (!w.dryRun) {
322
+ mkdirSync(root, { recursive: true })
323
+ // mkdir without recursive on the session folder itself: an existing session is never touched.
324
+ mkdirSync(dir)
325
+ try {
326
+ for (const [rel, bytes] of plan.assets) {
327
+ mkdirSync(join(dir, "attachments"), { recursive: true })
328
+ writeFileSync(join(dir, rel), bytes)
329
+ }
330
+ writeFileSync(paths.messages, plan.messages.map((m) => `${JSON.stringify(m)}\n`).join(""), { flag: "wx" })
331
+ // meta.json last: the listing reads it, and a folder without it is skipped.
332
+ writeFileSync(paths.meta, JSON.stringify(meta, null, 2), { flag: "wx" })
333
+ const back = await reader.read({ tool: TOOL, sourceId: sessionId, path: paths.messages, extra: { dir } }, w.home)
334
+ const n = countTurns(back)
335
+ const pairs = unpaired(back)
336
+ if (n.user !== plan.expect.users || n.assistant !== plan.expect.assistants || n.tool_call !== plan.expect.calls || pairs.calls.length || pairs.results.length)
337
+ throw new Error(
338
+ `the new Mistral Vibe session did not read back as written (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}); nothing was kept`,
339
+ )
340
+ } catch (e) {
341
+ rmSync(dir, { recursive: true, force: true })
342
+ throw e
343
+ }
344
+ }
345
+ return {
346
+ tool: TOOL,
347
+ way: "import",
348
+ sessionId,
349
+ written: [paths.messages, paths.meta],
350
+ open: `cd ${shellQuote(cwd)} && vibe --resume ${sessionId}`,
351
+ note: "Vibe imports the session into its own store the first time it is resumed.",
352
+ lost: plan.lost,
353
+ dryRun: !!w.dryRun,
354
+ }
355
+ },
356
+ }
357
+ }
358
+
359
+ export const mistralVibeImportWriter = mistralVibeWriter()
@@ -0,0 +1,10 @@
1
+ import type { Reader, Writer } from "../types.ts"
2
+ import { reader } from "./reader.ts"
3
+ import { museNativeWriter } from "./writer.ts"
4
+
5
+ // Muse Code (Meta's coding agent): reads its durable session event streams under ~/.local/share/muse, and
6
+ // continues any session as a new native Muse session that `muse exec --session-id <id>` resumes.
7
+ // Format and lifecycle ported from session-migrate (xhluca/session-migrate, MIT), checked against Muse 0.2.1.
8
+ export { reader }
9
+ export const readers: Reader[] = [reader]
10
+ export const writers: Writer[] = [museNativeWriter]