@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,329 @@
1
+ import { mkdirSync, rmSync, writeFileSync } from "node:fs"
2
+ import { join, resolve } from "node:path"
3
+ import { toolLabel } from "../../brief.ts"
4
+ import { exists, gitInfo, isDir } from "../../fsutil.ts"
5
+ import { imageDataUrl, parseDataUrl } from "../../session/attachments.ts"
6
+ import type { Project, Session, Turn } from "../../session/types.ts"
7
+ import { countTurns, firstUserText, unpaired } from "../../session/util.ts"
8
+ import type { Readiness, WriteOptions, WriteResult, Writer } from "../types.ts"
9
+ import { grokRejects } from "./image.ts"
10
+ import { grokReader } from "./reader.ts"
11
+ import {
12
+ LABEL, MAX_FOLDER_NAME, SUMMARY_PREFIX, SYNTHETIC, TESTED_VERSIONS, TOOL,
13
+ asGrokCall, continuedNote, defaultModel, encodeCwd, grokHome, grokVersion, reminder, sessionsDir,
14
+ } from "./records.ts"
15
+
16
+ // Continues a portable session in the Grok CLI as a new session folder with the three files Grok's own
17
+ // sessions need: summary.json (the session list), updates.jsonl (what the screen replays) and
18
+ // chat_history.jsonl (what the model is given). Both logs are written because Grok reads them for
19
+ // different things: a session with only updates.jsonl resumes with the earlier turns on screen and an
20
+ // empty model context; one with only chat_history.jsonl is "not found".
21
+ //
22
+ // Tool calls stay structural in both logs: assistant tool_calls paired with tool_result messages for the
23
+ // model, tool_call / tool_call_update events for the screen, mapped to Grok's own tools where the
24
+ // arguments map (asGrokCall), with ids in Grok's call-<uuid>-<n> style. The note saying where the session
25
+ // came from is a synthetic system reminder, the way Grok marks context it adds itself, and it is not on
26
+ // screen. No system message is written: Grok puts its current system prompt in on load (checked: a
27
+ // stored one, short or long, does not change the context it builds, and none is inserted when missing).
28
+ //
29
+ // A compaction follows Grok's own: the screen log keeps every turn, the model's history starts from the
30
+ // summary. Readable reasoning is shown on screen but not given to the model, which only replays its own
31
+ // encrypted reasoning items.
32
+
33
+ type Row = Record<string, unknown>
34
+ type Call = Extract<Turn, { kind: "tool_call" }>
35
+ type Result = Extract<Turn, { kind: "tool_result" }>
36
+
37
+ /** Grok's timestamps: RFC 3339 with microseconds. */
38
+ const micros = (ms: number) => new Date(ms).toISOString().replace(/\.(\d{3})Z$/, ".$1000Z")
39
+
40
+ const shellQuote = (s: string) => (/^[\w./@%+=:,-]+$/.test(s) ? s : `'${s.replace(/'/g, `'\\''`)}'`)
41
+
42
+ /** Function names the chat completions API accepts. */
43
+ const fnName = (name: string) => name.replace(/[^A-Za-z0-9_-]/g, "_").slice(0, 64) || "tool"
44
+
45
+ /** Arguments must be a JSON object; a freeform input (an apply_patch) is wrapped. */
46
+ const asObject = (input: unknown): Record<string, unknown> =>
47
+ input && typeof input === "object" && !Array.isArray(input) ? (input as Record<string, unknown>) : { input: input ?? null }
48
+
49
+ export interface GrokPlan {
50
+ summary: Row
51
+ updates: Row[]
52
+ chat: Row[]
53
+ lost: string[]
54
+ expect: { users: number; assistants: number; calls: number }
55
+ }
56
+
57
+ /** The three files of a new Grok session `id` continuing `session`. Pure: writes nothing. */
58
+ export function planGrokSession(
59
+ session: Session,
60
+ opts: { id: string; now: Date; cwd: string; home: string; model: string; git?: Omit<Project, "cwd"> },
61
+ ): GrokPlan {
62
+ const drops = new Map<string, number>()
63
+ const drop = (what: string, n = 1) => drops.set(what, (drops.get(what) ?? 0) + n)
64
+ const label = toolLabel(session.header.tool)
65
+ const { id } = opts
66
+ const turns = session.turns
67
+ const lastCompaction = turns.map((t) => t.kind).lastIndexOf("compaction")
68
+
69
+ const results = new Map<string, Result>()
70
+ const callIds = new Set<string>()
71
+ for (const t of turns) {
72
+ if (t.kind === "tool_result" && !results.has(t.callId)) results.set(t.callId, t)
73
+ if (t.kind === "tool_call") callIds.add(t.id)
74
+ }
75
+
76
+ // Grok's call ids: call-<uuid>-<n>, numbered through the session.
77
+ const callUuid = crypto.randomUUID()
78
+ const ids = new Map<string, string>()
79
+ const grokId = (callId: string) => {
80
+ let v = ids.get(callId)
81
+ if (!v) ids.set(callId, (v = `call-${callUuid}-${ids.size}`))
82
+ return v
83
+ }
84
+
85
+ // Event times never go backwards; a turn without a time takes the one before it plus a millisecond.
86
+ const known = turns.map((t) => (t.at ? Date.parse(t.at) : Number.NaN)).find(Number.isFinite)
87
+ let clock = (known ?? opts.now.getTime()) - 1
88
+ const stamp = (at?: string) => {
89
+ const t = at ? Date.parse(at) : Number.NaN
90
+ clock = Number.isFinite(t) && t > clock ? t : clock + 1
91
+ return clock
92
+ }
93
+
94
+ const updates: Row[] = []
95
+ let events = 0
96
+ let promptId: string | undefined
97
+ const event = (update: Row, at?: string) => {
98
+ const ms = stamp(at)
99
+ events++
100
+ updates.push({
101
+ method: "session/update",
102
+ params: { sessionId: id, update, _meta: { agentTimestampMs: ms, eventId: `${id}-${events}`, ...(promptId ? { promptId } : {}) } },
103
+ timestamp: Math.floor(ms / 1000),
104
+ })
105
+ }
106
+ const endPrompt = () => {
107
+ if (promptId) event({ sessionUpdate: "turn_completed", prompt_id: promptId, stop_reason: "end_turn" })
108
+ promptId = undefined
109
+ }
110
+
111
+ const chat: Row[] = [
112
+ { type: "user", content: [{ type: "text", text: reminder(continuedNote(label, session.header.id, opts.now.toISOString().slice(0, 10))) }], [SYNTHETIC]: "system_reminder" },
113
+ ]
114
+ const expect = { users: 0, assistants: 0, calls: 0 }
115
+ let promptIndex = 0
116
+ let model = session.header.models[session.header.models.length - 1] ?? opts.model
117
+
118
+ // The model's side: an assistant message gathers text and calls until a result comes back.
119
+ let cur: { text: string; model: string; calls: Call[] } | undefined
120
+ const flush = () => {
121
+ if (!cur) return
122
+ const m = cur
123
+ cur = undefined
124
+ if (!m.text.trim() && !m.calls.length) return
125
+ chat.push({
126
+ type: "assistant",
127
+ content: m.text,
128
+ model_id: m.model,
129
+ ...(m.calls.length ? { tool_calls: m.calls.map((c) => ({ id: grokId(c.id), name: fnName(asGrokCall(c.toolKind, c.name, c.input).name), arguments: JSON.stringify(asObject(asGrokCall(c.toolKind, c.name, c.input).input)) })) } : {}),
130
+ })
131
+ if (m.text.trim()) expect.assistants++
132
+ for (const c of m.calls) {
133
+ const r = results.get(c.id)
134
+ if (!r) drop("tool calls without a result (closed as failed)")
135
+ chat.push({ type: "tool_result", tool_call_id: grokId(c.id), content: r ? r.output + (r.fullPath ? `\n\n[Full output: ${r.fullPath}]` : "") : "No result was recorded for this call." })
136
+ }
137
+ }
138
+
139
+ // The screen's side, event by event; calls whose result never comes are closed as failed.
140
+ const screenCall = (c: Call) => {
141
+ const g = asGrokCall(c.toolKind, c.name, c.input)
142
+ event(
143
+ {
144
+ sessionUpdate: "tool_call",
145
+ toolCallId: grokId(c.id),
146
+ title: g.tool?.label ?? c.name,
147
+ kind: g.tool?.acp ?? "other",
148
+ status: "pending",
149
+ rawInput: asObject(g.input),
150
+ _meta: { "x.ai/tool": { name: g.name, kind: g.tool?.kind ?? "other", label: g.tool?.label ?? c.name, namespace: g.tool?.namespace ?? "external", read_only: g.tool?.readOnly ?? false, version: 1 } },
151
+ },
152
+ c.at,
153
+ )
154
+ if (!results.has(c.id)) screenResult(c.id, false, "No result was recorded for this call.", c.at)
155
+ }
156
+ const screenResult = (callId: string, ok: boolean, output: string, at?: string) =>
157
+ event({ sessionUpdate: "tool_call_update", toolCallId: grokId(callId), status: ok ? "completed" : "failed", content: [{ type: "content", content: { type: "text", text: output } }] }, at)
158
+
159
+ const seen = new Set<string>()
160
+ const answered = new Set<string>()
161
+ for (const [i, t] of turns.entries()) {
162
+ const toModel = i > lastCompaction
163
+ switch (t.kind) {
164
+ case "user": {
165
+ // Grok stores a pasted image in the user message as a content block {type:"image", url:<data URL>} (found by sending one with --prompt-json).
166
+ const images: Row[] = []
167
+ for (const a of t.attachments ?? []) {
168
+ const url = imageDataUrl(a)
169
+ const bytes = url && toModel ? Buffer.from(parseDataUrl(url)?.base64 ?? "", "base64") : undefined
170
+ // An image Grok would reject on load makes it move the whole history aside, so it stays behind.
171
+ if (url && bytes && !grokRejects(bytes)) images.push({ type: "image", url })
172
+ else drop(url && bytes && toModel ? "attachments Grok would reject (too small or unreadable)" : "attachments", 1)
173
+ }
174
+ if (!t.text.trim()) {
175
+ if (images.length) drop("attachments without text", images.length)
176
+ break
177
+ }
178
+ endPrompt()
179
+ promptId = crypto.randomUUID()
180
+ event({ sessionUpdate: "user_message_chunk", content: { type: "text", text: t.text }, _meta: { modelId: opts.model, promptIndex } }, t.at)
181
+ if (toModel) {
182
+ flush()
183
+ chat.push({ type: "user", content: [{ type: "text", text: t.text }, ...images], prompt_index: promptIndex })
184
+ }
185
+ promptIndex++
186
+ // The read-back reads the model's history, which starts at the last compaction.
187
+ if (toModel) expect.users++
188
+ break
189
+ }
190
+ case "assistant": {
191
+ if (t.model) model = t.model
192
+ if (!t.text.trim()) break
193
+ event({ sessionUpdate: "agent_message_chunk", content: { type: "text", text: t.text } }, t.at)
194
+ if (!toModel) break
195
+ // Text after a call is a new response: it was written after the call's result came back.
196
+ if (cur?.calls.length) flush()
197
+ cur ??= { text: "", model, calls: [] }
198
+ cur.text = cur.text ? `${cur.text}\n\n${t.text}` : t.text
199
+ break
200
+ }
201
+ case "tool_call": {
202
+ if (seen.has(t.id)) {
203
+ drop("repeated tool call ids")
204
+ break
205
+ }
206
+ seen.add(t.id)
207
+ screenCall(t)
208
+ if (toModel) {
209
+ cur ??= { text: "", model, calls: [] }
210
+ cur.calls.push(t)
211
+ expect.calls++
212
+ }
213
+ break
214
+ }
215
+ case "tool_result": {
216
+ if (!callIds.has(t.callId)) {
217
+ drop("tool results without a call")
218
+ break
219
+ }
220
+ if (answered.has(t.callId)) break
221
+ answered.add(t.callId)
222
+ screenResult(t.callId, t.ok, t.output, t.at)
223
+ // A result closes the response it answers, so a call made after it starts a new one.
224
+ if (toModel && cur?.calls.some((c) => c.id === t.callId)) flush()
225
+ break
226
+ }
227
+ case "reasoning":
228
+ event({ sessionUpdate: "agent_thought_chunk", content: { type: "text", text: t.text } }, t.at)
229
+ drop("reasoning (shown on screen, not given to the model)")
230
+ break
231
+ case "compaction":
232
+ if (i === lastCompaction)
233
+ chat.push({ type: "user", content: [{ type: "text", text: reminder(`${SUMMARY_PREFIX} in ${label}, which replaced the turns before it]\n\n${t.summary}`) }], [SYNTHETIC]: "system_reminder" })
234
+ break
235
+ case "file_edit":
236
+ if (!t.callId || !callIds.has(t.callId)) drop("file edits outside a tool call")
237
+ break
238
+ case "subagent":
239
+ drop("subagent sessions")
240
+ break
241
+ }
242
+ }
243
+ flush()
244
+ endPrompt()
245
+ if (!promptIndex && !updates.length) throw new Error("nothing Grok can show: the session has no messages or tool calls")
246
+
247
+ const title = session.header.title ?? firstUserText(session)?.replace(/\s+/g, " ").trim().slice(0, 60) ?? "Session"
248
+ const started = turns.map((t) => (t.at ? Date.parse(t.at) : Number.NaN)).find(Number.isFinite) ?? opts.now.getTime()
249
+ const git = opts.git ?? {}
250
+ const summary: Row = {
251
+ info: { id, cwd: opts.cwd },
252
+ session_summary: `${title} (from ${label})`,
253
+ generated_title: `${title} (from ${label})`,
254
+ created_at: micros(started),
255
+ updated_at: micros(opts.now.getTime()),
256
+ last_active_at: micros(opts.now.getTime()),
257
+ num_messages: updates.length,
258
+ num_chat_messages: chat.length,
259
+ current_model_id: opts.model,
260
+ next_trace_turn: 0,
261
+ chat_format_version: 1,
262
+ grok_home: grokHome(opts.home),
263
+ sandbox_profile: "off",
264
+ ...(git.gitRemote ? { git_remotes: [git.gitRemote] } : {}),
265
+ ...(git.gitBranch ? { head_branch: git.gitBranch } : {}),
266
+ ...(git.gitCommit ? { head_commit: git.gitCommit } : {}),
267
+ }
268
+ const lost = [...drops].map(([what, n]) => `${what} (${n})`)
269
+ if (lastCompaction >= 0) lost.push("turns before the last compaction are on screen only; the model starts from its summary")
270
+ return { summary, updates, chat, lost, expect }
271
+ }
272
+
273
+ const jsonl = (rows: Row[]) => `${rows.map((r) => JSON.stringify(r)).join("\n")}\n`
274
+
275
+ /** The Grok writer; `version` is for tests, which must not depend on the installed binary. */
276
+ export function grokWriter(opts: { version?: string } = {}): Writer {
277
+ const installed = (home: string) => opts.version ?? grokVersion(home)
278
+ return {
279
+ tool: TOOL,
280
+ label: LABEL,
281
+ way: "native",
282
+
283
+ async ready(w: WriteOptions): Promise<Readiness> {
284
+ const v = installed(w.home)
285
+ if (!v) return { ok: false, why: "the Grok CLI is not installed (`grok --version` did not run)" }
286
+ if (!TESTED_VERSIONS.includes(v)) return { ok: false, why: `Grok ${v} is not a version this writer was checked against (${TESTED_VERSIONS.join(", ")})` }
287
+ if (!isDir(w.cwd)) return { ok: false, why: `${w.cwd} is not a folder; Grok lists sessions by the folder they belong to` }
288
+ if (Buffer.byteLength(encodeCwd(resolve(w.cwd))) > MAX_FOLDER_NAME) return { ok: false, why: "the folder path is too long: Grok names such folders with a hash this writer does not reproduce" }
289
+ return { ok: true }
290
+ },
291
+
292
+ async write(session: Session, w: WriteOptions): Promise<WriteResult> {
293
+ const cwd = resolve(w.cwd)
294
+ let id = Bun.randomUUIDv7()
295
+ const group = join(sessionsDir(w.home), encodeCwd(cwd))
296
+ while (exists(join(group, id))) id = Bun.randomUUIDv7()
297
+ const dir = join(group, id)
298
+ const plan = planGrokSession(session, { id, now: new Date(), cwd, home: w.home, model: defaultModel(w.home), git: gitInfo(cwd, w.home) })
299
+ const files = { "summary.json": `${JSON.stringify(plan.summary, null, 2)}\n`, "updates.jsonl": jsonl(plan.updates), "chat_history.jsonl": jsonl(plan.chat) }
300
+ if (!w.dryRun) {
301
+ mkdirSync(group, { recursive: true })
302
+ // A new folder only: an existing session is never touched.
303
+ mkdirSync(dir)
304
+ try {
305
+ for (const [name, text] of Object.entries(files)) writeFileSync(join(dir, name), text, { flag: "wx" })
306
+ const back = await grokReader.read({ tool: TOOL, sourceId: id, path: dir }, w.home)
307
+ const n = countTurns(back)
308
+ const pairs = unpaired(back)
309
+ if (n.user !== plan.expect.users || n.tool_call !== plan.expect.calls || pairs.calls.length || pairs.results.length)
310
+ throw new Error(`the new Grok session did not read back intact (users ${n.user}/${plan.expect.users}, tool calls ${n.tool_call}/${plan.expect.calls}, unpaired ${pairs.calls.length + pairs.results.length})`)
311
+ } catch (e) {
312
+ rmSync(dir, { recursive: true, force: true })
313
+ throw new Error(`${(e as Error).message}; nothing was kept`)
314
+ }
315
+ }
316
+ return {
317
+ tool: TOOL,
318
+ way: "native",
319
+ sessionId: id,
320
+ written: Object.keys(files).map((f) => join(dir, f)),
321
+ open: `cd ${shellQuote(cwd)} && grok --resume ${id}`,
322
+ lost: plan.lost,
323
+ dryRun: !!w.dryRun,
324
+ }
325
+ },
326
+ }
327
+ }
328
+
329
+ export const grokNativeWriter = grokWriter()
@@ -0,0 +1,8 @@
1
+ import type { Reader, Writer } from "../types.ts"
2
+ import { hermesReader } from "./reader.ts"
3
+ import { hermesNativeWriter } from "./writer.ts"
4
+
5
+ // Hermes Agent (Nous Research): reads the sessions in ~/.hermes/state.db, and continues any session as a new
6
+ // native session in that store (backed up first; refused while Hermes holds it locked).
7
+ export const readers: Reader[] = [hermesReader]
8
+ export const writers: Writer[] = [hermesNativeWriter]
@@ -0,0 +1,249 @@
1
+ import type { Database } from "bun:sqlite"
2
+ import { exists, gitInfo } from "../../fsutil.ts"
3
+ import { keepImage, parseDataUrl } from "../../session/attachments.ts"
4
+ import type { Attachment, Session, SessionHeader, Turn } from "../../session/types.ts"
5
+ import { clip, header, iso, lossCounter, noteModel } from "../../session/util.ts"
6
+ import { withSnapshot } from "../../sqlite.ts"
7
+ import { CONTINUED_NOTE } from "../opencode/note.ts"
8
+ import type { Reader, SessionRef } from "../types.ts"
9
+ import { LABEL, TOOL, columnsOf, decodeContent, hermesToolKind, imageUrlOf, partsText, storePath, summaryOf, toolOutput, type Part } from "./records.ts"
10
+
11
+ // Reads Hermes Agent sessions from ~/.hermes/state.db (records.ts), always from a copy with its write-ahead log,
12
+ // as Hermes keeps the store open in WAL mode. Columns are looked up per store, so the schema-19 store of 0.18.2
13
+ // and the schema-26 store of 0.20.6 read alike. Only Hermes's active replay context becomes turns, as
14
+ // session-migrate projects it: rewound rows and rows a compaction replaced are counted in `lost`, never replayed.
15
+ //
16
+ // The list shows what Hermes's own session picker shows: no delegate (subagent) runs, no archived or hidden
17
+ // sessions, and for a session that was compressed into a continuation only the continuation, which starts with
18
+ // the summary and names the session it continues as its parent.
19
+
20
+ type Row = Record<string, any>
21
+
22
+ const json = (v: unknown): any => {
23
+ if (typeof v !== "string" || !v) return undefined
24
+ try {
25
+ return JSON.parse(v)
26
+ } catch {
27
+ return undefined
28
+ }
29
+ }
30
+ const text = (v: unknown) => (typeof v === "string" && v ? v : undefined)
31
+ const on = (v: unknown) => v === 1 || v === "1" || v === true
32
+
33
+ /** The newest moment a session row or its messages record. */
34
+ const latest = (...vs: unknown[]) => {
35
+ const ts = vs.map((v) => (typeof v === "number" && Number.isFinite(v) ? v : 0))
36
+ return iso(Math.max(...ts))
37
+ }
38
+
39
+ export function listHermes(d: Database, path: string): SessionRef[] {
40
+ const cols = columnsOf(d, "sessions")
41
+ const pick = (c: string) => (cols.has(c) ? `s.${c}` : "NULL")
42
+ const rows = d
43
+ .query(
44
+ `SELECT s.id, s.title, s.cwd, s.started_at, s.ended_at, s.end_reason, s.source, s.model_config,
45
+ ${pick("archived")} AS archived, ${pick("hidden")} AS hidden, ${pick("last_activity_at")} AS last_activity_at,
46
+ (SELECT MAX(m.timestamp) FROM messages m WHERE m.session_id = s.id) AS last,
47
+ EXISTS (SELECT 1 FROM messages m WHERE m.session_id = s.id AND COALESCE(m.active, 1) = 1) AS live,
48
+ EXISTS (SELECT 1 FROM sessions c WHERE c.parent_session_id = s.id) AS has_child
49
+ FROM sessions s`,
50
+ )
51
+ .all() as Row[]
52
+ const refs: SessionRef[] = []
53
+ for (const r of rows) {
54
+ if (!on(r.live) || on(r.archived) || on(r.hidden) || r.source === "tool") continue
55
+ if (json(r.model_config)?._delegate_from) continue
56
+ // Compressed into a continuation: the continuation is the live conversation (resolve_resume_session_id).
57
+ if (r.end_reason === "compression" && on(r.has_child)) continue
58
+ const updatedAt = latest(r.last, r.ended_at, r.last_activity_at, r.started_at)
59
+ refs.push({
60
+ tool: TOOL,
61
+ sourceId: String(r.id),
62
+ path,
63
+ ...(text(r.title) ? { title: String(r.title) } : {}),
64
+ ...(text(r.cwd) ? { cwd: String(r.cwd) } : {}),
65
+ ...(updatedAt ? { updatedAt } : {}),
66
+ })
67
+ }
68
+ return refs
69
+ }
70
+
71
+ /** What kind of child a session is of its parent: a /branch, a delegate run, or a compression continuation. */
72
+ function parentOf(d: Database, s: Row): SessionHeader["parent"] {
73
+ if (!s.parent_session_id) return undefined
74
+ const id = `${TOOL}:${s.parent_session_id}`
75
+ const config = json(s.model_config) ?? {}
76
+ if (config._delegate_from) return { id, kind: "subagent" }
77
+ if (config._branched_from) return { id, kind: "fork" }
78
+ const p = d.query("SELECT end_reason FROM sessions WHERE id = ?").get(s.parent_session_id) as { end_reason?: unknown } | null
79
+ return { id, kind: p?.end_reason === "branched" ? "fork" : "continued" }
80
+ }
81
+
82
+ /** A tool call's arguments: JSON text as Hermes stores it, parsed when it is JSON. */
83
+ function argsOf(v: unknown): unknown {
84
+ if (v && typeof v === "object") return v
85
+ if (typeof v !== "string") return {}
86
+ try {
87
+ return JSON.parse(v)
88
+ } catch {
89
+ return { input: v }
90
+ }
91
+ }
92
+
93
+ /** Whether OpenRouter-style reasoning_details carry a signature or encrypted data that cannot cross tools. */
94
+ const signedDetails = (v: unknown) =>
95
+ Array.isArray(v) && v.some((x) => x && typeof x === "object" && (x.signature || x.data || x.encrypted_content || String(x.type ?? "").includes("encrypted")))
96
+
97
+ /** One session from an open Hermes store. */
98
+ export function readHermes(d: Database, id: string, where: { path: string; home: string }): Session {
99
+ const s = d.query("SELECT * FROM sessions WHERE id = ?").get(id) as Row | null
100
+ if (!s) throw new Error(`no Hermes session ${id} in ${where.path}`)
101
+ const cwd = text(s.cwd)
102
+ const branch = text(s.git_branch)
103
+ const h = header(TOOL, id, {
104
+ sourcePath: where.path,
105
+ ...(text(s.title) ? { title: String(s.title) } : {}),
106
+ project: { ...(cwd ? { cwd, ...gitInfo(cwd, where.home) } : {}), ...(branch ? { gitBranch: branch } : {}) },
107
+ ...(iso(s.started_at) ? { startedAt: iso(s.started_at)! } : {}),
108
+ })
109
+ noteModel(h, text(s.model))
110
+ // Models the session's API calls used (schema 2x keeps a usage row per model).
111
+ if (columnsOf(d, "session_model_usage").size)
112
+ for (const u of d.query("SELECT model FROM session_model_usage WHERE session_id = ? ORDER BY first_seen").all(id) as Row[]) noteModel(h, text(u.model))
113
+ const parent = parentOf(d, s)
114
+ if (parent) h.parent = parent
115
+
116
+ const lost = lossCounter()
117
+ const turns: Turn[] = []
118
+ const emitted = new Set<string>()
119
+ /** Calls still waiting for their result, by the id Hermes stored (duplicates queue up). */
120
+ const waiting = new Map<string, string[]>()
121
+ let last = 0
122
+ /** Whether a prompt was read yet: brain's note opens the first one. */
123
+ let prompted = false
124
+
125
+ for (const r of d.query("SELECT * FROM messages WHERE session_id = ? ORDER BY id").all(id) as Row[]) {
126
+ if (typeof r.timestamp === "number" && r.timestamp > last) last = r.timestamp
127
+ // Hermes heals a NULL active to 1 on startup; only 0 is out of the replay.
128
+ if (r.active !== null && r.active !== undefined && !on(r.active)) {
129
+ lost.add(on(r.compacted) ? "turns Hermes compacted away (still in its store)" : "rewound turns")
130
+ continue
131
+ }
132
+ const role = String(r.role ?? "")
133
+ if (role === "session_meta") continue
134
+ if (on(r.observed)) {
135
+ lost.add("group-chat messages Hermes only observed")
136
+ continue
137
+ }
138
+ const at = iso(r.timestamp)
139
+ const stamp = at ? { at } : {}
140
+ const content = decodeContent(r.content)
141
+ let body = partsText(content)
142
+
143
+ if (role === "user" || role === "assistant") {
144
+ const sum = summaryOf(body, on(r._compressed_summary))
145
+ if (sum) {
146
+ if (sum.summary) turns.push({ kind: "compaction", ...stamp, summary: sum.summary })
147
+ body = sum.prior ?? ""
148
+ }
149
+ }
150
+ const parts = typeof content === "string" ? [] : content
151
+ const others = (keep: (p: Part) => boolean) => parts.filter((p) => p.type !== "text" && !keep(p))
152
+
153
+ if (role === "user") {
154
+ if (!prompted) {
155
+ const note = body.match(CONTINUED_NOTE)
156
+ if (note) {
157
+ // The note brain puts before the first message it carries over, not something the person wrote.
158
+ h.parent ??= { id: note[1]!, kind: "continued" }
159
+ body = body.replace(/^\[This session continues [^\n]*\]/, "").trimStart()
160
+ }
161
+ }
162
+ const attachments: Attachment[] = []
163
+ for (const p of parts) {
164
+ const url = imageUrlOf(p)
165
+ if (url === undefined) continue
166
+ const data = parseDataUrl(url)
167
+ const kept = data ? keepImage(data.base64, data.mime) : undefined
168
+ if (kept) attachments.push(kept)
169
+ else lost.add("images")
170
+ }
171
+ for (const p of others((p) => imageUrlOf(p) !== undefined)) lost.add(`unknown content parts: ${String(p.type)}`)
172
+ if (body.trim() || attachments.length) {
173
+ turns.push({ kind: "user", ...stamp, text: body, ...(attachments.length ? { attachments } : {}) })
174
+ prompted = true
175
+ }
176
+ continue
177
+ }
178
+
179
+ if (role === "assistant") {
180
+ // Hermes keeps readable reasoning in `reasoning` (and copies it to reasoning_content for providers that
181
+ // want it echoed); reasoning_details and Codex reasoning items carry signed or encrypted reasoning.
182
+ const reasoning = [r.reasoning, r.reasoning_content].find((v) => typeof v === "string" && v.trim()) as string | undefined
183
+ const details = json(r.reasoning_details)
184
+ if (reasoning) turns.push({ kind: "reasoning", ...stamp, text: reasoning })
185
+ if (signedDetails(details)) lost.add(reasoning ? "reasoning signatures" : "signed reasoning")
186
+ if (json(r.codex_reasoning_items)) lost.add("encrypted reasoning")
187
+ for (const p of others(() => false)) lost.add(`unknown content parts: ${String(p.type)}`)
188
+ if (body.trim()) turns.push({ kind: "assistant", ...stamp, text: body })
189
+ const calls = r.tool_calls ? json(r.tool_calls) : []
190
+ if (!Array.isArray(calls)) {
191
+ lost.add("unreadable tool calls")
192
+ continue
193
+ }
194
+ for (const c of calls as Row[]) {
195
+ const fn = (c?.function ?? {}) as Row
196
+ const stored = text(c?.id) ?? text(c?.call_id) ?? ""
197
+ const name = text(fn.name) ?? "unknown"
198
+ let callId = stored
199
+ for (let n = 2; !callId || emitted.has(callId); n++) callId = `${stored || "call"}#${n}`
200
+ emitted.add(callId)
201
+ waiting.set(stored, [...(waiting.get(stored) ?? []), callId])
202
+ if (c?.extra_content) lost.add("provider signatures on tool calls")
203
+ turns.push({ kind: "tool_call", ...stamp, id: callId, name, toolKind: hermesToolKind(name), input: argsOf(fn.arguments) })
204
+ }
205
+ continue
206
+ }
207
+
208
+ if (role === "tool") {
209
+ const stored = text(r.tool_call_id) ?? ""
210
+ const queue = waiting.get(stored)
211
+ const callId = queue?.shift()
212
+ if (!callId) {
213
+ lost.add(emitted.has(stored) ? "repeated tool results" : "tool results without a call")
214
+ continue
215
+ }
216
+ const images = parts.filter((p) => imageUrlOf(p) !== undefined).length
217
+ if (images) lost.add("images in tool results", images)
218
+ const out = toolOutput(content)
219
+ const c = clip(out.text)
220
+ turns.push({ kind: "tool_result", ...stamp, callId, ok: out.ok, output: c.text, ...(c.truncated ? { truncated: true } : {}) })
221
+ continue
222
+ }
223
+
224
+ lost.add(`messages with role ${role || "(none)"}`)
225
+ }
226
+ lost.flush(h)
227
+ const updatedAt = latest(last, s.ended_at, s.last_activity_at, s.started_at)
228
+ if (updatedAt) h.updatedAt = updatedAt
229
+ return { header: h, turns }
230
+ }
231
+
232
+ export const hermesReader: Reader = {
233
+ tool: TOOL,
234
+ label: LABEL,
235
+ async list(home) {
236
+ const db = storePath(home)
237
+ if (!exists(db)) return []
238
+ try {
239
+ return withSnapshot(db, (d) => listHermes(d, db)).sort((a, b) => (b.updatedAt ?? "").localeCompare(a.updatedAt ?? ""))
240
+ } catch {
241
+ // A store that cannot be copied or opened lists nothing; read() says why.
242
+ return []
243
+ }
244
+ },
245
+ async read(ref, home) {
246
+ if (!exists(ref.path)) throw new Error(`Hermes session store not found: ${ref.path}`)
247
+ return withSnapshot(ref.path, (d) => readHermes(d, ref.sourceId, { path: ref.path, home }))
248
+ },
249
+ }