@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,199 @@
1
+ import { join } from "node:path"
2
+ import { versionOf } from "../../fsutil.ts"
3
+ import type { Session, ToolKind, Turn, Usage } from "../../session/types.ts"
4
+ import { asText, clip, iso, lossCounter, noteModel, toolKindOf } from "../../session/util.ts"
5
+ import { CONTINUED_NOTE } from "../claude-code/records.ts"
6
+
7
+ // What Goose's sessions look like, shared by the reader and the writer. Checked against goose v1.57.0's
8
+ // source (crates/goose/src/session/session_manager.rs, import_formats/, goose-provider-types
9
+ // conversation/message.rs and tool_result_serde.rs, context_mgmt/mod.rs) and against rows the 1.57.0
10
+ // binary wrote itself into a temporary home. Catalogue: docs/sessions/2026-11-08-session-formats.md, "Goose".
11
+ //
12
+ // Since 1.10 every session is a row of ~/.local/share/goose/sessions/sessions.db (schema version 17 in
13
+ // 1.57.0) and every message a row whose content_json is the message's list of typed blocks. Before that a
14
+ // session was <YYYYMMDD_HHMMSS>.jsonl in the same folder: a metadata line, then one message per line. Goose
15
+ // copies those files into the database the first time it creates it, so they are read only when there is
16
+ // no database yet.
17
+
18
+ export const TOOL = "goose"
19
+ export const LABEL = "Goose"
20
+
21
+ /** Goose versions whose session JSON and import the writer was checked against. */
22
+ export const TESTED_VERSIONS = ["1.57.0"]
23
+
24
+ export const sessionsDir = (home: string) => join(home, ".local", "share", "goose", "sessions")
25
+ export const dbPath = (home: string) => join(sessionsDir(home), "sessions.db")
26
+
27
+ /** "1.57.0" from `goose --version` (which prints " 1.57.0"); undefined when goose is missing. */
28
+ export const gooseVersion = (bin = "goose") => versionOf(bin)?.match(/\d+\.\d+\.\d+/)?.[0]
29
+
30
+ /** The text Goose puts after its own compaction summary (context_mgmt CONVERSATION_CONTINUATION_TEXT). */
31
+ export const CONTINUATION_TEXT =
32
+ "Your context was compacted. The previous message contains a summary of the conversation so far.\nDo not mention that you read a summary or that conversation summarization occurred.\nJust continue the conversation naturally based on the summarized context."
33
+
34
+ /** One message as Goose serialises it (Message in goose-provider-types, camelCase). */
35
+ export interface GooseMessage {
36
+ id?: string | null
37
+ role: string
38
+ created: number
39
+ content: Array<Record<string, any>>
40
+ metadata?: {
41
+ userVisible?: boolean
42
+ agentVisible?: boolean
43
+ turnContext?: boolean
44
+ inference?: { provider?: string; requestedModel?: string; resolvedModel?: string }
45
+ usage?: { inputTokens?: number; outputTokens?: number; cacheReadTokens?: number; cacheWriteTokens?: number }
46
+ [k: string]: unknown
47
+ }
48
+ }
49
+
50
+ /**
51
+ * What a Goose tool does. Goose names a tool `<extension>__<tool>`; the developer extension's are shell,
52
+ * write, edit, tree and read_image in 1.57, and text_editor (with a `command`) in older versions.
53
+ */
54
+ export function gooseToolKind(name: string, input: unknown): ToolKind {
55
+ const short = name.includes("__") ? name.slice(name.lastIndexOf("__") + 2) : name
56
+ if (short === "tree") return "list"
57
+ if (short === "read_image") return "read"
58
+ if (short === "text_editor") {
59
+ const command = (input as { command?: unknown } | null)?.command
60
+ return command === "view" ? "read" : command === "write" ? "write" : "edit"
61
+ }
62
+ return toolKindOf(short)
63
+ }
64
+
65
+ /** The text of a tool result's content blocks (MCP CallToolResult content), and how many images it held. */
66
+ function resultText(content: unknown): { text: string; images: number } {
67
+ if (!Array.isArray(content)) return { text: asText(content), images: 0 }
68
+ const parts: string[] = []
69
+ let images = 0
70
+ for (const c of content as Array<Record<string, any>>) {
71
+ if (c?.type === "text" && typeof c.text === "string") parts.push(c.text)
72
+ else if (c?.type === "image") images++
73
+ else if (c?.type === "resource" && typeof c.resource?.text === "string") parts.push(c.resource.text)
74
+ else parts.push(asText(c))
75
+ }
76
+ return { text: parts.join("\n"), images }
77
+ }
78
+
79
+ /** Goose stores a failed call as the error's Display, "-32603: <message>", sometimes twice over. */
80
+ const errorText = (e: unknown) => asText(e).replace(/^(?:-?\d+: )+/, "")
81
+
82
+ const visible = (m: GooseMessage) => ({ user: m.metadata?.userVisible !== false, agent: m.metadata?.agentVisible !== false })
83
+
84
+ /**
85
+ * The portable turns of a Goose conversation. Messages the person saw are the history (including those a
86
+ * compaction hid from the model); an agent-only user message after them is Goose's compaction summary;
87
+ * Goose's own continuation text and per-turn context are not conversation. Returns the id of the session a
88
+ * brain-written conversation continues, when its note is there.
89
+ */
90
+ export function turnsOf(messages: GooseMessage[], h: Session["header"]): { turns: Turn[]; continued?: string } {
91
+ const turns: Turn[] = []
92
+ const lost = lossCounter()
93
+ let continued: string | undefined
94
+ let hiddenBefore = false
95
+ const isContinuation = (m: GooseMessage | undefined) =>
96
+ m?.role === "assistant" && m.metadata?.userVisible === false && m.content?.some((b) => typeof b?.text === "string" && b.text.startsWith("Your context was compacted"))
97
+ for (const [idx, m] of messages.entries()) {
98
+ const at = iso(m.created)
99
+ const stamp = at ? { at } : {}
100
+ const v = visible(m)
101
+ const content = Array.isArray(m.content) ? m.content : []
102
+ const texts = content.filter((b) => b?.type === "text" && typeof b.text === "string").map((b) => b.text as string)
103
+ if (!v.user && !v.agent) continue
104
+ if (m.metadata?.turnContext) continue
105
+ if (!v.user) {
106
+ const text = texts.join("\n")
107
+ const note = text.match(CONTINUED_NOTE)
108
+ if (note) continued = note[1]
109
+ else if (m.role === "user" && (hiddenBefore || isContinuation(messages[idx + 1])) && text.trim()) turns.push({ kind: "compaction", ...stamp, summary: text })
110
+ else if (!text.startsWith("Your context was compacted")) lost.add("Goose context messages (not carried)")
111
+ continue
112
+ }
113
+ if (!v.agent) hiddenBefore = true
114
+ const model = m.metadata?.inference?.resolvedModel ?? m.metadata?.inference?.requestedModel
115
+ if (m.role === "assistant") noteModel(h, model)
116
+ let usage: Usage | undefined
117
+ const u = m.metadata?.usage
118
+ if (u && m.role === "assistant")
119
+ usage = {
120
+ ...(u.inputTokens !== undefined ? { input: u.inputTokens } : {}),
121
+ ...(u.outputTokens !== undefined ? { output: u.outputTokens } : {}),
122
+ ...(u.cacheReadTokens !== undefined ? { cacheRead: u.cacheReadTokens } : {}),
123
+ ...(u.cacheWriteTokens !== undefined ? { cacheWrite: u.cacheWriteTokens } : {}),
124
+ }
125
+ let text = ""
126
+ const flushText = () => {
127
+ if (!text.trim()) return (text = "")
128
+ if (m.role === "user") turns.push({ kind: "user", ...stamp, text })
129
+ else {
130
+ turns.push({ kind: "assistant", ...stamp, text, ...(model ? { model } : {}), ...(usage ? { usage } : {}) })
131
+ usage = undefined
132
+ }
133
+ text = ""
134
+ }
135
+ for (const b of content) {
136
+ switch (b?.type) {
137
+ case "text":
138
+ text += (text ? "\n" : "") + String(b.text ?? "")
139
+ break
140
+ case "thinking":
141
+ flushText()
142
+ // Goose keeps Claude's signature beside the text; the text itself is readable.
143
+ if (typeof b.thinking === "string" && b.thinking.trim()) turns.push({ kind: "reasoning", ...stamp, text: b.thinking })
144
+ break
145
+ case "redactedThinking":
146
+ lost.add("redacted reasoning")
147
+ break
148
+ case "toolRequest": {
149
+ flushText()
150
+ const call = b.toolCall ?? {}
151
+ if (call.status === "success" && call.value && typeof call.value.name === "string") {
152
+ const input = call.value.arguments ?? {}
153
+ turns.push({ kind: "tool_call", ...stamp, id: String(b.id), name: call.value.name, toolKind: gooseToolKind(call.value.name, input), input })
154
+ } else {
155
+ // The model asked for a call Goose could not parse; the error is all there is.
156
+ turns.push({ kind: "tool_call", ...stamp, id: String(b.id), name: "invalid_tool_call", toolKind: "other", input: { error: errorText(call.error) } })
157
+ }
158
+ break
159
+ }
160
+ case "toolResponse": {
161
+ flushText()
162
+ const r = b.toolResult ?? {}
163
+ let output: string
164
+ let ok: boolean
165
+ if (r.status === "success") {
166
+ const value = Array.isArray(r.value) ? { content: r.value } : (r.value ?? {})
167
+ const { text: out, images } = resultText(value.content)
168
+ if (images) lost.add("images in tool results", images)
169
+ output = out
170
+ ok = value.isError !== true
171
+ } else {
172
+ output = errorText(r.error)
173
+ ok = false
174
+ }
175
+ const c = clip(output)
176
+ turns.push({ kind: "tool_result", ...stamp, callId: String(b.id), ok, output: c.text, ...(c.truncated ? { truncated: true } : {}) })
177
+ break
178
+ }
179
+ case "image":
180
+ lost.add("images")
181
+ break
182
+ case "document":
183
+ lost.add("documents")
184
+ break
185
+ case "toolConfirmationRequest":
186
+ case "actionRequired":
187
+ case "systemNotification":
188
+ case "error":
189
+ // Prompts and notices Goose shows while it works; not part of the conversation.
190
+ break
191
+ default:
192
+ lost.add(`unknown content types: ${String(b?.type)}`)
193
+ }
194
+ }
195
+ flushText()
196
+ }
197
+ lost.flush(h)
198
+ return { turns, ...(continued ? { continued } : {}) }
199
+ }
@@ -0,0 +1,340 @@
1
+ import { copyFileSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs"
2
+ import { dirname, extname, join, resolve } from "node:path"
3
+ import { toolLabel } from "../../brief.ts"
4
+ import { exists, isDir } from "../../fsutil.ts"
5
+ import type { Session, Turn } from "../../session/types.ts"
6
+ import { countTurns, firstUserText, unpaired } from "../../session/util.ts"
7
+ import { continuedNote } from "../claude-code/records.ts"
8
+ import type { Readiness, WriteOptions, WriteResult, Writer } from "../types.ts"
9
+ import { gooseReader } from "./reader.ts"
10
+ import { CONTINUATION_TEXT, type GooseMessage, LABEL, TESTED_VERSIONS, TOOL, dbPath, gooseVersion } from "./records.ts"
11
+
12
+ // Continues a portable session in Goose through Goose's own importer: the session is written as the JSON
13
+ // `goose session export --format json` produces (the shape import_formats::build_session_json builds), and
14
+ // `goose session import <file>` stores it as a new session, with an id Goose picks itself (YYYYMMDD_N),
15
+ // in Goose's own transaction. That is safer than inserting rows: Goose migrates its schema, allocates the
16
+ // id and writes with its own locking, so it works while Goose Desktop is open and before the database exists.
17
+ //
18
+ // Tool calls stay structural: toolRequest blocks in an assistant message, answered by toolResponse blocks
19
+ // in the next user message, as Goose writes them. Shell, write and edit calls are mapped onto Goose's
20
+ // developer tools (developer__shell, __write, __edit) when their arguments say what Goose needs; others keep
21
+ // their names. Readable reasoning stays as an unsigned thinking block, which Goose itself makes of plain
22
+ // reasoning. A compaction is written the way Goose compacts: the turns before it stay on screen but hidden
23
+ // from the model, then the summary and Goose's continuation text, visible to the model only. The note
24
+ // saying where the session came from is a model-only message too, so it is never shown as the person's words.
25
+
26
+ const IMAGE_TYPES: Record<string, string> = { ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".gif": "image/gif", ".webp": "image/webp" }
27
+ const IMAGE_LIMIT = 5 * 1024 * 1024
28
+ const NO_RESULT = "No result was recorded for this call; the session moved before it finished."
29
+
30
+ const shellQuote = (s: string) => (/^[\w./@%+=:,-]+$/.test(s) ? s : `'${s.replace(/'/g, `'\\''`)}'`)
31
+
32
+ type Call = Extract<Turn, { kind: "tool_call" }>
33
+ type Meta = NonNullable<GooseMessage["metadata"]>
34
+ const SHOWN: Meta = { userVisible: true, agentVisible: true }
35
+ const MODEL_ONLY: Meta = { userVisible: false, agentVisible: true }
36
+ const SCREEN_ONLY: Meta = { userVisible: true, agentVisible: false }
37
+ const NEITHER: Meta = { userVisible: false, agentVisible: false }
38
+
39
+ const str = (v: unknown) => (typeof v === "string" ? v : undefined)
40
+
41
+ /** A call as Goose's developer extension would have made it, when the arguments carry what Goose needs. */
42
+ export function gooseCall(c: Call): { name: string; args: Record<string, unknown> } | undefined {
43
+ const i = (c.input && typeof c.input === "object" && !Array.isArray(c.input) ? c.input : {}) as Record<string, unknown>
44
+ const path = str(i.file_path) ?? str(i.path) ?? str(i.filePath) ?? str(i.target_file) ?? str(i.absolute_path)
45
+ if (c.toolKind === "shell") {
46
+ const command = str(i.command) ?? str(i.cmd) ?? (Array.isArray(i.command) ? (i.command as unknown[]).map(String).join(" ") : undefined)
47
+ return command ? { name: "developer__shell", args: { command } } : undefined
48
+ }
49
+ if (c.toolKind === "write") {
50
+ const content = str(i.content) ?? str(i.file_text) ?? str(i.contents)
51
+ return path && content !== undefined ? { name: "developer__write", args: { path, content } } : undefined
52
+ }
53
+ if (c.toolKind === "edit") {
54
+ const before = str(i.old_string) ?? str(i.old_str) ?? str(i.oldText)
55
+ const after = str(i.new_string) ?? str(i.new_str) ?? str(i.newText)
56
+ return path && before !== undefined && after !== undefined ? { name: "developer__edit", args: { path, before, after } } : undefined
57
+ }
58
+ return undefined
59
+ }
60
+
61
+ export interface GoosePlan {
62
+ /** The session JSON `goose session import` takes. */
63
+ json: Record<string, unknown>
64
+ lost: string[]
65
+ /** What a read-back must find. */
66
+ expect: { users: number; assistants: number; calls: number }
67
+ }
68
+
69
+ /** The import JSON for `session` continued in `cwd`. Pure apart from reading attached images. */
70
+ export function planGooseSession(session: Session, opts: { cwd: string; now: Date }): GoosePlan {
71
+ const drops = new Map<string, number>()
72
+ const drop = (what: string, n = 1) => drops.set(what, (drops.get(what) ?? 0) + n)
73
+ const label = toolLabel(session.header.tool)
74
+ const turns = session.turns
75
+ const lastCompaction = turns.map((t) => t.kind).lastIndexOf("compaction")
76
+ const expect = { users: 0, assistants: 0, calls: 0 }
77
+
78
+ // Which calls are answered somewhere, so the ones that never are can be closed where they stand.
79
+ const answered = new Set(turns.filter((t) => t.kind === "tool_result").map((t) => (t as Extract<Turn, { kind: "tool_result" }>).callId))
80
+
81
+ // Tool call ids: providers take [A-Za-z0-9_-]; others are mapped once, the same way for call and result.
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 = /^[A-Za-z0-9_-]{1,64}$/.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
+ // Message times are seconds and never go backwards: Goose orders a conversation by them.
104
+ const known = turns.map((t) => (t.at ? Date.parse(t.at) : Number.NaN)).find(Number.isFinite)
105
+ let clock = Math.floor((known ?? opts.now.getTime()) / 1000)
106
+ const created = (at?: string) => {
107
+ const t = at ? Math.floor(Date.parse(at) / 1000) : Number.NaN
108
+ if (Number.isFinite(t) && t > clock) clock = t
109
+ return clock
110
+ }
111
+
112
+ const out: GooseMessage[] = []
113
+ /** Set when the next block must open a new message even if the last one has the same role. */
114
+ let split = false
115
+ /** Adds a block to the last message when it has the same role, visibility and kind, else opens a new one. */
116
+ const add = (role: "user" | "assistant", block: Record<string, unknown>, meta: Meta, at?: string) => {
117
+ const last = out[out.length - 1]
118
+ const results = block.type === "toolResponse"
119
+ const same =
120
+ !split &&
121
+ last &&
122
+ last.role === role &&
123
+ last.metadata?.userVisible === meta.userVisible &&
124
+ last.metadata?.agentVisible === meta.agentVisible &&
125
+ (role === "assistant" || last.content.every((b) => (b.type === "toolResponse") === results))
126
+ split = false
127
+ if (same) last.content.push(block)
128
+ else out.push({ id: null, role, created: created(at), content: [block], metadata: { ...meta } })
129
+ }
130
+ const assistantHasCalls = () => {
131
+ const last = out[out.length - 1]
132
+ return last?.role === "assistant" && last.content.some((b) => b.type === "toolRequest")
133
+ }
134
+ const response = (id: string, ok: boolean, output: string, meta: Meta, at?: string) =>
135
+ add("user", { type: "toolResponse", id: callId(id), toolResult: { status: "success", value: { content: [{ type: "text", text: output || "(no output)" }], isError: !ok } } }, meta, at)
136
+
137
+ add("user", { type: "text", text: continuedNote(label, session.header.id, opts.now.toISOString().slice(0, 10)) }, MODEL_ONLY, turns[0]?.at)
138
+ const written = new Set<string>()
139
+ const done = new Set<string>()
140
+
141
+ for (const [i, t] of turns.entries()) {
142
+ const meta = i < lastCompaction ? SCREEN_ONLY : SHOWN
143
+ switch (t.kind) {
144
+ case "user": {
145
+ const blocks: Array<Record<string, unknown>> = []
146
+ let text = t.text
147
+ for (const a of t.attachments ?? []) {
148
+ if (!a.path) continue // bytes the reader could not keep; it already counted them
149
+ if (a.kind === "file") {
150
+ text += `\n\n[Attached file: ${a.path}]`
151
+ continue
152
+ }
153
+ const mime = a.mime ?? IMAGE_TYPES[extname(a.path).toLowerCase()]
154
+ let size = Number.POSITIVE_INFINITY
155
+ try {
156
+ size = statSync(a.path).size
157
+ } catch {}
158
+ if (mime && Object.values(IMAGE_TYPES).includes(mime) && size <= IMAGE_LIMIT) blocks.push({ type: "image", data: readFileSync(a.path).toString("base64"), mimeType: mime })
159
+ else drop("images that could not be attached")
160
+ }
161
+ if (!text.trim() && !blocks.length) break
162
+ out.push({ id: null, role: "user", created: created(t.at), content: [{ type: "text", text: text.trim() ? text : "(image)" }, ...blocks], metadata: { ...meta } })
163
+ expect.users++
164
+ break
165
+ }
166
+ case "assistant":
167
+ if (!t.text.trim()) break
168
+ // Text after a tool call was written after its result came back: a new message.
169
+ if (assistantHasCalls()) split = true
170
+ add("assistant", { type: "text", text: t.text }, meta, t.at)
171
+ expect.assistants++
172
+ break
173
+ case "reasoning":
174
+ if (!t.text.trim()) break
175
+ if (assistantHasCalls()) split = true
176
+ // Goose keeps plain reasoning as thinking without a signature (it migrates `reasoning` blocks so).
177
+ add("assistant", { type: "thinking", thinking: t.text, signature: "" }, meta, t.at)
178
+ break
179
+ case "tool_call": {
180
+ if (written.has(t.id)) {
181
+ drop("repeated tool call ids")
182
+ break
183
+ }
184
+ written.add(t.id)
185
+ const mapped = gooseCall(t)
186
+ const args = mapped?.args ?? (t.input && typeof t.input === "object" && !Array.isArray(t.input) ? t.input : { input: t.input ?? null })
187
+ add("assistant", { type: "toolRequest", id: callId(t.id), toolCall: { status: "success", value: { name: mapped?.name ?? toolName(t.name), arguments: args } } }, meta, t.at)
188
+ expect.calls++
189
+ if (!answered.has(t.id)) {
190
+ response(t.id, false, NO_RESULT, meta, t.at)
191
+ done.add(t.id)
192
+ drop("tool calls without a result (closed as errors)")
193
+ }
194
+ break
195
+ }
196
+ case "tool_result": {
197
+ if (!written.has(t.callId)) {
198
+ drop("tool results without a call")
199
+ break
200
+ }
201
+ if (done.has(t.callId)) {
202
+ drop("repeated tool results")
203
+ break
204
+ }
205
+ done.add(t.callId)
206
+ response(t.callId, t.ok, t.fullPath ? `${t.output}\n\n[Full output: ${t.fullPath}]` : t.output, meta, t.at)
207
+ break
208
+ }
209
+ case "compaction": {
210
+ const summary = `[Summary of the earlier conversation in ${label}, which replaced the turns before it]\n\n${t.summary}`
211
+ if (i === lastCompaction) {
212
+ add("user", { type: "text", text: summary }, MODEL_ONLY, t.at)
213
+ add("assistant", { type: "text", text: CONTINUATION_TEXT }, MODEL_ONLY, t.at)
214
+ } else add("user", { type: "text", text: summary }, NEITHER, t.at)
215
+ break
216
+ }
217
+ case "file_edit":
218
+ if (!t.callId || !written.has(t.callId)) drop("file edits outside a tool call")
219
+ break
220
+ case "subagent":
221
+ drop("subagent transcripts (their results stay in the parent's tool results)")
222
+ break
223
+ }
224
+ }
225
+ const conversation = out
226
+
227
+ const title = session.header.title ?? firstUserText(session)?.replace(/\s+/g, " ").trim().slice(0, 60) ?? "Session"
228
+ const first = new Date((conversation[0]?.created ?? Math.floor(opts.now.getTime() / 1000)) * 1000).toISOString()
229
+ const last = new Date((conversation[conversation.length - 1]?.created ?? Math.floor(opts.now.getTime() / 1000)) * 1000).toISOString()
230
+ const usage = { input_tokens: null, output_tokens: null, total_tokens: null, cache_read_input_tokens: null, cache_write_input_tokens: null }
231
+ const json = {
232
+ id: "imported",
233
+ working_dir: opts.cwd,
234
+ name: `${title} (from ${label})`,
235
+ user_set_name: true,
236
+ session_type: "user",
237
+ created_at: first,
238
+ updated_at: last,
239
+ extension_data: {},
240
+ usage,
241
+ accumulated_usage: usage,
242
+ accumulated_cost: null,
243
+ schedule_id: null,
244
+ recipe: null,
245
+ user_recipe_values: null,
246
+ conversation,
247
+ message_count: conversation.filter((m) => m.metadata?.userVisible !== false).length,
248
+ provider_name: null,
249
+ model_config: null,
250
+ goose_mode: "auto",
251
+ archived_at: null,
252
+ project_id: null,
253
+ }
254
+ const lost = [...drops].map(([what, n]) => `${what} (${n})`)
255
+ if (remapped) lost.push(`tool call ids, remapped to ids providers accept (${remapped})`)
256
+ if (renamed) lost.push(`tool names changed to fit Goose (${renamed})`)
257
+ return { json, lost, expect }
258
+ }
259
+
260
+ /** The goose binary: the installer's ~/.local/bin/goose first, else whatever is on PATH. */
261
+ function resolveBin(home: string): { path: string; command: string } {
262
+ const own = join(home, ".local", "bin", "goose")
263
+ if (exists(own)) return { path: own, command: Bun.which("goose") === own ? "goose" : own }
264
+ return { path: "goose", command: "goose" }
265
+ }
266
+
267
+ const stamp = (d: Date) => d.toISOString().replace(/[:.]/g, "-")
268
+
269
+ /** The Goose writer; `bin` and `version` are for tests, which must not depend on an installed Goose. */
270
+ export function gooseWriter(opts: { bin?: string; version?: string } = {}): Writer {
271
+ const binFor = (home: string) => (opts.bin ? { path: opts.bin, command: opts.bin } : resolveBin(home))
272
+ const installed = (home: string) => opts.version ?? gooseVersion(binFor(home).path)
273
+ return {
274
+ tool: TOOL,
275
+ label: LABEL,
276
+ way: "import",
277
+
278
+ async ready(w: WriteOptions): Promise<Readiness> {
279
+ const v = installed(w.home)
280
+ if (!v) return { ok: false, why: "Goose is not installed (`goose --version` did not run)" }
281
+ if (!TESTED_VERSIONS.includes(v)) return { ok: false, why: `Goose ${v} is not a version this writer was checked against (${TESTED_VERSIONS.join(", ")})` }
282
+ if (!isDir(w.cwd)) return { ok: false, why: `the folder to continue in does not exist: ${w.cwd}` }
283
+ return { ok: true }
284
+ },
285
+
286
+ async write(session: Session, w: WriteOptions): Promise<WriteResult> {
287
+ const now = new Date()
288
+ const cwd = resolve(w.cwd)
289
+ const plan = planGooseSession(session, { cwd, now })
290
+ const file = join(w.home, ".brain", "exports", `goose-${stamp(now)}.json`)
291
+ const db = dbPath(w.home)
292
+ const bin = binFor(w.home)
293
+ if (w.dryRun)
294
+ return {
295
+ tool: TOOL,
296
+ way: "import",
297
+ written: [file, db],
298
+ open: `cd ${shellQuote(cwd)} && ${shellQuote(bin.command)} session import ${shellQuote(file)}, then ${shellQuote(bin.command)} session --resume --session-id <the id it prints> --history`,
299
+ lost: plan.lost,
300
+ dryRun: true,
301
+ }
302
+
303
+ mkdirSync(dirname(file), { recursive: true })
304
+ writeFileSync(file, `${JSON.stringify(plan.json, null, 2)}\n`, { flag: "wx" })
305
+ // Goose writes the rows itself; a copy of the database as it was comes first all the same.
306
+ if (exists(db)) {
307
+ const backup = join(w.home, ".brain", "backups", TOOL, stamp(now))
308
+ mkdirSync(backup, { recursive: true })
309
+ for (const side of ["", "-wal", "-shm"]) if (exists(db + side)) copyFileSync(db + side, join(backup, `sessions.db${side}`))
310
+ }
311
+ const env = { ...process.env, HOME: w.home }
312
+ const run = Bun.spawnSync([bin.path, "session", "import", file], { cwd, env, stdout: "pipe", stderr: "pipe", timeout: 60_000 })
313
+ const printed = run.stdout.toString()
314
+ const id = printed.split("Session imported:")[1]?.trim().match(/^(\S+) - /)?.[1]
315
+ if (run.exitCode !== 0 || !id) throw new Error(`goose session import failed (exit ${run.exitCode}): ${(run.stderr.toString() || printed).trim().slice(0, 500)}`)
316
+
317
+ // Read it back as the reader reads Goose; a session that does not round-trip is removed again.
318
+ const back = await gooseReader.read({ tool: TOOL, sourceId: id, path: db }, w.home)
319
+ const n = countTurns(back)
320
+ const pairs = unpaired(back)
321
+ if (n.user !== plan.expect.users || n.assistant !== plan.expect.assistants || n.tool_call !== plan.expect.calls || pairs.calls.length || pairs.results.length) {
322
+ Bun.spawnSync([bin.path, "session", "remove", "--session-id", id], { cwd, env, stdout: "pipe", stderr: "pipe", timeout: 60_000 })
323
+ throw new Error(
324
+ `the imported Goose 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}); it was removed`,
325
+ )
326
+ }
327
+ return {
328
+ tool: TOOL,
329
+ way: "import",
330
+ sessionId: id,
331
+ written: [file, db],
332
+ open: `cd ${shellQuote(cwd)} && ${shellQuote(bin.command)} session --resume --session-id ${id} --history`,
333
+ lost: plan.lost,
334
+ dryRun: false,
335
+ }
336
+ },
337
+ }
338
+ }
339
+
340
+ export const gooseImportWriter = gooseWriter()
@@ -0,0 +1,44 @@
1
+ // Grok checks every image in chat_history.jsonl when it loads a session (crates/.../session/image_normalize.rs) and moves
2
+ // the WHOLE file aside as chat_history.jsonl.corrupt when one fails, so a writer must not hand it an image it would reject.
3
+ // Measured against Grok 0.2.103 with the real binary: each side must be at least 8 px and the area at least 512 px
4
+ // (22x23 is rejected, 23x23 and 8x64 are accepted); 10000x10000 is accepted (its ceiling is above anything under our 5 MB limit).
5
+
6
+ export const MIN_SIDE = 8
7
+ export const MIN_AREA = 512
8
+
9
+ /** Width and height read from the file header of a PNG, JPEG, GIF or WebP; undefined when it cannot be read. */
10
+ export function imageSize(b: Buffer): { width: number; height: number } | undefined {
11
+ if (b.length > 24 && b.readUInt32BE(0) === 0x89504e47 && b.toString("latin1", 12, 16) === "IHDR") return { width: b.readUInt32BE(16), height: b.readUInt32BE(20) }
12
+ if (b.length > 10 && (b.toString("latin1", 0, 6) === "GIF87a" || b.toString("latin1", 0, 6) === "GIF89a")) return { width: b.readUInt16LE(6), height: b.readUInt16LE(8) }
13
+ if (b.length > 30 && b.toString("latin1", 0, 4) === "RIFF" && b.toString("latin1", 8, 12) === "WEBP") {
14
+ const kind = b.toString("latin1", 12, 16)
15
+ if (kind === "VP8X") return { width: 1 + b.readUIntLE(24, 3), height: 1 + b.readUIntLE(27, 3) }
16
+ if (kind === "VP8L" && b[20] === 0x2f) {
17
+ const bits = b.readUInt32LE(21)
18
+ return { width: 1 + (bits & 0x3fff), height: 1 + ((bits >> 14) & 0x3fff) }
19
+ }
20
+ if (kind === "VP8 ") return { width: b.readUInt16LE(26) & 0x3fff, height: b.readUInt16LE(28) & 0x3fff }
21
+ return undefined
22
+ }
23
+ if (b.length > 4 && b[0] === 0xff && b[1] === 0xd8) {
24
+ let i = 2
25
+ while (i + 9 < b.length) {
26
+ if (b[i] !== 0xff) return undefined
27
+ const m = b[i + 1]!
28
+ if (m === 0xff) { i++; continue }
29
+ if (m === 0xd8 || (m >= 0xd0 && m <= 0xd7) || m === 0x01) { i += 2; continue }
30
+ const len = b.readUInt16BE(i + 2)
31
+ if (m >= 0xc0 && m <= 0xcf && m !== 0xc4 && m !== 0xc8 && m !== 0xcc) return { width: b.readUInt16BE(i + 7), height: b.readUInt16BE(i + 5) }
32
+ i += 2 + len
33
+ }
34
+ }
35
+ return undefined
36
+ }
37
+
38
+ /** Why Grok would reject this image on load, or undefined when it takes it. */
39
+ export function grokRejects(b: Buffer): string | undefined {
40
+ const s = imageSize(b)
41
+ if (!s) return "unreadable image header"
42
+ if (s.width < MIN_SIDE || s.height < MIN_SIDE || s.width * s.height < MIN_AREA) return `too small (${s.width}x${s.height}; Grok needs sides of at least ${MIN_SIDE} px and ${MIN_AREA} px in all)`
43
+ return undefined
44
+ }
@@ -0,0 +1,8 @@
1
+ import type { Reader, Writer } from "../types.ts"
2
+ import { grokReader } from "./reader.ts"
3
+ import { grokNativeWriter } from "./writer.ts"
4
+
5
+ // xAI Grok CLI (grok, "Grok Build"): reads its session folders and continues any session as a new
6
+ // native Grok session that `grok --resume` opens.
7
+ export const readers: Reader[] = [grokReader]
8
+ export const writers: Writer[] = [grokNativeWriter]