@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,131 @@
1
+ import { join } from "node:path"
2
+ import { ls, readJson } from "../../fsutil.ts"
3
+ import type { Session, Turn } from "../../session/types.ts"
4
+ import { asText, clip, header, iso, lossCounter, toolKindOf } from "../../session/util.ts"
5
+ import type { Reader, SessionRef } from "../types.ts"
6
+ import { CONTINUED_NOTE, DUMMY_ANSWER, LABEL, SUMMARY_PREFIX, TOOL, historyDir, qKind } from "./records.ts"
7
+
8
+ // Reads Amazon Q's IDE chat history: one LokiJS file per workspace, each tab (conversation) a document
9
+ // in its "tabs" collection. Messages alternate prompt / answer; an answer's toolUses are answered by the
10
+ // next prompt's userInputMessageContext.toolResults (a prompt with an empty body carries only results).
11
+ // Prompts and answers Amazon Q hides (shouldDisplayMessage false) are its own: placeholder answers,
12
+ // retries, generated instructions.
13
+
14
+ type Row = Record<string, any>
15
+
16
+ export interface LokiDb {
17
+ collections: Array<{ name: string; data: Row[]; maxId?: number; idIndex?: number[] | null; binaryIndices?: Record<string, { name: string; dirty: boolean; values: number[] }> }>
18
+ [k: string]: unknown
19
+ }
20
+
21
+ export const tabsOf = (db: LokiDb | undefined): Row[] => db?.collections?.find((c) => c.name === "tabs")?.data ?? []
22
+
23
+ /** A tool result's content blocks as text: text blocks as they are, JSON blocks ({output} or {error}) unwrapped. */
24
+ function resultText(content: unknown): string {
25
+ const parts: string[] = []
26
+ for (const b of Array.isArray(content) ? (content as Row[]) : []) {
27
+ if (typeof b?.text === "string") parts.push(b.text)
28
+ else if (b?.json !== undefined) {
29
+ const j = b.json
30
+ parts.push(j && typeof j === "object" && !Array.isArray(j) && Object.keys(j).length === 1 && ("output" in j || "error" in j) ? asText(j.output ?? j.error) : asText(j))
31
+ }
32
+ }
33
+ return parts.join("\n")
34
+ }
35
+
36
+ function readTab(file: string, tab: Row): Session {
37
+ const messages = (Array.isArray(tab.conversations) ? (tab.conversations as Row[]) : []).flatMap((c) => (Array.isArray(c?.messages) ? (c.messages as Row[]) : []))
38
+ let cwd: string | undefined
39
+ for (const m of messages) {
40
+ const folders = m?.userInputMessageContext?.editorState?.workspaceFolders
41
+ if (Array.isArray(folders) && typeof folders[0] === "string") {
42
+ cwd = folders[0]
43
+ break
44
+ }
45
+ }
46
+ const stamps = messages.map((m) => iso(m?.timestamp)).filter((t): t is string => !!t)
47
+ const h = header(TOOL, String(tab.historyId), {
48
+ sourcePath: file,
49
+ ...(typeof tab.title === "string" && tab.title.trim() ? { title: tab.title } : {}),
50
+ project: cwd ? { cwd } : {},
51
+ ...(stamps[0] ? { startedAt: stamps[0] } : {}),
52
+ ...(iso(tab.updatedAt) ?? stamps[stamps.length - 1] ? { updatedAt: iso(tab.updatedAt) ?? stamps[stamps.length - 1] } : {}),
53
+ })
54
+ if (typeof tab.modelId === "string") h.models.push(tab.modelId)
55
+ const lost = lossCounter()
56
+ const turns: Turn[] = []
57
+ const calls = new Map<string, string>()
58
+ for (const m of messages) {
59
+ const at = iso(m?.timestamp)
60
+ const body = typeof m?.body === "string" ? m.body : ""
61
+ if (m?.type === "prompt") {
62
+ for (const r of (m.userInputMessageContext?.toolResults as Row[] | undefined) ?? []) {
63
+ if (typeof r?.toolUseId !== "string") continue
64
+ const { text, truncated } = clip(resultText(r.content))
65
+ turns.push({ kind: "tool_result", ...(at ? { at } : {}), callId: r.toolUseId, ok: r.status !== "error", output: text, ...(truncated ? { truncated } : {}) })
66
+ }
67
+ if (Array.isArray(m.images) && m.images.length) lost.add("images", m.images.length)
68
+ if (!body.trim()) continue
69
+ // A summary a continued session starts from (shown, as Amazon Q shows its own compaction summary).
70
+ if (body.startsWith(SUMMARY_PREFIX)) {
71
+ turns.push({ kind: "compaction", ...(at ? { at } : {}), summary: body.replace(/^\[[^\]]*\]\s*/, "") })
72
+ continue
73
+ }
74
+ if (m.shouldDisplayMessage === false) {
75
+ const from = body.match(CONTINUED_NOTE)
76
+ if (from) h.parent = { id: from[1]!, kind: "continued" }
77
+ else lost.add("Amazon Q's hidden prompts (not carried)")
78
+ continue
79
+ }
80
+ turns.push({ kind: "user", ...(at ? { at } : {}), text: body })
81
+ } else if (m?.type === "answer") {
82
+ if (m.shouldDisplayMessage === false && body === DUMMY_ANSWER) continue
83
+ if (body.trim()) turns.push({ kind: "assistant", ...(at ? { at } : {}), text: body })
84
+ for (const u of (m.toolUses as Row[] | undefined) ?? []) {
85
+ if (typeof u?.toolUseId !== "string" || typeof u?.name !== "string" || calls.has(u.toolUseId)) continue
86
+ calls.set(u.toolUseId, u.name)
87
+ turns.push({ kind: "tool_call", ...(at ? { at } : {}), id: u.toolUseId, name: u.name, toolKind: qKind(u.name) ?? toolKindOf(u.name), input: u.input ?? {} })
88
+ }
89
+ } else lost.add(`unknown message types: ${String(m?.type)}`)
90
+ }
91
+ lost.flush(h)
92
+ return { header: h, turns }
93
+ }
94
+
95
+ export const amazonQReader: Reader = {
96
+ tool: TOOL,
97
+ label: LABEL,
98
+
99
+ async list(home: string): Promise<SessionRef[]> {
100
+ const refs: SessionRef[] = []
101
+ const dir = historyDir(home)
102
+ for (const name of ls(dir)) {
103
+ if (!/^chat-history-.+\.json$/.test(name)) continue
104
+ const file = join(dir, name)
105
+ for (const tab of tabsOf(readJson<LokiDb>(file))) {
106
+ if (typeof tab.historyId !== "string") continue
107
+ const hasMessages = (tab.conversations as Row[] | undefined)?.some((c) => Array.isArray(c?.messages) && c.messages.length)
108
+ if (!hasMessages) continue
109
+ const updatedAt = iso(tab.updatedAt)
110
+ const folders = (tab.conversations as Row[]).flatMap((c) => c.messages as Row[]).map((m) => m?.userInputMessageContext?.editorState?.workspaceFolders).find((f) => Array.isArray(f) && typeof f[0] === "string")
111
+ refs.push({
112
+ tool: TOOL,
113
+ sourceId: tab.historyId,
114
+ path: file,
115
+ ...(typeof tab.title === "string" ? { title: tab.title } : {}),
116
+ ...(folders ? { cwd: folders[0] as string } : {}),
117
+ ...(updatedAt ? { updatedAt } : {}),
118
+ })
119
+ }
120
+ }
121
+ return refs.sort((a, b) => (b.updatedAt ?? "").localeCompare(a.updatedAt ?? ""))
122
+ },
123
+
124
+ async read(ref: SessionRef): Promise<Session> {
125
+ const db = readJson<LokiDb>(ref.path)
126
+ if (!db) throw new Error(`${ref.path} is not a readable Amazon Q history file`)
127
+ const tab = tabsOf(db).find((t) => t.historyId === ref.sourceId)
128
+ if (!tab) throw new Error(`conversation ${ref.sourceId} is not in ${ref.path}`)
129
+ return readTab(ref.path, tab)
130
+ },
131
+ }
@@ -0,0 +1,141 @@
1
+ import { createHash } from "node:crypto"
2
+ import { join } from "node:path"
3
+ import { isDir, ls } from "../../fsutil.ts"
4
+ import type { ToolKind } from "../../session/types.ts"
5
+
6
+ // What Amazon Q Developer's IDE chat (the VS Code extension, and JetBrains, through the same language
7
+ // server) keeps on disk, as far as this adapter relies on it. Checked against aws/language-servers at
8
+ // ba9d3b1 (2026-11-06: agenticChat/tools/chatDb/chatDb.ts, util.ts, tabBarController.ts,
9
+ // agenticChatController.ts), the language server 1.85.0 bundle installed here, LokiJS 1.5.12, and the
10
+ // history files on the author's Mac (structure and counts only).
11
+ //
12
+ // Each workspace has one LokiJS database, ~/.aws/amazonq/history/chat-history-<id>.json, holding every
13
+ // conversation ("tab") of that workspace: <id> is the MD5 of the workspace folder's URI path (several
14
+ // folders: their sorted paths joined by |; a saved .code-workspace file: SHA-256 of its path). A tab's
15
+ // messages are both what the History panel restores and what the agent sends as conversation history
16
+ // (fixAndGetHistory reads them from the database on every request; conversation ids are client-made).
17
+
18
+ export const TOOL = "amazon-q-ide"
19
+ export const LABEL = "Amazon Q (IDE)"
20
+
21
+ /** Language server versions the writer's records were checked against. */
22
+ export const TESTED_SERVERS = ["1.85.0"]
23
+
24
+ export const historyDir = (home: string) => join(home, ".aws", "amazonq", "history")
25
+ export const dbName = (workspaceId: string) => `chat-history-${workspaceId}.json`
26
+
27
+ /**
28
+ * The path part of the folder's URI as VS Code sends it (vscode-uri encodes everything outside RFC 3986's
29
+ * unreserved set except '/'), which the server MD5s (`new URL(uri).pathname`).
30
+ */
31
+ export function uriPath(folder: string): string {
32
+ return folder
33
+ .split("/")
34
+ .map((seg) => encodeURIComponent(seg).replace(/[!'()*]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`))
35
+ .join("/")
36
+ }
37
+
38
+ /** The database id of a single-folder workspace. */
39
+ export const workspaceId = (folder: string) => createHash("md5").update(uriPath(folder)).digest("hex")
40
+
41
+ /** The newest Amazon Q language server downloaded to this home, e.g. "1.85.0". */
42
+ export function serverVersion(home: string): string | undefined {
43
+ const dir = join(home, "Library", "Caches", "aws", "toolkits", "language-servers", "AmazonQ")
44
+ const versions = ls(dir).filter((v) => /^\d+\.\d+\.\d+$/.test(v) && isDir(join(dir, v)))
45
+ const num = (v: string) => v.split(".").map(Number)
46
+ versions.sort((a, b) => {
47
+ const x = num(a)
48
+ const y = num(b)
49
+ for (let i = 0; i < 3; i++) if (x[i] !== y[i]) return x[i]! - y[i]!
50
+ return 0
51
+ })
52
+ return versions[versions.length - 1]
53
+ }
54
+
55
+ /** The editors with the Amazon Q extension installed here, as the command that opens a folder in them. */
56
+ export function editors(home: string): string[] {
57
+ const out: string[] = []
58
+ for (const [dir, cmd] of [[".vscode", "code"], [".cursor", "cursor"], [".windsurf", "windsurf"], [".kiro", "kiro"]] as const)
59
+ if (ls(join(home, dir, "extensions")).some((e) => e.startsWith("amazonwebservices.amazon-q-vscode-"))) out.push(cmd)
60
+ return out
61
+ }
62
+
63
+ /** Whether Amazon Q's language server is running: it keeps the database in memory and saves it every second. */
64
+ export function serverRunning(): boolean {
65
+ const p = Bun.spawnSync(["pgrep", "-f", "aws-lsp-codewhisperer"], { stdout: "pipe", stderr: "pipe" })
66
+ return p.exitCode === 0
67
+ }
68
+
69
+ /** Amazon Q's tools, by what they do (toolConstants.ts). */
70
+ const KIND_OF: Record<string, ToolKind> = {
71
+ fsRead: "read",
72
+ fsWrite: "write",
73
+ fsReplace: "edit",
74
+ listDirectory: "list",
75
+ grepSearch: "search",
76
+ fileSearch: "search",
77
+ executeBash: "shell",
78
+ codeReview: "other",
79
+ }
80
+ export const qKind = (name: string): ToolKind | undefined => KIND_OF[name]
81
+
82
+ const pick = (o: Record<string, unknown>, keys: string[]): unknown => {
83
+ for (const k of keys) if (o[k] !== undefined && o[k] !== null && o[k] !== "") return o[k]
84
+ return undefined
85
+ }
86
+ const str = (v: unknown) => (typeof v === "string" ? v : Array.isArray(v) && v.every((x) => typeof x === "string") ? v.join(" ") : undefined)
87
+
88
+ /**
89
+ * A call from another tool as the Amazon Q tool that does the same thing, with Q's input shape, so the
90
+ * history panel renders it like its own (toolUseToToolChatMessage) and the model sees calls it knows.
91
+ * Calls whose input does not map cleanly keep their own name and input.
92
+ */
93
+ export function asQCall(kind: ToolKind, name: string, input: unknown): { name: string; input: Record<string, unknown> } {
94
+ const obj = input && typeof input === "object" && !Array.isArray(input) ? (input as Record<string, unknown>) : { input: input ?? null }
95
+ if (KIND_OF[name]) return { name, input: obj }
96
+ const path = str(pick(obj, ["file_path", "target_file", "path", "filePath", "absolute_path", "filename"]))
97
+ switch (kind) {
98
+ case "read":
99
+ if (path) return { name: "fsRead", input: { paths: [path] } }
100
+ break
101
+ case "write": {
102
+ const text = pick(obj, ["content", "contents", "file_text", "fileText", "text"])
103
+ if (path && typeof text === "string") return { name: "fsWrite", input: { command: "create", path, fileText: text } }
104
+ break
105
+ }
106
+ case "edit": {
107
+ const oldStr = str(pick(obj, ["old_string", "old_str", "oldText", "old_text"]))
108
+ const newStr = pick(obj, ["new_string", "new_str", "newText", "new_text"])
109
+ if (path && oldStr !== undefined && typeof newStr === "string") return { name: "fsReplace", input: { path, diffs: [{ oldStr, newStr }] } }
110
+ break
111
+ }
112
+ case "list": {
113
+ const dir = str(pick(obj, ["path", "target_directory", "dir_path", "directory", "relative_workspace_path"]))
114
+ if (dir) return { name: "listDirectory", input: { path: dir } }
115
+ break
116
+ }
117
+ case "shell": {
118
+ const command = str(pick(obj, ["command", "cmd", "commandLine"]))
119
+ if (command) return { name: "executeBash", input: { command } }
120
+ break
121
+ }
122
+ case "search": {
123
+ const query = str(pick(obj, ["pattern", "regex", "query"]))
124
+ if (/grep|ripgrep|search_files/i.test(name) && query) return { name: "grepSearch", input: { query, ...(path ? { path } : {}) } }
125
+ break
126
+ }
127
+ default:
128
+ break
129
+ }
130
+ return { name, input: obj }
131
+ }
132
+
133
+ /** The placeholder answer Amazon Q itself stores to keep prompts and answers alternating; never shown. */
134
+ export const DUMMY_ANSWER = "Working..."
135
+
136
+ /** The note the writer puts first; the reader recognises it and records where the session came from. */
137
+ export const continuedNote = (label: string, id: string, when: string) =>
138
+ `[This conversation continues ${label} session ${id}, carried over by brain session on ${when}. The messages after this note are its history; reasoning and anything listed as lost did not carry over. Check files before relying on details, since they may have changed.]`
139
+ export const CONTINUED_NOTE = /^\[This conversation continues .+? session (\S+), carried over by brain session/
140
+
141
+ export const SUMMARY_PREFIX = "[Summary of the earlier conversation"
@@ -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]