@100xprompt/brain 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (213) hide show
  1. package/LICENSE +48 -0
  2. package/README.md +172 -2
  3. package/package.json +48 -4
  4. package/src/adapters/aider/index.ts +8 -0
  5. package/src/adapters/aider/reader.ts +129 -0
  6. package/src/adapters/aider/records.ts +220 -0
  7. package/src/adapters/aider/writer.ts +269 -0
  8. package/src/adapters/amazon-q-cli/index.ts +8 -0
  9. package/src/adapters/amazon-q-cli/reader.ts +84 -0
  10. package/src/adapters/amazon-q-cli/records.ts +191 -0
  11. package/src/adapters/amazon-q-cli/writer.ts +370 -0
  12. package/src/adapters/amazon-q-ide/index.ts +9 -0
  13. package/src/adapters/amazon-q-ide/reader.ts +131 -0
  14. package/src/adapters/amazon-q-ide/records.ts +141 -0
  15. package/src/adapters/amazon-q-ide/writer.ts +325 -0
  16. package/src/adapters/antigravity/index.ts +9 -0
  17. package/src/adapters/antigravity/proto.ts +194 -0
  18. package/src/adapters/antigravity/reader.ts +424 -0
  19. package/src/adapters/antigravity/records.ts +236 -0
  20. package/src/adapters/antigravity/writer.ts +463 -0
  21. package/src/adapters/auggie/index.ts +36 -0
  22. package/src/adapters/claude-code/index.ts +8 -0
  23. package/src/adapters/claude-code/reader.ts +639 -0
  24. package/src/adapters/claude-code/records.ts +88 -0
  25. package/src/adapters/claude-code/writer.ts +329 -0
  26. package/src/adapters/cline/index.ts +12 -0
  27. package/src/adapters/cline/reader.ts +432 -0
  28. package/src/adapters/cline/records.ts +151 -0
  29. package/src/adapters/cline/vscdb.ts +56 -0
  30. package/src/adapters/cline/writer.ts +656 -0
  31. package/src/adapters/cline/xml.ts +177 -0
  32. package/src/adapters/cline4/index.ts +9 -0
  33. package/src/adapters/cline4/reader.ts +179 -0
  34. package/src/adapters/cline4/records.ts +92 -0
  35. package/src/adapters/cline4/writer.ts +361 -0
  36. package/src/adapters/codebuff/index.ts +8 -0
  37. package/src/adapters/codebuff/project.ts +207 -0
  38. package/src/adapters/codebuff/reader.ts +215 -0
  39. package/src/adapters/codebuff/records.ts +173 -0
  40. package/src/adapters/codebuff/writer.ts +383 -0
  41. package/src/adapters/codex/index.ts +8 -0
  42. package/src/adapters/codex/native.ts +239 -0
  43. package/src/adapters/codex/reader.ts +747 -0
  44. package/src/adapters/codex/records.ts +114 -0
  45. package/src/adapters/codex/writer.ts +479 -0
  46. package/src/adapters/continue/index.ts +9 -0
  47. package/src/adapters/continue/reader.ts +68 -0
  48. package/src/adapters/continue/records.ts +418 -0
  49. package/src/adapters/continue/writer.ts +162 -0
  50. package/src/adapters/copilot-cli/index.ts +9 -0
  51. package/src/adapters/copilot-cli/reader.ts +186 -0
  52. package/src/adapters/copilot-cli/records.ts +241 -0
  53. package/src/adapters/copilot-cli/writer.ts +353 -0
  54. package/src/adapters/crush/index.ts +8 -0
  55. package/src/adapters/crush/reader.ts +180 -0
  56. package/src/adapters/crush/records.ts +169 -0
  57. package/src/adapters/crush/writer.ts +333 -0
  58. package/src/adapters/cursor-cli/index.ts +405 -0
  59. package/src/adapters/cursor-ide/agentkv.ts +95 -0
  60. package/src/adapters/cursor-ide/index.ts +501 -0
  61. package/src/adapters/cursor-ide/transcripts.ts +122 -0
  62. package/src/adapters/cursor-ide/workspaces.ts +119 -0
  63. package/src/adapters/cursor-ide/writer.ts +170 -0
  64. package/src/adapters/devin/index.ts +8 -0
  65. package/src/adapters/devin/reader.ts +305 -0
  66. package/src/adapters/devin/records.ts +277 -0
  67. package/src/adapters/devin/writer.ts +523 -0
  68. package/src/adapters/factory-droid/index.ts +12 -0
  69. package/src/adapters/factory-droid/reader.ts +184 -0
  70. package/src/adapters/factory-droid/records.ts +79 -0
  71. package/src/adapters/factory-droid/writer.ts +350 -0
  72. package/src/adapters/gemini-cli/exchanges.ts +228 -0
  73. package/src/adapters/gemini-cli/index.ts +396 -0
  74. package/src/adapters/gemini-cli/records.ts +521 -0
  75. package/src/adapters/gemini-cli/writer.ts +354 -0
  76. package/src/adapters/goose/index.ts +8 -0
  77. package/src/adapters/goose/reader.ts +138 -0
  78. package/src/adapters/goose/records.ts +199 -0
  79. package/src/adapters/goose/writer.ts +340 -0
  80. package/src/adapters/grok-cli/image.ts +44 -0
  81. package/src/adapters/grok-cli/index.ts +8 -0
  82. package/src/adapters/grok-cli/reader.ts +302 -0
  83. package/src/adapters/grok-cli/records.ts +172 -0
  84. package/src/adapters/grok-cli/writer.ts +329 -0
  85. package/src/adapters/hermes/index.ts +8 -0
  86. package/src/adapters/hermes/reader.ts +249 -0
  87. package/src/adapters/hermes/records.ts +285 -0
  88. package/src/adapters/hermes/writer.ts +452 -0
  89. package/src/adapters/iflow-cli/index.ts +72 -0
  90. package/src/adapters/iflow-cli/records.ts +215 -0
  91. package/src/adapters/iflow-cli/writer.ts +204 -0
  92. package/src/adapters/index.ts +112 -0
  93. package/src/adapters/kilo-cli/index.ts +21 -0
  94. package/src/adapters/kilo-code/index.ts +11 -0
  95. package/src/adapters/kilo-code/writer.ts +92 -0
  96. package/src/adapters/kimi/index.ts +9 -0
  97. package/src/adapters/kimi/reader.ts +92 -0
  98. package/src/adapters/kimi/records.ts +221 -0
  99. package/src/adapters/kimi/writer.ts +367 -0
  100. package/src/adapters/kiro-cli/index.ts +8 -0
  101. package/src/adapters/kiro-cli/reader.ts +87 -0
  102. package/src/adapters/kiro-cli/records.ts +260 -0
  103. package/src/adapters/kiro-cli/writer.ts +305 -0
  104. package/src/adapters/kiro-ide/index.ts +8 -0
  105. package/src/adapters/kiro-ide/writer.ts +52 -0
  106. package/src/adapters/letta/index.ts +8 -0
  107. package/src/adapters/letta/reader.ts +177 -0
  108. package/src/adapters/letta/records.ts +119 -0
  109. package/src/adapters/letta/writer.ts +413 -0
  110. package/src/adapters/mastracode/index.ts +8 -0
  111. package/src/adapters/mastracode/reader.ts +323 -0
  112. package/src/adapters/mastracode/records.ts +336 -0
  113. package/src/adapters/mastracode/writer.ts +385 -0
  114. package/src/adapters/mistral-vibe/index.ts +9 -0
  115. package/src/adapters/mistral-vibe/reader.ts +62 -0
  116. package/src/adapters/mistral-vibe/records.ts +104 -0
  117. package/src/adapters/mistral-vibe/writer.ts +359 -0
  118. package/src/adapters/muse/index.ts +10 -0
  119. package/src/adapters/muse/reader.ts +332 -0
  120. package/src/adapters/muse/records.ts +104 -0
  121. package/src/adapters/muse/writer.ts +387 -0
  122. package/src/adapters/omp/index.ts +13 -0
  123. package/src/adapters/omp/reader.ts +314 -0
  124. package/src/adapters/omp/records.ts +153 -0
  125. package/src/adapters/omp/writer.ts +216 -0
  126. package/src/adapters/opencode/ids.ts +52 -0
  127. package/src/adapters/opencode/index.ts +31 -0
  128. package/src/adapters/opencode/note.ts +8 -0
  129. package/src/adapters/opencode/reader.ts +452 -0
  130. package/src/adapters/opencode/tools.ts +174 -0
  131. package/src/adapters/opencode/v2.ts +367 -0
  132. package/src/adapters/opencode/writer.ts +379 -0
  133. package/src/adapters/openhands/index.ts +8 -0
  134. package/src/adapters/openhands/reader.ts +222 -0
  135. package/src/adapters/openhands/records.ts +136 -0
  136. package/src/adapters/openhands/writer.ts +404 -0
  137. package/src/adapters/pearai/index.ts +10 -0
  138. package/src/adapters/pi/index.ts +28 -0
  139. package/src/adapters/pi/reader.ts +261 -0
  140. package/src/adapters/pi/records.ts +99 -0
  141. package/src/adapters/pi/writer.ts +362 -0
  142. package/src/adapters/qoder-cli/index.ts +8 -0
  143. package/src/adapters/qoder-cli/reader.ts +83 -0
  144. package/src/adapters/qoder-cli/records.ts +45 -0
  145. package/src/adapters/qoder-cli/writer.ts +72 -0
  146. package/src/adapters/qwen-code/index.ts +84 -0
  147. package/src/adapters/qwen-code/records.ts +194 -0
  148. package/src/adapters/qwen-code/writer.ts +224 -0
  149. package/src/adapters/tabnine-cli/index.ts +8 -0
  150. package/src/adapters/tabnine-cli/reader.ts +106 -0
  151. package/src/adapters/tabnine-cli/records.ts +33 -0
  152. package/src/adapters/tabnine-cli/writer.ts +156 -0
  153. package/src/adapters/types.ts +73 -0
  154. package/src/adapters/void/index.ts +9 -0
  155. package/src/adapters/void/reader.ts +48 -0
  156. package/src/adapters/void/records.ts +285 -0
  157. package/src/adapters/void/writer.ts +93 -0
  158. package/src/adapters/vscode-copilot/index.ts +9 -0
  159. package/src/adapters/vscode-copilot/reader.ts +212 -0
  160. package/src/adapters/vscode-copilot/records.ts +388 -0
  161. package/src/adapters/vscode-copilot/shared.ts +68 -0
  162. package/src/adapters/vscode-copilot/writer.ts +546 -0
  163. package/src/adapters/zed/index.ts +8 -0
  164. package/src/adapters/zed/reader.ts +198 -0
  165. package/src/adapters/zed/records.ts +87 -0
  166. package/src/adapters/zed/writer.ts +341 -0
  167. package/src/brief-writers.ts +71 -0
  168. package/src/brief.ts +238 -0
  169. package/src/cli.ts +326 -0
  170. package/src/fsutil.ts +133 -0
  171. package/src/gateway/connect.ts +845 -0
  172. package/src/gateway/signin.ts +323 -0
  173. package/src/handoff.ts +104 -0
  174. package/src/hooks.ts +455 -0
  175. package/src/memory/audit.ts +514 -0
  176. package/src/memory/blobs.ts +94 -0
  177. package/src/memory/cli.ts +117 -0
  178. package/src/memory/distill.ts +381 -0
  179. package/src/memory/externalize.ts +435 -0
  180. package/src/memory/lookups.ts +70 -0
  181. package/src/memory/mcp-install.ts +401 -0
  182. package/src/memory/mcp-server.ts +296 -0
  183. package/src/memory/prepare.ts +179 -0
  184. package/src/memory/preview.ts +154 -0
  185. package/src/memory/project.ts +35 -0
  186. package/src/memory/redact.ts +72 -0
  187. package/src/memory/relevance.ts +187 -0
  188. package/src/memory/repomap.ts +516 -0
  189. package/src/memory/store.ts +270 -0
  190. package/src/memory/sync.ts +166 -0
  191. package/src/memory/tidy.ts +193 -0
  192. package/src/memory/types.ts +118 -0
  193. package/src/org/admin-ops.ts +96 -0
  194. package/src/org/auth.ts +112 -0
  195. package/src/org/backup.ts +115 -0
  196. package/src/org/cli.ts +438 -0
  197. package/src/org/client.ts +404 -0
  198. package/src/org/config.ts +44 -0
  199. package/src/org/crypto.ts +537 -0
  200. package/src/org/db.ts +435 -0
  201. package/src/org/graph.ts +318 -0
  202. package/src/org/oidc.ts +435 -0
  203. package/src/org/policy.ts +398 -0
  204. package/src/org/server.ts +920 -0
  205. package/src/org/sso.ts +316 -0
  206. package/src/org/tls.ts +66 -0
  207. package/src/org/types.ts +193 -0
  208. package/src/self.ts +68 -0
  209. package/src/session/attachments.ts +53 -0
  210. package/src/session/peekcache.ts +50 -0
  211. package/src/session/types.ts +104 -0
  212. package/src/session/util.ts +130 -0
  213. package/src/sqlite.ts +48 -0
@@ -0,0 +1,302 @@
1
+ import { basename, dirname, join } from "node:path"
2
+ import { exists, isDir, ls, readJson, readJsonl } from "../../fsutil.ts"
3
+ import type { Session, SessionHeader, Turn } from "../../session/types.ts"
4
+ import { clip, header, iso, lossCounter, noteModel, toolKindOf } from "../../session/util.ts"
5
+ import type { Reader, SessionRef } from "../types.ts"
6
+ import { CONTINUED_NOTE, LABEL, SUMMARY_PREFIX, SYNTHETIC, TOOL, grokKind, sessionsDir, unwrapReminder } from "./records.ts"
7
+
8
+ // Reads Grok CLI sessions: <home>/.grok/sessions/<encoded cwd>/<uuid>/. The conversation comes from
9
+ // chat_history.jsonl, the messages Grok gives the model (tool calls paired with results by id);
10
+ // updates.jsonl adds what only the screen log has: when each prompt and tool call happened and whether a
11
+ // call failed. Subagents are sessions of their own in the same tree, linked from the parent's
12
+ // subagents/<id>/meta.json and named in the spawn_subagent result.
13
+
14
+ type Row = Record<string, any>
15
+
16
+ interface Summary {
17
+ info?: { id?: string; cwd?: string }
18
+ generated_title?: string
19
+ session_summary?: string
20
+ created_at?: string
21
+ updated_at?: string
22
+ last_active_at?: string
23
+ current_model_id?: string
24
+ session_kind?: string
25
+ git_remotes?: string[]
26
+ head_branch?: string
27
+ head_commit?: string
28
+ }
29
+
30
+ const textOf = (content: unknown): { text: string; images: number } => {
31
+ if (typeof content === "string") return { text: content, images: 0 }
32
+ let images = 0
33
+ const parts: string[] = []
34
+ for (const p of Array.isArray(content) ? (content as Row[]) : []) {
35
+ if (typeof p?.text === "string") parts.push(p.text)
36
+ else if (p?.type === "image" || p?.type === "image_url") images++
37
+ }
38
+ return { text: parts.join("\n"), images }
39
+ }
40
+
41
+ /** What updates.jsonl knows that chat_history.jsonl does not: prompt and call times, failed calls. */
42
+ function screenLog(path: string) {
43
+ const promptAt = new Map<number, string>()
44
+ const callAt = new Map<string, string>()
45
+ const resultAt = new Map<string, string>()
46
+ const failed = new Set<string>()
47
+ for (const o of readJsonl(path).rows) {
48
+ const u = o?.params?.update as Row | undefined
49
+ const at = iso(o?.params?._meta?.agentTimestampMs) ?? iso(o?.timestamp)
50
+ if (!u || !at) continue
51
+ if (u.sessionUpdate === "user_message_chunk" && typeof u._meta?.promptIndex === "number" && !promptAt.has(u._meta.promptIndex)) promptAt.set(u._meta.promptIndex, at)
52
+ if (u.sessionUpdate === "tool_call" && typeof u.toolCallId === "string") callAt.set(u.toolCallId, at)
53
+ if (u.sessionUpdate === "tool_call_update" && typeof u.toolCallId === "string") {
54
+ if (u.status === "failed") failed.add(u.toolCallId)
55
+ if (u.status === "completed" || u.status === "failed") resultAt.set(u.toolCallId, at)
56
+ }
57
+ }
58
+ return { promptAt, callAt, resultAt, failed }
59
+ }
60
+
61
+ /**
62
+ * The conversation from updates.jsonl alone, for a folder with no chat_history.jsonl (the sanitized grok 1.0.5
63
+ * capture in the session-migrate corpus has only summary.json and updates.jsonl). The screen log has the
64
+ * same turns: prompts (user_message_chunk), reply text in chunks, tool_call and its tool_call_update with
65
+ * the result; it lacks the model's own reasoning items and system reminders.
66
+ */
67
+ function turnsFromScreenLog(path: string, h: SessionHeader, model: string | undefined, lost: ReturnType<typeof lossCounter>): { turns: Turn[]; spawnResults: Array<{ callId: string; text: string }> } {
68
+ const turns: Turn[] = []
69
+ const spawnResults: Array<{ callId: string; text: string }> = []
70
+ const names = new Map<string, string>()
71
+ const answered = new Set<string>()
72
+ let streaming: "user" | "assistant" | "reasoning" | undefined
73
+ let userModel = model
74
+ const append = (kind: "user" | "assistant" | "reasoning", text: string, at: string | undefined) => {
75
+ const last = turns[turns.length - 1]
76
+ if (streaming === kind && last?.kind === kind) {
77
+ ;(last as { text: string }).text += text
78
+ return
79
+ }
80
+ streaming = kind
81
+ if (kind === "assistant") {
82
+ noteModel(h, userModel)
83
+ turns.push({ kind, ...(at ? { at } : {}), text, ...(userModel ? { model: userModel } : {}) })
84
+ } else turns.push({ kind, ...(at ? { at } : {}), text })
85
+ }
86
+ for (const o of readJsonl(path).rows) {
87
+ const u = o?.params?.update as Row | undefined
88
+ if (!u || typeof u.sessionUpdate !== "string") continue
89
+ const at = iso(o?.params?._meta?.agentTimestampMs) ?? iso(o?.timestamp)
90
+ const chunk = typeof u.content?.text === "string" ? (u.content.text as string) : ""
91
+ switch (u.sessionUpdate) {
92
+ case "user_message_chunk":
93
+ if (typeof u._meta?.modelId === "string") userModel = u._meta.modelId
94
+ // Chunks of one prompt share a promptIndex; a new index is a new prompt.
95
+ if (streaming === "user" && (turns[turns.length - 1] as { promptIndex?: number } | undefined)?.promptIndex === u._meta?.promptIndex) append("user", chunk, at)
96
+ else {
97
+ streaming = undefined
98
+ append("user", chunk, at)
99
+ ;(turns[turns.length - 1] as { promptIndex?: unknown }).promptIndex = u._meta?.promptIndex
100
+ }
101
+ break
102
+ case "agent_message_chunk":
103
+ append("assistant", chunk, at)
104
+ break
105
+ case "agent_thought_chunk":
106
+ append("reasoning", chunk, at)
107
+ break
108
+ case "tool_call": {
109
+ if (typeof u.toolCallId !== "string") break
110
+ streaming = undefined
111
+ const name = typeof u._meta?.["x.ai/tool"]?.name === "string" ? (u._meta["x.ai/tool"].name as string) : typeof u.title === "string" ? u.title : "unknown"
112
+ names.set(u.toolCallId, name)
113
+ turns.push({ kind: "tool_call", ...(at ? { at } : {}), id: u.toolCallId, name, toolKind: grokKind(name) ?? toolKindOf(name), input: u.rawInput ?? {} })
114
+ break
115
+ }
116
+ case "tool_call_update": {
117
+ // The first update of a call only restates it (kind, title, locations); the one with a status is the result.
118
+ if (typeof u.toolCallId !== "string" || (u.status !== "completed" && u.status !== "failed") || answered.has(u.toolCallId)) break
119
+ answered.add(u.toolCallId)
120
+ streaming = undefined
121
+ let images = 0
122
+ const parts: string[] = []
123
+ for (const c of Array.isArray(u.content) ? (u.content as Row[]) : []) {
124
+ if (typeof c?.content?.text === "string") parts.push(c.content.text)
125
+ else if (c?.content?.type === "image") images++
126
+ }
127
+ if (images) lost.add("images", images)
128
+ const raw = parts.join("\n")
129
+ const { text, truncated } = clip(raw)
130
+ turns.push({ kind: "tool_result", ...(at ? { at } : {}), callId: u.toolCallId, ok: u.status === "completed", output: text, ...(truncated ? { truncated } : {}) })
131
+ if (names.get(u.toolCallId) === "spawn_subagent") spawnResults.push({ callId: u.toolCallId, text: raw })
132
+ break
133
+ }
134
+ case "turn_completed":
135
+ streaming = undefined
136
+ break
137
+ default:
138
+ break
139
+ }
140
+ }
141
+ for (const t of turns) delete (t as { promptIndex?: unknown }).promptIndex
142
+ return { turns: turns.filter((t) => (t.kind === "user" || t.kind === "assistant" || t.kind === "reasoning" ? t.text.trim() : true)), spawnResults }
143
+ }
144
+
145
+ /** One session folder as a portable session (children not attached). */
146
+ function readOne(dir: string): { session: Session; spawnResults: Array<{ callId: string; text: string }> } {
147
+ const summary = readJson<Summary>(join(dir, "summary.json")) ?? {}
148
+ const id = summary.info?.id ?? basename(dir)
149
+ const screenOnly = !exists(join(dir, "chat_history.jsonl")) && exists(join(dir, "updates.jsonl"))
150
+ if (!exists(join(dir, "chat_history.jsonl")) && !screenOnly) throw new Error(`${dir} has no chat_history.jsonl; Grok keeps the conversation there`)
151
+ const cwd = summary.info?.cwd
152
+ const h = header(TOOL, id, {
153
+ sourcePath: dir,
154
+ ...(summary.generated_title || summary.session_summary ? { title: summary.generated_title || summary.session_summary } : {}),
155
+ project: {
156
+ ...(cwd ? { cwd } : {}),
157
+ ...(summary.git_remotes?.[0] ? { gitRemote: summary.git_remotes[0] } : {}),
158
+ ...(summary.head_branch ? { gitBranch: summary.head_branch } : {}),
159
+ ...(summary.head_commit ? { gitCommit: summary.head_commit } : {}),
160
+ },
161
+ ...(iso(summary.created_at) ? { startedAt: iso(summary.created_at) } : {}),
162
+ ...(iso(summary.last_active_at ?? summary.updated_at) ? { updatedAt: iso(summary.last_active_at ?? summary.updated_at) } : {}),
163
+ })
164
+ const lost = lossCounter()
165
+ if (screenOnly) {
166
+ const r = turnsFromScreenLog(join(dir, "updates.jsonl"), h, summary.current_model_id, lost)
167
+ lost.add("model history (no chat_history.jsonl: read from the screen log)")
168
+ lost.flush(h)
169
+ return { session: { header: h, turns: r.turns }, spawnResults: r.spawnResults }
170
+ }
171
+ const log = screenLog(join(dir, "updates.jsonl"))
172
+ const { rows, bad } = readJsonl(join(dir, "chat_history.jsonl"))
173
+ if (bad) lost.add("unreadable chat history lines", bad)
174
+
175
+ const turns: Turn[] = []
176
+ const names = new Map<string, string>()
177
+ const spawnResults: Array<{ callId: string; text: string }> = []
178
+ for (const r of rows) {
179
+ switch (r.type) {
180
+ case "system":
181
+ // Grok's own system prompt; it writes a fresh one when the session loads.
182
+ break
183
+ case "user": {
184
+ const { text, images } = textOf(r.content)
185
+ if (images) lost.add("images", images)
186
+ if (r[SYNTHETIC]) {
187
+ const inner = unwrapReminder(text)
188
+ const from = inner.match(CONTINUED_NOTE)
189
+ if (from) h.parent = { id: from[1]!, kind: "continued" }
190
+ else if (inner.startsWith(SUMMARY_PREFIX)) turns.push({ kind: "compaction", summary: inner.replace(/^\[[^\]]*\]\s*/, "") })
191
+ else lost.add("Grok reminders and context (not carried)")
192
+ break
193
+ }
194
+ if (!text.trim()) break
195
+ const at = typeof r.prompt_index === "number" ? log.promptAt.get(r.prompt_index) : undefined
196
+ turns.push({ kind: "user", ...(at ? { at } : {}), text })
197
+ break
198
+ }
199
+ case "reasoning": {
200
+ const summaryText = (Array.isArray(r.summary) ? (r.summary as Row[]) : []).map((s) => s?.text).filter((t): t is string => typeof t === "string").join("\n")
201
+ if (summaryText.trim()) turns.push({ kind: "reasoning", text: summaryText })
202
+ if (r.encrypted_content) lost.add("encrypted reasoning")
203
+ break
204
+ }
205
+ case "assistant": {
206
+ const model = typeof r.model_id === "string" ? r.model_id : undefined
207
+ noteModel(h, model)
208
+ const text = typeof r.content === "string" ? r.content : textOf(r.content).text
209
+ if (text.trim()) turns.push({ kind: "assistant", text, ...(model ? { model } : {}) })
210
+ for (const c of Array.isArray(r.tool_calls) ? (r.tool_calls as Row[]) : []) {
211
+ if (typeof c?.id !== "string" || typeof c?.name !== "string") continue
212
+ let input: unknown = c.arguments
213
+ if (typeof c.arguments === "string") {
214
+ try {
215
+ input = JSON.parse(c.arguments)
216
+ } catch {
217
+ input = c.arguments
218
+ }
219
+ }
220
+ names.set(c.id, c.name)
221
+ const at = log.callAt.get(c.id)
222
+ turns.push({ kind: "tool_call", ...(at ? { at } : {}), id: c.id, name: c.name, toolKind: grokKind(c.name) ?? toolKindOf(c.name), input })
223
+ }
224
+ break
225
+ }
226
+ case "tool_result": {
227
+ if (typeof r.tool_call_id !== "string") break
228
+ const raw = typeof r.content === "string" ? r.content : textOf(r.content).text
229
+ const { text, truncated } = clip(raw)
230
+ const at = log.resultAt.get(r.tool_call_id)
231
+ turns.push({ kind: "tool_result", ...(at ? { at } : {}), callId: r.tool_call_id, ok: !log.failed.has(r.tool_call_id), output: text, ...(truncated ? { truncated } : {}) })
232
+ if (names.get(r.tool_call_id) === "spawn_subagent") spawnResults.push({ callId: r.tool_call_id, text: raw })
233
+ break
234
+ }
235
+ default:
236
+ lost.add(`unknown chat history types: ${String(r.type)}`)
237
+ }
238
+ }
239
+ lost.flush(h)
240
+ return { session: { header: h, turns }, spawnResults }
241
+ }
242
+
243
+ /** Subagent sessions named in the parent's subagents/ folder, each placed after the spawn call whose result names it. */
244
+ function attachChildren(dir: string, parent: Session, spawnResults: Array<{ callId: string; text: string }>) {
245
+ const sub = join(dir, "subagents")
246
+ const kids: Array<{ session: Session; callId?: string; description?: string }> = []
247
+ for (const childId of ls(sub)) {
248
+ const meta = readJson<Row>(join(sub, childId, "meta.json"))
249
+ const childDir = join(dirname(dir), childId)
250
+ if (!meta || !isDir(childDir)) continue
251
+ try {
252
+ const child = readOne(childDir).session
253
+ child.header.parent = { id: parent.header.id, kind: "subagent" }
254
+ const callId = spawnResults.find((r) => r.text.includes(childId))?.callId
255
+ kids.push({ session: child, ...(callId ? { callId } : {}), ...(typeof meta.description === "string" ? { description: meta.description } : {}) })
256
+ } catch {
257
+ parent.header.lost.push("an unreadable subagent session")
258
+ }
259
+ }
260
+ if (!kids.length) return
261
+ const out: Turn[] = []
262
+ const unplaced = kids.filter((k) => !k.callId)
263
+ for (const t of parent.turns) {
264
+ out.push(t)
265
+ if (t.kind !== "tool_result") continue
266
+ for (const k of kids.filter((k) => k.callId === t.callId))
267
+ out.push({ kind: "subagent", ...(k.session.header.startedAt ? { at: k.session.header.startedAt } : {}), sessionId: k.session.header.id, callId: t.callId, ...(k.description ? { description: k.description } : {}) })
268
+ }
269
+ for (const k of unplaced) out.push({ kind: "subagent", ...(k.session.header.startedAt ? { at: k.session.header.startedAt } : {}), sessionId: k.session.header.id, ...(k.description ? { description: k.description } : {}) })
270
+ parent.turns = out
271
+ parent.children = kids.map((k) => k.session)
272
+ }
273
+
274
+ export const grokReader: Reader = {
275
+ tool: TOOL,
276
+ label: LABEL,
277
+
278
+ async list(home: string): Promise<SessionRef[]> {
279
+ const refs: SessionRef[] = []
280
+ const root = sessionsDir(home)
281
+ for (const group of ls(root)) {
282
+ const groupDir = join(root, group)
283
+ if (!isDir(groupDir)) continue
284
+ for (const name of ls(groupDir)) {
285
+ const dir = join(groupDir, name)
286
+ const s = readJson<Summary>(join(dir, "summary.json"))
287
+ // Subagent sessions are read as part of their parent.
288
+ if (!s?.info?.id || s.session_kind === "subagent" || !(exists(join(dir, "chat_history.jsonl")) || exists(join(dir, "updates.jsonl")))) continue
289
+ const title = s.generated_title || s.session_summary
290
+ const updatedAt = iso(s.last_active_at ?? s.updated_at)
291
+ refs.push({ tool: TOOL, sourceId: s.info.id, path: dir, ...(title ? { title } : {}), ...(s.info.cwd ? { cwd: s.info.cwd } : {}), ...(updatedAt ? { updatedAt } : {}) })
292
+ }
293
+ }
294
+ return refs.sort((a, b) => (b.updatedAt ?? "").localeCompare(a.updatedAt ?? ""))
295
+ },
296
+
297
+ async read(ref: SessionRef): Promise<Session> {
298
+ const { session, spawnResults } = readOne(ref.path)
299
+ attachChildren(ref.path, session, spawnResults)
300
+ return session
301
+ },
302
+ }
@@ -0,0 +1,172 @@
1
+ import { readFileSync } from "node:fs"
2
+ import { join } from "node:path"
3
+ import { exists, versionOf } from "../../fsutil.ts"
4
+ import type { ToolKind } from "../../session/types.ts"
5
+
6
+ // What the xAI Grok CLI ("Grok Build", the official closed-source binary) keeps per session, as far as
7
+ // this adapter relies on it. Checked against grok 0.2.103: the sessions it wrote on the author's Mac
8
+ // (structure and counts only), sessions it created itself in a throwaway GROK_HOME, and what its own
9
+ // `grok sessions list`, `grok export` and ACP `session/load` + `_x.ai/session/info` do with written ones.
10
+ //
11
+ // A session is a folder <GROK_HOME>/sessions/<url-encoded cwd>/<uuidv7>/ holding two parallel logs:
12
+ // updates.jsonl, the ACP session/update stream the screen is rebuilt from (export and the resume replay
13
+ // read only this), and chat_history.jsonl, the messages the model is given (session/load rebuilds the
14
+ // model context only from this; a session without it resumes with an empty context). summary.json is the
15
+ // entry the session list reads. The search index (sessions/session_search.sqlite) is rebuilt by Grok.
16
+
17
+ export const TOOL = "grok-cli"
18
+ export const LABEL = "Grok CLI"
19
+
20
+ /** Grok versions the writer's files were checked against. */
21
+ export const TESTED_VERSIONS = ["0.2.103"]
22
+
23
+ export const grokHome = (home: string) => join(home, ".grok")
24
+ export const sessionsDir = (home: string) => join(grokHome(home), "sessions")
25
+
26
+ /**
27
+ * The folder name Grok gives a cwd: percent-encoding of everything outside RFC 3986's unreserved set
28
+ * (checked with spaces, @, +, comma, parentheses and ~ in the path). A cwd is not resolved first: /tmp
29
+ * and /private/tmp are different folders.
30
+ */
31
+ export function encodeCwd(cwd: string): string {
32
+ return encodeURIComponent(cwd).replace(/[!'()*]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`)
33
+ }
34
+
35
+ /** Past 255 bytes Grok names the folder slug-plus-hash instead, with a hash this adapter does not reproduce. */
36
+ export const MAX_FOLDER_NAME = 255
37
+
38
+ /** Messages Grok adds for the model and marks as not typed by the person (all wrapped in <system-reminder>). */
39
+ export const SYNTHETIC = "synthetic_reason"
40
+
41
+ /** The installed Grok version ("grok 0.2.103 (89c3d36fb6f1)" → "0.2.103"), from the PATH or Grok's own bin folder. */
42
+ export function grokVersion(home: string): string | undefined {
43
+ const own = join(grokHome(home), "bin", "grok")
44
+ const line = versionOf("grok") ?? (exists(own) ? versionOf(own) : undefined)
45
+ return line?.match(/(\d+\.\d+\.\d+)/)?.[1]
46
+ }
47
+
48
+ /** The model new sessions start with: [models] default in config.toml, else Grok's documented default. */
49
+ export function defaultModel(home: string): string {
50
+ let text = ""
51
+ try {
52
+ text = readFileSync(join(grokHome(home), "config.toml"), "utf8")
53
+ } catch {
54
+ return "grok-build"
55
+ }
56
+ const section = text.split(/^\s*\[/m).find((s) => /^models\]/.test(s)) ?? ""
57
+ return section.match(/^\s*default\s*=\s*["']([^"']+)["']/m)?.[1] ?? "grok-build"
58
+ }
59
+
60
+ /** Grok's built-in tools, by what they do, with the metadata its own tool_call updates carry. */
61
+ export interface GrokTool {
62
+ name: string
63
+ /** `x.ai/tool.kind` */
64
+ kind: string
65
+ label: string
66
+ namespace: string
67
+ readOnly: boolean
68
+ /** ACP ToolKind for the update's `kind`. */
69
+ acp: string
70
+ }
71
+
72
+ export const GROK_TOOLS: Record<string, GrokTool> = {
73
+ read_file: { name: "read_file", kind: "read", label: "Read", namespace: "grok_build", readOnly: true, acp: "read" },
74
+ run_terminal_command: { name: "run_terminal_command", kind: "execute", label: "Run Command", namespace: "grok_build", readOnly: false, acp: "execute" },
75
+ search_replace: { name: "search_replace", kind: "edit", label: "Edit", namespace: "grok_build", readOnly: false, acp: "edit" },
76
+ write: { name: "write", kind: "write", label: "Write", namespace: "opencode", readOnly: false, acp: "edit" },
77
+ list_dir: { name: "list_dir", kind: "list", label: "List Files", namespace: "grok_build", readOnly: true, acp: "search" },
78
+ grep: { name: "grep", kind: "search", label: "Search", namespace: "grok_build", readOnly: true, acp: "search" },
79
+ web_search: { name: "web_search", kind: "web_search", label: "Web Search", namespace: "grok_build", readOnly: true, acp: "fetch" },
80
+ todo_write: { name: "todo_write", kind: "plan", label: "Plan", namespace: "grok_build", readOnly: false, acp: "other" },
81
+ spawn_subagent: { name: "spawn_subagent", kind: "task", label: "Subagent", namespace: "grok_build", readOnly: false, acp: "other" },
82
+ ask_user_question: { name: "ask_user_question", kind: "ask_user", label: "Ask User", namespace: "grok_build", readOnly: true, acp: "other" },
83
+ }
84
+
85
+ /** What a Grok tool name does; Grok's names are checked before the shared name patterns. */
86
+ const KIND_OF: Record<string, ToolKind> = {
87
+ read_file: "read",
88
+ run_terminal_command: "shell",
89
+ get_command_or_subagent_output: "shell",
90
+ search_replace: "edit",
91
+ write: "write",
92
+ list_dir: "list",
93
+ grep: "search",
94
+ search_tool: "search",
95
+ web_search: "web",
96
+ todo_write: "todo",
97
+ spawn_subagent: "task",
98
+ ask_user_question: "ask",
99
+ use_tool: "mcp",
100
+ }
101
+ export const grokKind = (name: string): ToolKind | undefined => KIND_OF[name]
102
+
103
+ const pick = (o: Record<string, unknown>, keys: string[]): unknown => {
104
+ for (const k of keys) if (o[k] !== undefined && o[k] !== null && o[k] !== "") return o[k]
105
+ return undefined
106
+ }
107
+ const str = (v: unknown) => (typeof v === "string" ? v : Array.isArray(v) && v.every((x) => typeof x === "string") ? v.join(" ") : undefined)
108
+
109
+ /**
110
+ * A call from another tool as the Grok tool that does the same thing, with Grok's argument names, so the
111
+ * model sees calls it knows and the screen renders them like its own. Calls whose arguments do not map
112
+ * cleanly (an apply_patch, a filename glob, an MCP call) keep their own name and input.
113
+ */
114
+ export function asGrokCall(kind: ToolKind, name: string, input: unknown): { tool?: GrokTool; name: string; input: unknown } {
115
+ if (GROK_TOOLS[name]) return { tool: GROK_TOOLS[name], name, input }
116
+ if (!input || typeof input !== "object" || Array.isArray(input)) return { name, input }
117
+ const o = input as Record<string, unknown>
118
+ const path = str(pick(o, ["file_path", "target_file", "path", "filePath", "absolute_path", "filename"]))
119
+ const as = (tool: string, args: Record<string, unknown>) => ({ tool: GROK_TOOLS[tool], name: tool, input: args })
120
+ switch (kind) {
121
+ case "read":
122
+ if (path) return as("read_file", { target_file: path, ...(typeof o.offset === "number" ? { offset: o.offset } : {}), ...(typeof o.limit === "number" ? { limit: o.limit } : {}) })
123
+ break
124
+ case "shell": {
125
+ const command = str(pick(o, ["command", "cmd", "commandLine"]))
126
+ if (command) return as("run_terminal_command", { command, ...(typeof o.description === "string" ? { description: o.description } : {}) })
127
+ break
128
+ }
129
+ case "edit": {
130
+ const oldS = str(pick(o, ["old_string", "old_str", "oldText", "old_text"]))
131
+ const newS = pick(o, ["new_string", "new_str", "newText", "new_text"])
132
+ if (path && oldS !== undefined && typeof newS === "string")
133
+ return as("search_replace", { file_path: path, old_string: oldS, new_string: newS, ...(o.replace_all === true ? { replace_all: true } : {}) })
134
+ break
135
+ }
136
+ case "write": {
137
+ const content = pick(o, ["content", "contents", "file_text", "fileText", "text"])
138
+ if (path && typeof content === "string") return as("write", { file_path: path, content })
139
+ break
140
+ }
141
+ case "list": {
142
+ const dir = str(pick(o, ["target_directory", "path", "dir_path", "directory", "relative_workspace_path"]))
143
+ if (dir) return as("list_dir", { target_directory: dir })
144
+ break
145
+ }
146
+ case "search": {
147
+ // Only content searches: a filename glob has a pattern too, but grep would read it as a regex.
148
+ const pattern = str(pick(o, ["pattern", "regex", "query"]))
149
+ if (/grep|ripgrep|search_files/i.test(name) && pattern) return as("grep", { pattern, ...(path ? { path } : {}), ...(typeof o.glob === "string" ? { glob: o.glob } : {}) })
150
+ break
151
+ }
152
+ case "web": {
153
+ const query = str(pick(o, ["query", "q"]))
154
+ if (/search/i.test(name) && query) return as("web_search", { query })
155
+ break
156
+ }
157
+ default:
158
+ break
159
+ }
160
+ return { name, input }
161
+ }
162
+
163
+ /** The note the writer puts first; the reader recognises it and records where the session came from. */
164
+ export const continuedNote = (label: string, id: string, when: string) =>
165
+ `This session continues ${label} session ${id}, carried over by brain session on ${when}. The conversation after this note is its history; reasoning and anything listed as lost did not carry over. Check files before relying on details, since they may have changed.`
166
+ export const CONTINUED_NOTE = /This session continues .+? session (\S+), carried over by brain session/
167
+
168
+ /** The start of a compaction summary the writer gives the model in place of the turns it replaced. */
169
+ export const SUMMARY_PREFIX = "[Summary of the earlier conversation"
170
+
171
+ export const reminder = (text: string) => `<system-reminder>\n${text}\n</system-reminder>`
172
+ export const unwrapReminder = (text: string) => text.replace(/^\s*<system-reminder>\n?/, "").replace(/\n?<\/system-reminder>\s*$/, "")