@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,325 @@
1
+ import { randomBytes, randomUUID } from "node:crypto"
2
+ import { copyFileSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs"
3
+ import { basename, join, resolve } from "node:path"
4
+ import { toolLabel } from "../../brief.ts"
5
+ import { exists, isDir } from "../../fsutil.ts"
6
+ import type { 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 { type LokiDb, amazonQReader, tabsOf } from "./reader.ts"
10
+ import {
11
+ DUMMY_ANSWER, LABEL, SUMMARY_PREFIX, TESTED_SERVERS, TOOL,
12
+ asQCall, continuedNote, dbName, editors, historyDir, serverRunning, serverVersion, workspaceId,
13
+ } from "./records.ts"
14
+
15
+ // Continues a portable session in Amazon Q's IDE chat as a new conversation (tab) in the workspace's
16
+ // history database, the document ChatDatabase.addMessage builds: prompts and answers alternating, each
17
+ // answer's toolUses answered by the next prompt's toolResults, ids in Q's style. The tab is marked open,
18
+ // so Amazon Q restores it when the editor next opens the folder (tabBarController.loadChats); it is also
19
+ // in Amazon Q's History list. Its messages are the conversation history the agent sends with the next
20
+ // request, so the model has the whole conversation.
21
+ //
22
+ // The database holds the workspace's other conversations too, so the writer only adds a document and
23
+ // never changes another, refuses while Amazon Q's language server runs (it keeps the database in memory
24
+ // and saves it every second), and copies the file to ~/.brain/backups/amazon-q-ide/<time>/ first.
25
+ //
26
+ // Amazon Q's own conventions decide the rest: the note saying where the session came from is a prompt
27
+ // the panel does not show (shouldDisplayMessage false) followed by Q's hidden "Working..." answer, the
28
+ // pair Q itself stores to keep prompts and answers alternating; a compaction starts the conversation from
29
+ // its summary, as Q's replaceWithSummary does. Reasoning has no place in Q's history and is dropped.
30
+
31
+ type Row = Record<string, unknown>
32
+ type Call = Extract<Turn, { kind: "tool_call" }>
33
+ type Result = Extract<Turn, { kind: "tool_result" }>
34
+
35
+ interface QMessage {
36
+ body: string
37
+ type: "prompt" | "answer"
38
+ timestamp: string
39
+ shouldDisplayMessage: boolean
40
+ origin?: "IDE"
41
+ userInputMessageContext?: { toolResults?: Row[] }
42
+ images?: unknown[]
43
+ toolUses?: Array<{ toolUseId: string; name: string; input: Record<string, unknown> }>
44
+ }
45
+
46
+ const ALNUM = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
47
+ /** Amazon Q's tool use ids: tooluse_ and 22 characters. */
48
+ const toolUseId = () => `tooluse_${[...randomBytes(22)].map((b) => ALNUM[b % ALNUM.length]).join("")}`
49
+
50
+ const shellQuote = (s: string) => (/^[\w./@%+=:,-]+$/.test(s) ? s : `'${s.replace(/'/g, `'\\''`)}'`)
51
+
52
+ export interface QPlan {
53
+ tab: Row
54
+ lost: string[]
55
+ expect: { users: number; calls: number }
56
+ }
57
+
58
+ /** The tab document of a new Amazon Q conversation continuing `session` (without $loki and meta). Pure. */
59
+ export function planTab(session: Session, opts: { historyId: string; conversationId: string; now: Date }): QPlan {
60
+ const drops = new Map<string, number>()
61
+ const drop = (what: string, n = 1) => drops.set(what, (drops.get(what) ?? 0) + n)
62
+ const label = toolLabel(session.header.tool)
63
+ const turns = session.turns
64
+ const lastCompaction = turns.map((t) => t.kind).lastIndexOf("compaction")
65
+ if (lastCompaction > 0) drop("turns before the last compaction (Amazon Q keeps only the summary)", turns.slice(0, lastCompaction).filter((t) => t.kind === "user" || t.kind === "assistant").length)
66
+
67
+ const results = new Map<string, Result>()
68
+ const callIds = new Set<string>()
69
+ for (const t of turns) {
70
+ if (t.kind === "tool_result" && !results.has(t.callId)) results.set(t.callId, t)
71
+ if (t.kind === "tool_call") callIds.add(t.id)
72
+ }
73
+ const ids = new Map<string, string>()
74
+ const qId = (id: string) => {
75
+ let v = ids.get(id)
76
+ if (!v) ids.set(id, (v = toolUseId()))
77
+ return v
78
+ }
79
+
80
+ const known = turns.map((t) => (t.at ? Date.parse(t.at) : Number.NaN)).find(Number.isFinite)
81
+ let clock = (known ?? opts.now.getTime()) - 1
82
+ const stamp = (at?: string) => {
83
+ const t = at ? Date.parse(at) : Number.NaN
84
+ clock = Number.isFinite(t) && t > clock ? t : clock + 1
85
+ return new Date(clock).toISOString()
86
+ }
87
+
88
+ const msgs: QMessage[] = []
89
+ const prompt = (body: string, shown: boolean, at?: string, toolResults?: Row[]): QMessage => ({
90
+ body,
91
+ type: "prompt",
92
+ origin: "IDE",
93
+ userInputMessageContext: toolResults ? { toolResults } : {},
94
+ shouldDisplayMessage: shown,
95
+ timestamp: stamp(at),
96
+ images: [],
97
+ })
98
+ const dummy = (): QMessage => ({ body: DUMMY_ANSWER, type: "answer", shouldDisplayMessage: false, timestamp: stamp() })
99
+ /** Appends keeping Q's invariant: prompts and answers alternate, starting with a prompt. */
100
+ const push = (m: QMessage) => {
101
+ const last = msgs[msgs.length - 1]
102
+ if (last?.type === "prompt" && m.type === "prompt") {
103
+ // A request typed right after tool results travels with them, as Q sends it; otherwise Q's hidden placeholder answer goes between.
104
+ if (!last.body && last.userInputMessageContext?.toolResults && !m.userInputMessageContext?.toolResults && m.shouldDisplayMessage) {
105
+ last.body = m.body
106
+ return
107
+ }
108
+ msgs.push(dummy())
109
+ } else if (last?.type === "answer" && m.type === "answer" && !last.toolUses?.length) {
110
+ last.body = [last.body, m.body].filter((b) => b.trim()).join("\n\n")
111
+ last.toolUses = m.toolUses ?? []
112
+ return
113
+ }
114
+ msgs.push(m)
115
+ }
116
+
117
+ push(prompt(continuedNote(label, session.header.id, opts.now.toISOString().slice(0, 10)), false))
118
+
119
+ let cur: { text: string; at?: string; calls: Call[] } | undefined
120
+ const flush = () => {
121
+ if (!cur) return
122
+ const a = cur
123
+ cur = undefined
124
+ if (!a.text.trim() && !a.calls.length) return
125
+ push({
126
+ body: a.text,
127
+ type: "answer",
128
+ toolUses: a.calls.map((c) => {
129
+ const q = asQCall(c.toolKind, c.name, c.input)
130
+ return { toolUseId: qId(c.id), name: q.name, input: q.input }
131
+ }),
132
+ shouldDisplayMessage: true,
133
+ timestamp: stamp(a.at),
134
+ })
135
+ if (!a.calls.length) return
136
+ const toolResults = a.calls.map((c) => {
137
+ const r = results.get(c.id)
138
+ if (!r) drop("tool calls without a result (closed as errors)")
139
+ const text = r ? r.output + (r.fullPath ? `\n\n[Full output: ${r.fullPath}]` : "") : "No result was recorded for this call."
140
+ return { toolUseId: qId(c.id), status: r?.ok ? "success" : "error", content: [{ text: text || "(no output)" }] }
141
+ })
142
+ push(prompt("", true, results.get(a.calls[a.calls.length - 1]!.id)?.at, toolResults))
143
+ }
144
+
145
+ const expect = { users: 0, calls: 0 }
146
+ const seen = new Set<string>()
147
+ for (const [i, t] of turns.entries()) {
148
+ if (i < lastCompaction) continue
149
+ switch (t.kind) {
150
+ case "compaction":
151
+ push(prompt(`${SUMMARY_PREFIX} in ${label}, which replaced the turns before it]\n\n${t.summary}`, true, t.at))
152
+ break
153
+ case "user":
154
+ if (t.attachments?.length) drop("attachments", t.attachments.length)
155
+ if (!t.text.trim()) break
156
+ flush()
157
+ push(prompt(t.text, true, t.at))
158
+ expect.users++
159
+ break
160
+ case "assistant":
161
+ if (!t.text.trim()) break
162
+ // Text after a call is a new answer: it was written after the call's result came back.
163
+ if (cur?.calls.length) flush()
164
+ cur ??= { text: "", ...(t.at ? { at: t.at } : {}), calls: [] }
165
+ cur.text = cur.text ? `${cur.text}\n\n${t.text}` : t.text
166
+ break
167
+ case "tool_call":
168
+ if (seen.has(t.id)) {
169
+ drop("repeated tool call ids")
170
+ break
171
+ }
172
+ seen.add(t.id)
173
+ cur ??= { text: "", ...(t.at ? { at: t.at } : {}), calls: [] }
174
+ cur.calls.push(t)
175
+ expect.calls++
176
+ break
177
+ case "tool_result":
178
+ if (!callIds.has(t.callId)) drop("tool results without a call")
179
+ // A result closes the answer it belongs to, so a call made after it starts a new answer.
180
+ else if (cur?.calls.some((c) => c.id === t.callId)) flush()
181
+ break
182
+ case "reasoning":
183
+ drop("reasoning")
184
+ break
185
+ case "file_edit":
186
+ if (!t.callId || !callIds.has(t.callId)) drop("file edits outside a tool call")
187
+ break
188
+ case "subagent":
189
+ drop("subagent sessions")
190
+ break
191
+ }
192
+ }
193
+ flush()
194
+ // Q ends a stored conversation on an answer; a trailing request gets its hidden placeholder, as Q adds one.
195
+ if (msgs[msgs.length - 1]?.type === "prompt") msgs.push(dummy())
196
+
197
+ const title = session.header.title ?? firstUserText(session)?.replace(/\s+/g, " ").trim().slice(0, 60) ?? "Session"
198
+ const tab: Row = {
199
+ historyId: opts.historyId,
200
+ // LokiJS stores a top-level Date as epoch milliseconds; nested ones as ISO strings.
201
+ updatedAt: opts.now.getTime(),
202
+ isOpen: true,
203
+ tabType: "cwc",
204
+ title: `${title} (from ${label})`,
205
+ conversations: [{ conversationId: opts.conversationId, clientType: "vscode", updatedAt: opts.now.toISOString(), messages: msgs }],
206
+ }
207
+ return { tab, lost: [...drops].map(([what, n]) => `${what} (${n})`), expect }
208
+ }
209
+
210
+ /** A database file as LokiJS 1.5 writes one for a workspace whose history is still empty (copied from Q's own). */
211
+ export function emptyDb(file: string, now: Date): LokiDb {
212
+ const events = { insert: [], update: [], "pre-insert": [], "pre-update": [], close: [], flushbuffer: [], error: [], delete: [null], warning: [null] }
213
+ const collection = (name: string, extra: Row) => ({
214
+ name, data: [], idIndex: null, binaryIndices: {}, constraints: null, uniqueNames: [], transforms: {}, objType: name, dirty: false, cachedIndex: null,
215
+ cachedBinaryIndex: null, cachedData: null, adaptiveBinaryIndices: true, transactional: false, cloneObjects: false, cloneMethod: "parse-stringify",
216
+ asyncListeners: false, disableMeta: false, disableChangesApi: true, disableDeltaChangesApi: true, autoupdate: false, serializableIndices: true,
217
+ disableFreeze: true, ttl: null, maxId: 0, DynamicViews: [], events, changes: [], dirtyIds: [], isIncremental: false, ...extra,
218
+ })
219
+ return {
220
+ filename: basename(file),
221
+ collections: [
222
+ collection("tabs", { binaryIndices: { updatedAt: { name: "updatedAt", dirty: false, values: [] }, isOpen: { name: "isOpen", dirty: false, values: [] } }, uniqueNames: ["historyId"] }),
223
+ collection("settings", {}),
224
+ ],
225
+ databaseVersion: 1.5,
226
+ engineVersion: 1.5,
227
+ autosave: true,
228
+ autosaveInterval: 1000,
229
+ autosaveHandle: null,
230
+ throttledSaves: true,
231
+ options: { adapter: null, autosave: true, autoload: true, autosaveInterval: 1000, persistenceMethod: "fs", serializationMethod: "normal", destructureDelimiter: "$<\n", recursiveWait: true, recursiveWaitLimit: false, recursiveWaitLimitDuration: 2000, started: now.getTime() },
232
+ persistenceMethod: "adapter",
233
+ persistenceAdapter: null,
234
+ verbose: false,
235
+ events: { init: [null], loaded: [], flushChanges: [], close: [], changes: [], warning: [] },
236
+ ENV: "NODEJS",
237
+ isIncremental: false,
238
+ }
239
+ }
240
+
241
+ /** Adds `tab` to the database as LokiJS's insert would: the next $loki id, fresh meta, indices left for LokiJS to rebuild. */
242
+ export function insertTab(db: LokiDb, tab: Row, now: Date): void {
243
+ const tabs = db.collections.find((c) => c.name === "tabs")
244
+ if (!tabs) throw new Error("the history file has no tabs collection")
245
+ const id = (tabs.maxId ?? 0) + 1
246
+ tabs.maxId = id
247
+ tabs.data.push({ ...tab, meta: { revision: 0, created: now.getTime(), version: 0 }, $loki: id })
248
+ if (Array.isArray(tabs.idIndex)) tabs.idIndex.push(id)
249
+ // Marked dirty, LokiJS rebuilds a binary index before its next use, as it does after its own inserts.
250
+ for (const index of Object.values(tabs.binaryIndices ?? {})) index.dirty = true
251
+ }
252
+
253
+ /** The Amazon Q writer; `server` and `running` are for tests, which must not depend on this machine. */
254
+ export function amazonQWriter(opts: { server?: string; running?: () => boolean } = {}): Writer {
255
+ const installed = (home: string) => opts.server ?? serverVersion(home)
256
+ const running = opts.running ?? serverRunning
257
+ return {
258
+ tool: TOOL,
259
+ label: LABEL,
260
+ way: "native",
261
+
262
+ async ready(w: WriteOptions): Promise<Readiness> {
263
+ const v = installed(w.home)
264
+ if (!v) return { ok: false, why: "Amazon Q's language server was not found (it is downloaded when the Amazon Q extension first starts)" }
265
+ if (!TESTED_SERVERS.includes(v)) return { ok: false, why: `Amazon Q language server ${v} is not a version this writer was checked against (${TESTED_SERVERS.join(", ")})` }
266
+ if (running()) return { ok: false, why: "Amazon Q is running (its language server keeps the chat history in memory and would overwrite the new conversation); close VS Code, Cursor or the JetBrains IDE first" }
267
+ if (!isDir(w.cwd)) return { ok: false, why: `${w.cwd} is not a folder; Amazon Q keeps chat history per workspace folder` }
268
+ return { ok: true }
269
+ },
270
+
271
+ async write(session: Session, w: WriteOptions): Promise<WriteResult> {
272
+ const cwd = resolve(w.cwd)
273
+ const now = new Date()
274
+ const file = join(historyDir(w.home), dbName(workspaceId(cwd)))
275
+ const backupDir = join(w.home, ".brain", "backups", TOOL, now.toISOString().replace(/[:.]/g, "-"))
276
+ const had = exists(file)
277
+ let db: LokiDb
278
+ if (had) {
279
+ try {
280
+ db = JSON.parse(readFileSync(file, "utf8")) as LokiDb
281
+ } catch {
282
+ throw new Error(`${file} is not a readable Amazon Q history file; nothing was changed`)
283
+ }
284
+ } else db = emptyDb(file, now)
285
+ let historyId = randomUUID()
286
+ while (tabsOf(db).some((t) => t.historyId === historyId)) historyId = randomUUID()
287
+ const plan = planTab(session, { historyId, conversationId: randomUUID(), now })
288
+ insertTab(db, plan.tab, now)
289
+
290
+ if (!w.dryRun) {
291
+ mkdirSync(historyDir(w.home), { recursive: true })
292
+ if (had) {
293
+ mkdirSync(backupDir, { recursive: true })
294
+ copyFileSync(file, join(backupDir, basename(file)))
295
+ }
296
+ const tmp = `${file}.brain-${process.pid}`
297
+ writeFileSync(tmp, JSON.stringify(db), { mode: 0o600 })
298
+ renameSync(tmp, file)
299
+ try {
300
+ const back = await amazonQReader.read({ tool: TOOL, sourceId: historyId, path: file }, w.home)
301
+ const n = countTurns(back)
302
+ const pairs = unpaired(back)
303
+ if (n.user !== plan.expect.users || n.tool_call !== plan.expect.calls || pairs.calls.length || pairs.results.length)
304
+ throw new Error(`the new Amazon Q conversation did not read back intact (requests ${n.user}/${plan.expect.users}, tool calls ${n.tool_call}/${plan.expect.calls}, unpaired ${pairs.calls.length + pairs.results.length})`)
305
+ } catch (e) {
306
+ if (had) copyFileSync(join(backupDir, basename(file)), file)
307
+ else rmSync(file, { force: true })
308
+ throw new Error(`${(e as Error).message}; the history file was restored`)
309
+ }
310
+ }
311
+ const editor = editors(w.home)[0] ?? "code"
312
+ return {
313
+ tool: TOOL,
314
+ way: "native",
315
+ sessionId: historyId,
316
+ written: had ? [file, join(backupDir, basename(file))] : [file],
317
+ open: `${editor} ${shellQuote(cwd)} # Amazon Q reopens "${plan.tab.title as string}" when the folder opens; it is also in Amazon Q's History`,
318
+ lost: plan.lost,
319
+ dryRun: !!w.dryRun,
320
+ }
321
+ },
322
+ }
323
+ }
324
+
325
+ export const amazonQNativeWriter = amazonQWriter()
@@ -0,0 +1,9 @@
1
+ import type { Reader, Writer } from "../types.ts"
2
+ import { antigravityReader } from "./reader.ts"
3
+ import { antigravityWriter } from "./writer.ts"
4
+
5
+ // Antigravity (Google): reads the CLI's conversations and the IDE's SQLite ones under ~/.gemini, and continues
6
+ // any session as a new native conversation of the Antigravity CLI (1.1.16). The IDE has no native writer; it is a
7
+ // target through the continuation brief.
8
+ export const readers: Reader[] = [antigravityReader]
9
+ export const writers: Writer[] = [antigravityWriter]
@@ -0,0 +1,194 @@
1
+ // A small, bounded protobuf wire codec: enough to read and write the few Antigravity messages brain needs,
2
+ // with no generated classes and no descriptors. It takes varints (canonical, at most 64 bits), fixed64,
3
+ // length-delimited values and fixed32; it rejects groups and unknown wire types, caps every length and the
4
+ // number of fields in one message, and never recurses on its own (callers decode nested messages they know).
5
+ // Ported from the wire subset session-migrate (MIT, xhluca) pins for Antigravity CLI 1.1.16.
6
+
7
+ export const WIRE = { varint: 0, fixed64: 1, bytes: 2, fixed32: 5 } as const
8
+
9
+ export interface Field {
10
+ no: number
11
+ wire: 0 | 1 | 2 | 5
12
+ /** A varint as a number when it is a safe integer, else a bigint; raw bytes for the other wire types. */
13
+ value: number | bigint | Uint8Array
14
+ }
15
+
16
+ export interface Limits {
17
+ /** Longest length-delimited value, and longest message. */
18
+ maxBytes: number
19
+ /** Most fields in one message. */
20
+ maxFields: number
21
+ }
22
+
23
+ export const LIMITS: Limits = { maxBytes: 64 * 1024 * 1024, maxFields: 100_000 }
24
+
25
+ export class ProtoError extends Error {
26
+ constructor(why: string) {
27
+ super(`protobuf: ${why}`)
28
+ }
29
+ }
30
+
31
+ const MAX_FIELD_NO = 536_870_911
32
+
33
+ /** A varint at `at`: its value and where the next byte is. Rejects truncated, over-long and non-canonical encodings. */
34
+ function readVarint(b: Uint8Array, at: number): [bigint, number] {
35
+ let v = 0n
36
+ for (let i = 0; i < 10; i++) {
37
+ if (at >= b.length) throw new ProtoError("varint is truncated")
38
+ const byte = b[at++]!
39
+ if (i === 9 && byte > 1) throw new ProtoError("varint exceeds 64 bits")
40
+ v |= BigInt(byte & 0x7f) << BigInt(7 * i)
41
+ if (byte < 0x80) {
42
+ // Canonical: no trailing zero byte (0x80 0x00 is 0 written long).
43
+ if (i > 0 && byte === 0) throw new ProtoError("varint is not canonical")
44
+ return [v, at]
45
+ }
46
+ }
47
+ throw new ProtoError("varint exceeds 64 bits")
48
+ }
49
+
50
+ const small = (v: bigint): number | bigint => (v <= BigInt(Number.MAX_SAFE_INTEGER) ? Number(v) : v)
51
+
52
+ /** The fields of one message, in wire order. Throws ProtoError on anything outside the subset or the limits. */
53
+ export function decode(b: Uint8Array, limits: Limits = LIMITS): Field[] {
54
+ if (b.length > limits.maxBytes) throw new ProtoError("message exceeds the size limit")
55
+ const out: Field[] = []
56
+ let at = 0
57
+ while (at < b.length) {
58
+ if (out.length >= limits.maxFields) throw new ProtoError("message has too many fields")
59
+ const [key, next] = readVarint(b, at)
60
+ at = next
61
+ const no = Number(key >> 3n)
62
+ const wire = Number(key & 7n)
63
+ if (no <= 0 || no > MAX_FIELD_NO) throw new ProtoError(`invalid field number ${no}`)
64
+ switch (wire) {
65
+ case 0: {
66
+ const [v, n] = readVarint(b, at)
67
+ at = n
68
+ out.push({ no, wire: 0, value: small(v) })
69
+ break
70
+ }
71
+ case 1:
72
+ case 5: {
73
+ const size = wire === 1 ? 8 : 4
74
+ if (at + size > b.length) throw new ProtoError(`fixed${size * 8} value is truncated`)
75
+ out.push({ no, wire, value: b.slice(at, at + size) })
76
+ at += size
77
+ break
78
+ }
79
+ case 2: {
80
+ const [len, n] = readVarint(b, at)
81
+ at = n
82
+ if (len > BigInt(limits.maxBytes)) throw new ProtoError("value exceeds the size limit")
83
+ const end = at + Number(len)
84
+ if (end > b.length) throw new ProtoError("length-delimited value is truncated")
85
+ out.push({ no, wire: 2, value: b.slice(at, end) })
86
+ at = end
87
+ break
88
+ }
89
+ case 3:
90
+ case 4:
91
+ throw new ProtoError("groups are not supported")
92
+ default:
93
+ throw new ProtoError(`unknown wire type ${wire}`)
94
+ }
95
+ }
96
+ return out
97
+ }
98
+
99
+ // Encoding.
100
+
101
+ function varintBytes(v: number | bigint): Uint8Array {
102
+ if (typeof v === "number" && (!Number.isSafeInteger(v) || v < 0)) throw new ProtoError("cannot encode a negative, fractional or unsafe varint")
103
+ let n = typeof v === "bigint" ? v : BigInt(v)
104
+ if (n < 0n || n >= 1n << 64n) throw new ProtoError("varint out of range")
105
+ const out: number[] = []
106
+ while (n >= 0x80n) {
107
+ out.push(Number(n & 0x7fn) | 0x80)
108
+ n >>= 7n
109
+ }
110
+ out.push(Number(n))
111
+ return Uint8Array.from(out)
112
+ }
113
+
114
+ export function concat(parts: Uint8Array[]): Uint8Array {
115
+ const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0))
116
+ let at = 0
117
+ for (const p of parts) {
118
+ out.set(p, at)
119
+ at += p.length
120
+ }
121
+ return out
122
+ }
123
+
124
+ const key = (no: number, wire: number) => {
125
+ if (!Number.isInteger(no) || no <= 0 || no > MAX_FIELD_NO) throw new ProtoError(`invalid field number ${no}`)
126
+ return varintBytes(BigInt(no) * 8n + BigInt(wire))
127
+ }
128
+
129
+ export const varintField = (no: number, v: number | bigint) => concat([key(no, 0), varintBytes(v)])
130
+
131
+ export function bytesField(no: number, v: Uint8Array, limits: Limits = LIMITS): Uint8Array {
132
+ if (v.length > limits.maxBytes) throw new ProtoError("value exceeds the size limit")
133
+ return concat([key(no, 2), varintBytes(v.length), v])
134
+ }
135
+
136
+ export const textField = (no: number, s: string) => bytesField(no, new TextEncoder().encode(s))
137
+
138
+ /** A nested message as a length-delimited field. */
139
+ export const messageField = (no: number, parts: Uint8Array[]) => bytesField(no, concat(parts))
140
+
141
+ export function fixedField(no: number, v: Uint8Array): Uint8Array {
142
+ if (v.length !== 4 && v.length !== 8) throw new ProtoError("a fixed value is 4 or 8 bytes")
143
+ return concat([key(no, v.length === 8 ? 1 : 5), v])
144
+ }
145
+
146
+ /** Decoded fields back to bytes: encode(decode(b)) is b for any canonical message. */
147
+ export function encode(fields: Field[]): Uint8Array {
148
+ return concat(
149
+ fields.map((f) => {
150
+ if (f.wire === 0) return varintField(f.no, f.value as number | bigint)
151
+ if (f.wire === 2) return bytesField(f.no, f.value as Uint8Array)
152
+ return fixedField(f.no, f.value as Uint8Array)
153
+ }),
154
+ )
155
+ }
156
+
157
+ // Reading fields. Protobuf lets a singular field repeat (the last one wins), so these take the last.
158
+
159
+ const last = (fields: Field[], no: number, wire: number) => {
160
+ for (let i = fields.length - 1; i >= 0; i--) if (fields[i]!.no === no && fields[i]!.wire === wire) return fields[i]!
161
+ return undefined
162
+ }
163
+
164
+ /** Every length-delimited value of a repeated field, in order. */
165
+ export const allBytes = (fields: Field[], no: number): Uint8Array[] => fields.filter((f) => f.no === no && f.wire === 2).map((f) => f.value as Uint8Array)
166
+
167
+ export const bytesOf = (fields: Field[], no: number): Uint8Array | undefined => last(fields, no, 2)?.value as Uint8Array | undefined
168
+
169
+ export function numberOf(fields: Field[], no: number): number | undefined {
170
+ const v = last(fields, no, 0)?.value
171
+ return v === undefined ? undefined : Number(v)
172
+ }
173
+
174
+ const utf8 = new TextDecoder("utf-8", { fatal: true })
175
+
176
+ /** A value as UTF-8 text; undefined when absent or not valid UTF-8. */
177
+ export function textOf(fields: Field[], no: number): string | undefined {
178
+ const b = bytesOf(fields, no)
179
+ return b === undefined ? undefined : asText(b)
180
+ }
181
+
182
+ export function asText(b: Uint8Array): string | undefined {
183
+ try {
184
+ return utf8.decode(b)
185
+ } catch {
186
+ return undefined
187
+ }
188
+ }
189
+
190
+ /** A nested message, decoded; undefined when absent. */
191
+ export function messageOf(fields: Field[], no: number, limits: Limits = LIMITS): Field[] | undefined {
192
+ const b = bytesOf(fields, no)
193
+ return b === undefined ? undefined : decode(b, limits)
194
+ }