@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,424 @@
1
+ import type { Database } from "bun:sqlite"
2
+ import { statSync } from "node:fs"
3
+ import { basename, dirname, join } from "node:path"
4
+ import { fileURLToPath } from "node:url"
5
+ import { exists, gitInfo, ls, mtime } from "../../fsutil.ts"
6
+ import { withSnapshot } from "../../sqlite.ts"
7
+ import type { Session, SessionHeader, Turn } from "../../session/types.ts"
8
+ import { clip, header, lossCounter } from "../../session/util.ts"
9
+ import { CONTINUED_NOTE } from "../claude-code/records.ts"
10
+ import type { Reader, SessionRef } from "../types.ts"
11
+ import { allBytes, bytesOf, decode, numberOf, textOf, type Field } from "./proto.ts"
12
+ import {
13
+ CONVERSATION_FILE,
14
+ LABEL,
15
+ LINK_KEY,
16
+ MAX_DB_BYTES,
17
+ MAX_STEPS,
18
+ PAYLOAD_FIELD,
19
+ STATUS,
20
+ STEP,
21
+ STORES,
22
+ TOOL,
23
+ antigravityToolKind,
24
+ conversationsDir,
25
+ decodeCall,
26
+ entriesOf,
27
+ hasThinking,
28
+ sqlTime,
29
+ summariesPath,
30
+ timestampOf,
31
+ workspaceOf,
32
+ type NativeCall,
33
+ } from "./records.ts"
34
+
35
+ // Reads Antigravity conversations (records.ts) from the CLI's store and the IDE's two. Each conversation
36
+ // database is read from a copy with its write-ahead log, so a live one reads consistently; the summaries
37
+ // database beside it gives the title, folder and time, reading only the columns it needs (the IDE's table has
38
+ // more). The projection follows session-migrate's: user inputs are user turns, planner responses are replies
39
+ // and tool calls, generic tool steps and the run_command, view_file, shell_exec and MCP steps are results.
40
+ // Private thinking never becomes a turn: it is only counted. Every other step type is counted by number.
41
+
42
+ interface Summary {
43
+ title?: string
44
+ cwd?: string
45
+ updatedAt?: string
46
+ parent?: string
47
+ }
48
+
49
+ /** A local folder from a file:// URI; undefined for anything else. */
50
+ function pathOfUri(uri: unknown): string | undefined {
51
+ if (typeof uri !== "string" || !uri.startsWith("file://")) return undefined
52
+ try {
53
+ const u = new URL(uri)
54
+ return u.host ? undefined : fileURLToPath(u)
55
+ } catch {
56
+ return undefined
57
+ }
58
+ }
59
+
60
+ const WANTED = ["conversation_id", "title", "workspace_uris", "last_modified_time", "parent_conversation_id"]
61
+
62
+ /** Summary rows by conversation id, from whichever of the wanted columns the table has. */
63
+ function summaryRows(d: Database, id?: string): Map<string, Summary> {
64
+ const out = new Map<string, Summary>()
65
+ const cols = new Set((d.query("PRAGMA table_info(conversation_summaries)").all() as Array<{ name: string }>).map((c) => c.name))
66
+ if (!cols.has("conversation_id")) return out
67
+ const sql = `SELECT ${WANTED.filter((c) => cols.has(c))
68
+ .map((c) => `"${c}"`)
69
+ .join(", ")} FROM conversation_summaries${id ? " WHERE conversation_id = ?" : ""}`
70
+ const rows = (id ? d.query(sql).all(id) : d.query(sql).all()) as Array<Record<string, unknown>>
71
+ for (const r of rows) {
72
+ let cwd: string | undefined
73
+ try {
74
+ const uris = JSON.parse(String(r.workspace_uris ?? ""))
75
+ if (Array.isArray(uris)) cwd = pathOfUri(uris[0])
76
+ } catch {
77
+ // no folder
78
+ }
79
+ out.set(String(r.conversation_id), {
80
+ ...(typeof r.title === "string" && r.title.trim() ? { title: r.title } : {}),
81
+ ...(cwd ? { cwd } : {}),
82
+ ...(sqlTime(r.last_modified_time) ? { updatedAt: sqlTime(r.last_modified_time)! } : {}),
83
+ ...(typeof r.parent_conversation_id === "string" && r.parent_conversation_id ? { parent: r.parent_conversation_id } : {}),
84
+ })
85
+ }
86
+ return out
87
+ }
88
+
89
+ /** A store's summaries, read from a copy; none when the database is missing or unreadable. */
90
+ function readSummaries(path: string, id?: string): Map<string, Summary> {
91
+ if (!exists(path)) return new Map()
92
+ try {
93
+ return withSnapshot(path, (d) => summaryRows(d, id))
94
+ } catch {
95
+ return new Map()
96
+ }
97
+ }
98
+
99
+ export const antigravityReader: Reader = {
100
+ tool: TOOL,
101
+ label: LABEL,
102
+ async list(home) {
103
+ const found = new Map<string, SessionRef>()
104
+ for (const store of STORES) {
105
+ const dir = conversationsDir(home, store)
106
+ // Only the SQLite conversations: the IDE's <uuid>.pb files are encrypted.
107
+ const files = ls(dir).filter((n) => CONVERSATION_FILE.test(n))
108
+ if (!files.length) continue
109
+ const sums = readSummaries(summariesPath(home, store))
110
+ for (const name of files) {
111
+ const id = name.slice(0, -3)
112
+ const path = join(dir, name)
113
+ const s = sums.get(id)
114
+ const updatedAt = s?.updatedAt ?? mtime(path)
115
+ const ref: SessionRef = {
116
+ tool: TOOL,
117
+ sourceId: id,
118
+ path,
119
+ ...(s?.title ? { title: s.title } : {}),
120
+ ...(s?.cwd ? { cwd: s.cwd } : {}),
121
+ ...(updatedAt ? { updatedAt } : {}),
122
+ extra: { store },
123
+ }
124
+ // The same conversation in two stores: the newer copy.
125
+ const prev = found.get(id)
126
+ if (!prev || (ref.updatedAt ?? "") > (prev.updatedAt ?? "")) found.set(id, ref)
127
+ }
128
+ }
129
+ return [...found.values()].sort((a, b) => (b.updatedAt ?? "").localeCompare(a.updatedAt ?? ""))
130
+ },
131
+ async read(ref) {
132
+ if (!exists(ref.path)) throw new Error(`Antigravity conversation not found: ${ref.path}`)
133
+ if (statSync(ref.path).size > MAX_DB_BYTES) throw new Error(`Antigravity conversation is larger than ${MAX_DB_BYTES} bytes: ${ref.path}`)
134
+ const id = basename(ref.path).replace(/\.db$/i, "")
135
+ const s = readSummaries(join(dirname(dirname(ref.path)), "conversation_summaries.db"), id).get(id)
136
+ const title = s?.title ?? ref.title
137
+ const cwd = s?.cwd ?? ref.cwd
138
+ const updatedAt = s?.updatedAt ?? ref.updatedAt
139
+ return withSnapshot(ref.path, (d) =>
140
+ readConversation(d, id, { path: ref.path, ...(title ? { title } : {}), ...(cwd ? { cwd } : {}), ...(updatedAt ? { updatedAt } : {}), ...(s?.parent ? { parent: s.parent } : {}) }),
141
+ )
142
+ },
143
+ }
144
+
145
+ export interface Where {
146
+ path: string
147
+ title?: string
148
+ cwd?: string
149
+ updatedAt?: string
150
+ /** The conversation this one was spawned from, per the summaries table. */
151
+ parent?: string
152
+ }
153
+
154
+ const stamp = (at: string | undefined) => (at ? { at } : {})
155
+
156
+ /** A nested message, or undefined when it is absent or not a message. */
157
+ function tryMessage(fields: Field[] | undefined, no: number): Field[] | undefined {
158
+ const b = fields && bytesOf(fields, no)
159
+ if (!b) return undefined
160
+ try {
161
+ return decode(b)
162
+ } catch {
163
+ return undefined
164
+ }
165
+ }
166
+
167
+ function tryCall(b: Uint8Array | undefined): NativeCall | undefined {
168
+ if (!b) return undefined
169
+ try {
170
+ return decodeCall(b)
171
+ } catch {
172
+ return undefined
173
+ }
174
+ }
175
+
176
+ /** Step types read as tool results by `builtin`. */
177
+ const BUILTIN: Record<number, true> = { [STEP.runCommand]: true, [STEP.viewFile]: true, [STEP.shellExec]: true }
178
+
179
+ /** A text from a built-in tool step: what it was given and what it returned. */
180
+ function builtin(type: number, body: Field[]): { name: string; input: Record<string, unknown>; output?: string } {
181
+ if (type === STEP.runCommand) {
182
+ const command = textOf(body, 23) || textOf(body, 25)
183
+ const out = tryMessage(body, 21) ?? tryMessage(body, 26)
184
+ const output = (out && textOf(out, 1)) || textOf(body, 4) || textOf(body, 5)
185
+ return { name: "run_command", input: command ? { command } : {}, ...(output !== undefined ? { output } : {}) }
186
+ }
187
+ if (type === STEP.viewFile) {
188
+ const path = textOf(body, 1)
189
+ const output = textOf(body, 4) || textOf(body, 9)
190
+ return { name: "view_file", input: path ? { AbsolutePath: path } : {}, ...(output !== undefined ? { output } : {}) }
191
+ }
192
+ const command = textOf(body, 1)
193
+ const output = textOf(body, 3) || textOf(body, 4)
194
+ return { name: "shell_exec", input: command ? { command } : {}, ...(output !== undefined ? { output } : {}) }
195
+ }
196
+
197
+ /** One conversation from an open database (a snapshot, or the writer's own before it is published). */
198
+ export function readConversation(d: Database, id: string, where: Where): Session {
199
+ const tables = new Set((d.query("SELECT name FROM sqlite_master WHERE type = 'table'").all() as Array<{ name: string }>).map((r) => r.name))
200
+ if (!tables.has("steps")) throw new Error(`not an Antigravity conversation database (no steps table): ${where.path}`)
201
+ const count = (d.query("SELECT count(*) AS n FROM steps").get() as { n: number }).n
202
+ if (count > MAX_STEPS) throw new Error(`Antigravity conversation has ${count} steps, more than ${MAX_STEPS}: ${where.path}`)
203
+
204
+ const lost = lossCounter()
205
+ const turns: Turn[] = []
206
+
207
+ // Subagent and battle trajectories are stored beside the conversation's own and are not read.
208
+ if (tables.has("trajectory_meta")) {
209
+ const n = (d.query("SELECT count(*) AS n FROM trajectory_meta").get() as { n: number }).n
210
+ if (n > 1) lost.add("auxiliary trajectories (subagents, battles)", n - 1)
211
+ }
212
+ if (tables.has("battle_mode_infos")) {
213
+ const n = (d.query("SELECT count(*) AS n FROM battle_mode_infos").get() as { n: number }).n
214
+ if (n) lost.add("battle mode records", n)
215
+ }
216
+ let startedAt: string | undefined
217
+ let metaCwd: string | undefined
218
+ if (tables.has("trajectory_metadata_blob")) {
219
+ const row = d.query("SELECT data FROM trajectory_metadata_blob WHERE id = 'main'").get() as { data: unknown } | null
220
+ if (row?.data instanceof Uint8Array)
221
+ try {
222
+ const m = decode(row.data)
223
+ startedAt = timestampOf(tryMessage(m, 2))
224
+ metaCwd = pathOfUri(workspaceOf(m))
225
+ } catch {
226
+ // Metadata that does not decode only costs the start time and folder.
227
+ }
228
+ }
229
+
230
+ // Calls waiting for their result: the id the store gave them, and the id they have here.
231
+ const pending: Array<{ native: string; id: string }> = []
232
+ const used = new Set<string>()
233
+ let continued: string | undefined
234
+ let firstAt: string | undefined
235
+ let lastAt: string | undefined
236
+
237
+ const parseArgs = (args: string): unknown => {
238
+ if (!args.trim()) return {}
239
+ try {
240
+ return JSON.parse(args)
241
+ } catch {
242
+ lost.add("unreadable tool arguments")
243
+ return { arguments: args }
244
+ }
245
+ }
246
+ const call = (at: string | undefined, native: string, name: string, input: unknown, kind = antigravityToolKind(name)) => {
247
+ let callId = native
248
+ for (let k = 2; !callId || used.has(callId); k++) callId = `${native || "call"}#${k}`
249
+ used.add(callId)
250
+ turns.push({ kind: "tool_call", ...stamp(at), id: callId, name, toolKind: kind, input })
251
+ return callId
252
+ }
253
+ /** The pending call a result answers: the linked one when the step names it, else the oldest. */
254
+ const take = (link: string | undefined) => {
255
+ if (link === undefined) return pending.shift()
256
+ const i = pending.findIndex((p) => p.native === link)
257
+ return i >= 0 ? pending.splice(i, 1)[0] : undefined
258
+ }
259
+ const result = (at: string | undefined, link: Partial<NativeCall> | undefined, fallback: { name: string; input: unknown }, ok: boolean, output: string) => {
260
+ let callId = take(link?.id)?.id
261
+ if (!callId) {
262
+ // A result whose call was not recorded (or not readable): the call is rebuilt from what the step says.
263
+ callId = call(at, link?.id ?? "", link?.name ?? fallback.name, link?.args !== undefined ? parseArgs(link.args) : fallback.input)
264
+ lost.add("tool calls rebuilt from their results")
265
+ }
266
+ const c = clip(output)
267
+ turns.push({ kind: "tool_result", ...stamp(at), callId, ok, output: c.text, ...(c.truncated ? { truncated: true } : {}) })
268
+ }
269
+
270
+ const rows = d.query("SELECT idx, step_type, status, has_subtrajectory, metadata, step_payload, step_format FROM steps ORDER BY idx").all() as Array<Record<string, unknown>>
271
+ for (const r of rows) {
272
+ const type = Number(r.step_type)
273
+ const status = Number(r.status)
274
+ if (Number(r.step_format ?? 0) !== 0) {
275
+ lost.add("Antigravity steps in an unknown encoding")
276
+ continue
277
+ }
278
+ if (!(r.step_payload instanceof Uint8Array)) {
279
+ lost.add("unreadable Antigravity steps")
280
+ continue
281
+ }
282
+ let outer: Field[]
283
+ let body: Field[] | undefined
284
+ try {
285
+ outer = decode(r.step_payload)
286
+ // The row and the serialised step say the same type and status, or the step is not trusted.
287
+ const t = numberOf(outer, 1)
288
+ const s = numberOf(outer, 4)
289
+ if ((t !== undefined && t !== type) || (s !== undefined && s !== status)) throw new Error("step row and payload disagree")
290
+ const field = PAYLOAD_FIELD[type]
291
+ const b = field === undefined ? undefined : bytesOf(outer, field)
292
+ body = b ? decode(b) : undefined
293
+ } catch {
294
+ lost.add("unreadable Antigravity steps")
295
+ continue
296
+ }
297
+ if (Number(r.has_subtrajectory) > 0) lost.add("subtrajectories under steps")
298
+ let meta: Field[] | undefined
299
+ try {
300
+ meta = r.metadata instanceof Uint8Array ? decode(r.metadata) : tryMessage(outer, 5)
301
+ } catch {
302
+ meta = undefined
303
+ }
304
+ const at = timestampOf(tryMessage(meta, 1))
305
+ if (at) {
306
+ firstAt ??= at
307
+ lastAt = at
308
+ }
309
+ // A result step's metadata names the call it answers.
310
+ const linked = tryCall(meta && bytesOf(meta, 4))
311
+
312
+ if (!body && (type === STEP.userInput || type === STEP.plannerResponse || type === STEP.generic || type in BUILTIN || type === STEP.mcpTool)) {
313
+ lost.add("unreadable Antigravity steps")
314
+ continue
315
+ }
316
+ switch (type) {
317
+ case STEP.userInput: {
318
+ let text = textOf(body!, 2) || textOf(body!, 1) || ""
319
+ // The note brain puts before a continued session's first request says where it came from.
320
+ const note = CONTINUED_NOTE.exec(text)
321
+ if (note) {
322
+ continued ??= note[1]
323
+ text = text.replace(/^\[[^\n]*\]\n*/, "")
324
+ }
325
+ const context = allBytes(body!, 3).length + allBytes(body!, 9).length
326
+ if (context) lost.add("context attached to user messages", context)
327
+ if (text.trim()) turns.push({ kind: "user", ...stamp(at), text })
328
+ break
329
+ }
330
+ case STEP.plannerResponse: {
331
+ const thinking = hasThinking(body!)
332
+ if (thinking) lost.add("private reasoning")
333
+ const text = textOf(body!, 1) ?? ""
334
+ if (text.trim()) turns.push({ kind: "assistant", ...stamp(at), text })
335
+ const calls = allBytes(body!, 7)
336
+ for (const b of calls) {
337
+ const c = tryCall(b)
338
+ if (!c) {
339
+ lost.add("unreadable tool calls")
340
+ continue
341
+ }
342
+ pending.push({ native: c.id, id: call(at, c.id, c.name, parseArgs(c.args)) })
343
+ }
344
+ if (!text.trim() && !calls.length && !thinking) lost.add("Antigravity replies with nothing readable")
345
+ break
346
+ }
347
+ case STEP.generic: {
348
+ const res = tryMessage(body, 2)
349
+ if (!res) {
350
+ lost.add("unreadable Antigravity steps")
351
+ break
352
+ }
353
+ const key = entriesOf(res, 2).find(([k]) => k === LINK_KEY)?.[1]
354
+ const args = Object.fromEntries(
355
+ entriesOf(body!, 1).map(([k, v]) => {
356
+ try {
357
+ return [k, JSON.parse(v)]
358
+ } catch {
359
+ return [k, v]
360
+ }
361
+ }),
362
+ )
363
+ result(at, key ? { id: key } : linked, { name: "generic", input: args }, status !== STATUS.error, textOf(res, 1) ?? "")
364
+ break
365
+ }
366
+ case STEP.mcpTool: {
367
+ const c = tryCall(bytesOf(body!, 2))
368
+ let callId: string | undefined
369
+ if (c) {
370
+ const i = pending.findIndex((p) => p.native === c.id)
371
+ callId = i >= 0 ? pending.splice(i, 1)[0]!.id : call(at, c.id, c.name, parseArgs(c.args), "mcp")
372
+ }
373
+ const output = textOf(body!, 3) ?? textOf(body!, 8)
374
+ if (output === undefined) {
375
+ lost.add("MCP steps without a readable result")
376
+ break
377
+ }
378
+ callId ??= call(at, "", "mcp_tool", {}, "mcp")
379
+ const o = clip(output)
380
+ turns.push({ kind: "tool_result", ...stamp(at), callId, ok: status !== STATUS.error, output: o.text, ...(o.truncated ? { truncated: true } : {}) })
381
+ break
382
+ }
383
+ case STEP.runCommand:
384
+ case STEP.viewFile:
385
+ case STEP.shellExec: {
386
+ const b = builtin(type, body!)
387
+ result(at, linked, b, status !== STATUS.error, b.output ?? "")
388
+ break
389
+ }
390
+ default: {
391
+ lost.add(`antigravity step type ${type}`)
392
+ // A step brain cannot read that answers a pending call still closes it, so the call is not left open.
393
+ const i = linked ? pending.findIndex((p) => p.native === linked.id) : -1
394
+ if (i >= 0)
395
+ turns.push({
396
+ kind: "tool_result",
397
+ ...stamp(at),
398
+ callId: pending.splice(i, 1)[0]!.id,
399
+ ok: status !== STATUS.error,
400
+ output: `[Antigravity kept this result in a step type brain cannot read (${type}).]`,
401
+ })
402
+ }
403
+ }
404
+ }
405
+
406
+ const cwd = where.cwd ?? metaCwd
407
+ const started = startedAt ?? firstAt
408
+ const updated = where.updatedAt ?? lastAt
409
+ const parent: SessionHeader["parent"] = continued
410
+ ? { id: continued, kind: "continued" }
411
+ : where.parent
412
+ ? { id: `${TOOL}:${where.parent}`, kind: "subagent" }
413
+ : undefined
414
+ const h = header(TOOL, id, {
415
+ sourcePath: where.path,
416
+ ...(where.title ? { title: where.title } : {}),
417
+ project: { ...(cwd ? { cwd } : {}), ...gitInfo(cwd) },
418
+ ...(started ? { startedAt: started } : {}),
419
+ ...(updated ? { updatedAt: updated } : {}),
420
+ ...(parent ? { parent } : {}),
421
+ })
422
+ lost.flush(h)
423
+ return { header: h, turns }
424
+ }
@@ -0,0 +1,236 @@
1
+ import { join } from "node:path"
2
+ import type { ToolKind } from "../../session/types.ts"
3
+ import { toolKindOf } from "../../session/util.ts"
4
+ import { allBytes, asText, concat, decode, messageField, numberOf, textField, textOf, varintField, type Field } from "./proto.ts"
5
+
6
+ // Antigravity (Google): where its conversations are and what their rows look like. Ported from the clean-room
7
+ // format notes and reference adapter of session-migrate (MIT, xhluca: docs/antigravity-format.md,
8
+ // src/session_migrate/formats/antigravity.py), which pin Antigravity CLI 1.1.16, and checked against the real
9
+ // 1.1.16 capture in test/fixtures/native-corpus/antigravity.
10
+ //
11
+ // Every conversation is its own SQLite database, ~/.gemini/<store>/conversations/<uuid>.db, with seven tables:
12
+ // trajectory_meta (the conversation's trajectory: type 4 cascade, source 17 for the CLI), steps (one row per
13
+ // step, idx 0..n-1, each with its type and status both as columns and inside the serialised Step protobuf),
14
+ // trajectory_metadata_blob (one "main" row), and four runtime tables. Beside the folder,
15
+ // conversation_summaries.db is the picker's index: title, preview, workspace, times. The CLI keeps its store in
16
+ // antigravity-cli; the IDE uses antigravity and antigravity-ide, the same layout, a wider summaries table, and
17
+ // encrypted <uuid>.pb files that nothing here reads.
18
+ //
19
+ // A Step carries its type in field 1, its status in field 4 (3 done, 7 error), its metadata (times, the tool
20
+ // call a result answers) in field 5, and its body in a field that depends on the type (PAYLOAD_FIELD). A user
21
+ // input keeps the person's text in field 2; a planner response keeps the reply in field 1, a message id in
22
+ // field 6 and its tool calls (id 1, name 2, JSON arguments 3) in repeated field 7, and private thinking in
23
+ // fields 3, 4, 14 and 16; a generic tool step keeps the arguments as {key, JSON value} entries in repeated field
24
+ // 1 and the result in field 2 (text in its field 1, {key, value} metadata in its repeated field 2).
25
+
26
+ export const TOOL = "antigravity"
27
+ export const LABEL = "Antigravity"
28
+ /** Antigravity CLI versions whose store the writer was checked against (by session-migrate's live checks). */
29
+ export const TESTED_VERSIONS = ["1.1.16"]
30
+
31
+ /** The CLI's store, then the IDE's two. */
32
+ export const STORES = ["antigravity-cli", "antigravity", "antigravity-ide"] as const
33
+ export type Store = (typeof STORES)[number]
34
+ export const CLI_STORE: Store = "antigravity-cli"
35
+
36
+ export const storeDir = (home: string, store: Store) => join(home, ".gemini", store)
37
+ export const conversationsDir = (home: string, store: Store) => join(storeDir(home, store), "conversations")
38
+ export const summariesPath = (home: string, store: Store) => join(storeDir(home, store), "conversation_summaries.db")
39
+
40
+ /** A conversation database's file name: its UUID and .db. */
41
+ export const CONVERSATION_FILE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.db$/i
42
+
43
+ export const STEP = {
44
+ viewFile: 8,
45
+ listDirectory: 9,
46
+ userInput: 14,
47
+ plannerResponse: 15,
48
+ runCommand: 21,
49
+ checkpoint: 23,
50
+ mcpTool: 38,
51
+ fileChange: 86,
52
+ conversationHistory: 98,
53
+ shellExec: 112,
54
+ writeBlob: 128,
55
+ generic: 132,
56
+ } as const
57
+
58
+ /** The Step field holding each type's body, as observed in 1.1.16. */
59
+ export const PAYLOAD_FIELD: Record<number, number> = { 8: 14, 9: 15, 14: 19, 15: 20, 21: 28, 23: 30, 38: 47, 86: 98, 98: 111, 112: 127, 128: 142, 132: 140 }
60
+
61
+ export const STATUS = { done: 3, error: 7 } as const
62
+ export const TRAJECTORY = { cascade: 4, cli: 17 } as const
63
+ /** The project the CLI files conversations under when it has no other. */
64
+ export const PROJECT_ID = "default-cli-project"
65
+ /** The generic result's metadata key linking it to its call (session-migrate's, kept for interoperability). */
66
+ export const LINK_KEY = "session_migrate_call_id"
67
+
68
+ /** Bounds on what a reader accepts from a store. */
69
+ export const MAX_DB_BYTES = 256 * 1024 * 1024
70
+ export const MAX_STEPS = 100_000
71
+
72
+ /** The conversation database as the 1.1.16 CLI creates it (from the capture's sqlite_master). */
73
+ export const SCHEMA = `
74
+ CREATE TABLE \`trajectory_meta\` (\`trajectory_id\` text,\`cascade_id\` text,\`trajectory_type\` integer,\`source\` integer,PRIMARY KEY (\`trajectory_id\`));
75
+ CREATE TABLE \`steps\` (\`idx\` integer,\`step_type\` integer NOT NULL DEFAULT 0,\`status\` integer NOT NULL DEFAULT 0,\`has_subtrajectory\` numeric NOT NULL DEFAULT false,\`metadata\` blob,\`error_details\` blob,\`permissions\` blob,\`task_details\` blob,\`render_info\` blob,\`step_payload\` blob,\`step_format\` integer NOT NULL DEFAULT 0,PRIMARY KEY (\`idx\`));
76
+ CREATE TABLE \`gen_metadata\` (\`idx\` integer,\`data\` blob,\`size\` integer NOT NULL DEFAULT 0,PRIMARY KEY (\`idx\`));
77
+ CREATE TABLE \`executor_metadata\` (\`idx\` integer,\`data\` blob,PRIMARY KEY (\`idx\`));
78
+ CREATE TABLE \`parent_references\` (\`idx\` integer,\`data\` blob,PRIMARY KEY (\`idx\`));
79
+ CREATE TABLE \`trajectory_metadata_blob\` (\`id\` text DEFAULT "main",\`data\` blob,PRIMARY KEY (\`id\`));
80
+ CREATE TABLE \`battle_mode_infos\` (\`idx\` integer,\`data\` blob,PRIMARY KEY (\`idx\`));
81
+ CREATE INDEX \`idx_steps_status\` ON \`steps\`(\`status\`);
82
+ CREATE INDEX \`idx_steps_step_type\` ON \`steps\`(\`step_type\`);
83
+ `
84
+
85
+ /** The picker index as 1.1.16 creates it. */
86
+ export const SUMMARY_SCHEMA = `
87
+ CREATE TABLE \`conversation_summaries\` (\`conversation_id\` text,\`title\` text NOT NULL DEFAULT "",\`preview\` text NOT NULL DEFAULT "",\`step_count\` integer NOT NULL DEFAULT 0,\`last_modified_time\` datetime NOT NULL,\`workspace_uris\` text NOT NULL,\`status\` text NOT NULL DEFAULT "",\`source\` text NOT NULL DEFAULT "",\`project_id\` text NOT NULL DEFAULT "",\`agent_name\` text NOT NULL DEFAULT "",\`parent_conversation_id\` text NOT NULL DEFAULT "",\`nesting_depth\` integer NOT NULL DEFAULT 0,\`battle_id\` text NOT NULL DEFAULT "",\`winning_conversation_id\` text NOT NULL DEFAULT "",\`not_fully_idle\` numeric NOT NULL DEFAULT false,\`killed\` numeric NOT NULL DEFAULT false,\`last_user_input_time\` datetime NOT NULL,\`last_user_input_step_index\` integer NOT NULL DEFAULT -1,\`app_data_dir\` text NOT NULL DEFAULT "",PRIMARY KEY (\`conversation_id\`));
88
+ CREATE INDEX \`idx_conversation_summaries_last_user_input_time\` ON \`conversation_summaries\`(\`last_user_input_time\`);
89
+ CREATE INDEX \`idx_conversation_summaries_last_modified_time\` ON \`conversation_summaries\`(\`last_modified_time\`);
90
+ `
91
+
92
+ /** The summary columns 1.1.16 has, all of which the writer fills. Newer builds and the IDE add more. */
93
+ export const SUMMARY_COLUMNS = [
94
+ "conversation_id",
95
+ "title",
96
+ "preview",
97
+ "step_count",
98
+ "last_modified_time",
99
+ "workspace_uris",
100
+ "status",
101
+ "source",
102
+ "project_id",
103
+ "agent_name",
104
+ "parent_conversation_id",
105
+ "nesting_depth",
106
+ "battle_id",
107
+ "winning_conversation_id",
108
+ "not_fully_idle",
109
+ "killed",
110
+ "last_user_input_time",
111
+ "last_user_input_step_index",
112
+ "app_data_dir",
113
+ ] as const
114
+
115
+ /** A google.protobuf.Timestamp ({1: seconds, 2: nanos}) as ISO time. */
116
+ export function timestampOf(fields: Field[] | undefined): string | undefined {
117
+ const seconds = fields && numberOf(fields, 1)
118
+ if (!seconds || seconds <= 0) return undefined
119
+ const nanos = numberOf(fields!, 2) ?? 0
120
+ const ms = seconds * 1000 + Math.floor(nanos / 1e6)
121
+ return Number.isFinite(ms) && ms < 8.64e15 ? new Date(ms).toISOString() : undefined
122
+ }
123
+
124
+ /** A summaries datetime ("2026-09-01 00:08:05.04913412+00:00", as Go writes it) as ISO time; Go's zero time is none. */
125
+ export function sqlTime(v: unknown): string | undefined {
126
+ if (typeof v !== "string") return undefined
127
+ const m = /^(\d{4}-\d\d-\d\d)[ T](\d\d:\d\d:\d\d)(?:\.(\d+))?\s*(Z|[+-]\d\d:?\d\d)?$/.exec(v.trim())
128
+ if (!m || m[1]!.startsWith("0001-")) return undefined
129
+ const frac = (m[3] ?? "").slice(0, 3).padEnd(3, "0")
130
+ const t = Date.parse(`${m[1]}T${m[2]}.${frac}${m[4] ?? "Z"}`)
131
+ return Number.isFinite(t) ? new Date(t).toISOString() : undefined
132
+ }
133
+
134
+ /** A time as the summaries table stores it. */
135
+ export const toSqlTime = (d: Date) => `${d.toISOString().slice(0, 10)} ${d.toISOString().slice(11, 23)}+00:00`
136
+
137
+ /** A tool call as a planner response stores it. */
138
+ export interface NativeCall {
139
+ id: string
140
+ name: string
141
+ /** The arguments JSON as stored. */
142
+ args: string
143
+ }
144
+
145
+ export function decodeCall(b: Uint8Array): NativeCall | undefined {
146
+ const f = decode(b)
147
+ const id = textOf(f, 1)
148
+ const name = textOf(f, 2)
149
+ if (!id || !name) return undefined
150
+ return { id, name, args: textOf(f, 3) ?? "" }
151
+ }
152
+
153
+ /** Whether a planner response holds private thinking (visible or raw thinking, a signature, or the thinking flag). */
154
+ export const hasThinking = (planner: Field[]) => [3, 4, 14, 16].some((n) => allBytes(planner, n).length > 0) || numberOf(planner, 5) === 1
155
+
156
+ /** The {key, value} entries of a repeated map field. */
157
+ export function entriesOf(fields: Field[], no: number): Array<[string, string]> {
158
+ const out: Array<[string, string]> = []
159
+ for (const e of allBytes(fields, no)) {
160
+ try {
161
+ const f = decode(e)
162
+ const k = textOf(f, 1)
163
+ if (k !== undefined) out.push([k, textOf(f, 2) ?? ""])
164
+ } catch {
165
+ // An entry that is not a {key, value} message is skipped.
166
+ }
167
+ }
168
+ return out
169
+ }
170
+
171
+ /** What Antigravity's own tools do; other names go through the shared guess. */
172
+ const KINDS: Record<string, ToolKind> = {
173
+ view_file: "read",
174
+ view_file_outline: "read",
175
+ view_code_item: "read",
176
+ list_dir: "list",
177
+ find_by_name: "search",
178
+ grep_search: "search",
179
+ codebase_search: "search",
180
+ run_command: "shell",
181
+ command_status: "shell",
182
+ send_command_input: "shell",
183
+ shell_exec: "shell",
184
+ write_to_file: "write",
185
+ replace_file_content: "edit",
186
+ multi_replace_file_content: "edit",
187
+ read_url_content: "web",
188
+ search_web: "web",
189
+ }
190
+ export const antigravityToolKind = (name: string): ToolKind => KINDS[name] ?? toolKindOf(name)
191
+
192
+ // Encoding the four messages the writer emits.
193
+
194
+ const uuidText = (no: number, id: string) => textField(no, id)
195
+
196
+ /** A whole Step: type, status, and the body in its type's field. */
197
+ export function stepBytes(type: number, status: number, body: Uint8Array): Uint8Array {
198
+ const field = PAYLOAD_FIELD[type]
199
+ if (field === undefined) throw new Error(`no payload field is known for Antigravity step type ${type}`)
200
+ return concat([varintField(1, type), varintField(4, status), messageField(field, [body])])
201
+ }
202
+
203
+ export const userInputBody = (text: string) => textField(2, text)
204
+
205
+ export function plannerBody(o: { text?: string; messageId: string; calls?: NativeCall[] }): Uint8Array {
206
+ return concat([
207
+ ...(o.text ? [textField(1, o.text)] : []),
208
+ uuidText(6, o.messageId),
209
+ ...(o.calls ?? []).map((c) => messageField(7, [textField(1, c.id), textField(2, c.name), textField(3, c.args)])),
210
+ ])
211
+ }
212
+
213
+ export function genericBody(o: { args: Array<[string, string]>; result: string; callId: string }): Uint8Array {
214
+ return concat([
215
+ ...o.args.map(([k, v]) => messageField(1, [textField(1, k), textField(2, v)])),
216
+ messageField(2, [textField(1, o.result), messageField(2, [textField(1, LINK_KEY), textField(2, o.callId)])]),
217
+ ])
218
+ }
219
+
220
+ /** The minimal "main" trajectory metadata: creation time, root conversation id, project. */
221
+ export const trajectoryMetadata = (seconds: number, conversationId: string) =>
222
+ concat([messageField(2, [varintField(1, seconds)]), textField(6, conversationId), textField(18, PROJECT_ID)])
223
+
224
+ /** The first workspace folder in trajectory metadata (field 1's folder URI, else field 7). */
225
+ export function workspaceOf(meta: Field[]): string | undefined {
226
+ for (const w of allBytes(meta, 1)) {
227
+ try {
228
+ const uri = textOf(decode(w), 1)
229
+ if (uri) return uri
230
+ } catch {
231
+ // not a workspace message
232
+ }
233
+ }
234
+ const first = allBytes(meta, 7)[0]
235
+ return first ? asText(first) : undefined
236
+ }