@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,269 @@
1
+ import { closeSync, ftruncateSync, mkdirSync, openSync, readFileSync, rmSync, statSync, writeSync } from "node:fs"
2
+ import { join, resolve } from "node:path"
3
+ import { toolLabel } from "../../brief.ts"
4
+ import { exists, isDir } from "../../fsutil.ts"
5
+ import type { Session, Turn } from "../../session/types.ts"
6
+ import type { Readiness, WriteOptions, WriteResult, Writer } from "../types.ts"
7
+ import { runId } from "./reader.ts"
8
+ import {
9
+ HISTORY_FILE,
10
+ INPUT_HISTORY_FILE,
11
+ LABEL,
12
+ SUMMARY_PREFIX,
13
+ TESTED_VERSIONS,
14
+ TOOL,
15
+ aiderVersion,
16
+ assistantText,
17
+ continuedNote,
18
+ escapeLine,
19
+ historyRoot,
20
+ parseRuns,
21
+ pyLines,
22
+ userText,
23
+ } from "./records.ts"
24
+
25
+ // Continues a portable session in Aider as a new run appended to the project's chat history
26
+ // (.aider.chat.history.md at the git root, else the folder), written the way Aider's InputOutput writes
27
+ // one: the "# aider chat started at" line, each request as "#### " lines ending in a Markdown line break,
28
+ // each reply as plain text between blank lines. `aider --restore-chat-history` then gives the model the
29
+ // whole file as messages (utils.split_chat_history_markdown), this run included.
30
+ //
31
+ // Why append, not a fresh file with the old one backed up: Aider treats the file as the project's one
32
+ // history. Every launch appends to it and --restore-chat-history replays all of it, so a continued session
33
+ // is one more launch in that history; moving the existing file aside would hide the person's earlier Aider
34
+ // work from Aider, which is modifying another session. Appending leaves every earlier byte as it was, and a
35
+ // run that does not read back is cut off again at the length the file had before.
36
+ //
37
+ // Aider has no record for tool calls or reasoning. Tool calls and their results travel as text inside the
38
+ // reply they belong to (that is what the model would see in an Aider chat anyway: edits and commands live in
39
+ // the reply's text), reasoning is dropped. Lines the parser would misread ("# ", "#### ", "> " at the start
40
+ // of a reply line) get one leading space, which Markdown ignores. A compaction is written the way Aider's
41
+ // own summariser hands one to the model: a user message starting "I spoke to you previously about a number
42
+ // of things." followed by the turns after it. The prompts also go into .aider.input.history, so the arrow
43
+ // keys recall them as they would in Aider.
44
+
45
+ const shellQuote = (s: string) => (/^[\w./@%+=:,-]+$/.test(s) ? s : `'${s.replace(/'/g, `'\\''`)}'`)
46
+
47
+ const pad = (n: number, w = 2) => String(n).padStart(w, "0")
48
+ /** Local time as Python's datetime.now().strftime("%Y-%m-%d %H:%M:%S"). */
49
+ const localStamp = (d: Date) => `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
50
+ /** Local time as str(datetime.now()), which prompt_toolkit's FileHistory writes before each entry. */
51
+ const pyNow = (d: Date) => `${localStamp(d)}.${pad(d.getMilliseconds(), 3)}000`
52
+
53
+ /** A fence longer than any run of backticks in the text, so the text cannot close it. */
54
+ const fence = (text: string) => "`".repeat(Math.max(3, ...[...text.matchAll(/`+/g)].map((m) => m[0].length + 1)))
55
+ const fenced = (text: string, lang = "") => {
56
+ const f = fence(text)
57
+ return `${f}${lang}\n${text.replace(/\n+$/, "")}\n${f}`
58
+ }
59
+
60
+ /** Text with every line break Aider's parser knows as "\n", so what is written is what reads back. */
61
+ const oneBreak = (text: string) => pyLines(text).join("\n")
62
+
63
+ type Call = Extract<Turn, { kind: "tool_call" }>
64
+ type Result = Extract<Turn, { kind: "tool_result" }>
65
+
66
+ /** A tool call and its result as reply text. */
67
+ function callText(call: Call, result: Result | undefined): string {
68
+ const input = typeof call.input === "string" ? call.input : JSON.stringify(call.input ?? {}, null, 2)
69
+ const parts = [`[Tool call: ${call.name} (${call.toolKind})]`, fenced(input, typeof call.input === "string" ? "" : "json")]
70
+ if (!result) parts.push("[No result was recorded for this call.]")
71
+ else {
72
+ let out = result.output || "(no output)"
73
+ if (result.fullPath) out += `\n\n[Full output: ${result.fullPath}]`
74
+ parts.push(result.ok ? "[Result]" : "[Result: failed]", fenced(out))
75
+ }
76
+ return parts.join("\n")
77
+ }
78
+
79
+ export interface MdPlan {
80
+ /** The run's text, appended to the history file as it stands. */
81
+ history: string
82
+ /** The prompts, in prompt_toolkit FileHistory form, appended to the input history. */
83
+ input: string
84
+ /** The messages Aider's parser must find in the run, for the read-back. */
85
+ messages: Array<{ role: "user" | "assistant"; text: string }>
86
+ lost: string[]
87
+ }
88
+
89
+ /** The run that continues `session` in Aider. Pure. */
90
+ export function planRun(session: Session, now: Date): MdPlan {
91
+ const drops = new Map<string, number>()
92
+ const drop = (what: string, n = 1) => drops.set(what, (drops.get(what) ?? 0) + n)
93
+ const label = toolLabel(session.header.tool)
94
+
95
+ // From the last compaction on: Aider keeps no compaction in its file, it hands the model the summary instead.
96
+ let start = 0
97
+ let summary: string | undefined
98
+ session.turns.forEach((t, i) => {
99
+ if (t.kind === "compaction") {
100
+ start = i + 1
101
+ summary = t.summary
102
+ }
103
+ })
104
+ const before = session.turns.slice(0, start).filter((t) => t.kind !== "compaction").length
105
+ if (before) drop("turns before the last compaction, replaced by its summary", before)
106
+
107
+ const results = new Map<string, Result>()
108
+ for (const t of session.turns) if (t.kind === "tool_result" && !results.has(t.callId)) results.set(t.callId, t)
109
+ const callIds = new Set(session.turns.filter((t) => t.kind === "tool_call").map((t) => (t as Call).id))
110
+
111
+ const messages: MdPlan["messages"] = []
112
+ const prompts: string[] = []
113
+ let reply: string[] = []
114
+ const flush = () => {
115
+ const text = oneBreak(reply.join("\n\n")).trim()
116
+ if (text) messages.push({ role: "assistant", text })
117
+ reply = []
118
+ }
119
+ const user = (text: string, prompt: boolean) => {
120
+ flush()
121
+ messages.push({ role: "user", text: oneBreak(text) })
122
+ if (prompt) prompts.push(oneBreak(text))
123
+ }
124
+
125
+ user(continuedNote(label, session.header.id, now.toISOString().slice(0, 10)), false)
126
+ if (summary) user(`${SUMMARY_PREFIX}${summary}`, false)
127
+ const seen = new Set<string>()
128
+ for (const t of session.turns.slice(start)) {
129
+ switch (t.kind) {
130
+ case "user":
131
+ if (t.attachments?.length) drop("attachments (Aider's history holds text only)", t.attachments.length)
132
+ if (t.text.trim()) user(t.text, true)
133
+ break
134
+ case "assistant":
135
+ if (t.text.trim()) reply.push(t.text.trim())
136
+ break
137
+ case "tool_call":
138
+ if (seen.has(t.id)) break
139
+ seen.add(t.id)
140
+ reply.push(callText(t, results.get(t.id)))
141
+ drop("tool calls, kept as text in the replies (Aider has no tool-call record)")
142
+ if (!results.has(t.id)) drop("tool calls without a result")
143
+ break
144
+ case "tool_result":
145
+ if (!callIds.has(t.callId)) drop("tool results without a call")
146
+ break
147
+ case "file_edit":
148
+ if (t.callId && callIds.has(t.callId)) break
149
+ reply.push([`[Edited ${t.path}]`, ...(t.patch ? [fenced(t.patch, "diff")] : [])].join("\n"))
150
+ drop("file edits outside a tool call, kept as text")
151
+ break
152
+ case "reasoning":
153
+ drop("reasoning (Aider's history has no place for it)")
154
+ break
155
+ case "subagent":
156
+ drop("subagent sessions")
157
+ break
158
+ case "compaction":
159
+ break
160
+ }
161
+ }
162
+ flush()
163
+
164
+ // As InputOutput writes them: user_input() and ai_output() through append_chat_history().
165
+ const lines = [`\n# aider chat started at ${localStamp(now)}\n\n`]
166
+ for (const m of messages) {
167
+ if (m.role === "user") {
168
+ const body = pyLines(m.text).join(" \n#### ").replace(/\s+$/, "")
169
+ lines.push(`\n#### ${body} \n`)
170
+ } else {
171
+ lines.push(`\n${pyLines(m.text).map(escapeLine).join("\n")}\n\n`)
172
+ }
173
+ }
174
+ const input = prompts.map((p) => `\n# ${pyNow(now)}\n${p.split("\n").map((l) => `+${l}\n`).join("")}`).join("")
175
+ return { history: lines.join(""), input, messages, lost: [...drops].map(([what, n]) => `${what} (${n})`) }
176
+ }
177
+
178
+ /** Appends `text` to `path` and returns how long the file was before, so a failed run can be cut off again. */
179
+ function append(path: string, text: string): number {
180
+ const size = exists(path) ? statSync(path).size : -1
181
+ const fd = openSync(path, "a")
182
+ try {
183
+ writeSync(fd, text)
184
+ } finally {
185
+ closeSync(fd)
186
+ }
187
+ return size
188
+ }
189
+
190
+ /** Puts a file back to the length it had (or removes it when it did not exist). */
191
+ function restore(path: string, size: number) {
192
+ if (size < 0) return rmSync(path, { force: true })
193
+ const fd = openSync(path, "r+")
194
+ try {
195
+ ftruncateSync(fd, size)
196
+ } finally {
197
+ closeSync(fd)
198
+ }
199
+ }
200
+
201
+ /**
202
+ * Whether an Aider is running: its command line runs the `aider` script (a pip install runs it through Python,
203
+ * so the process name is not "aider"). A running Aider appends its own lines to the history it has open, and
204
+ * lines it wrote after the continued run would read as part of it.
205
+ */
206
+ function aiderRunning(): boolean {
207
+ try {
208
+ return Bun.spawnSync(["pgrep", "-f", "(^|/)aider( |$)"], { stdout: "pipe", stderr: "pipe" }).exitCode === 0
209
+ } catch {
210
+ return false
211
+ }
212
+ }
213
+
214
+ /** The Aider writer; `version` and `running` are for tests, which must not depend on an installed Aider. */
215
+ export function aiderWriter(opts: { version?: string; running?: () => boolean } = {}): Writer {
216
+ const installed = () => opts.version ?? aiderVersion()
217
+ const running = opts.running ?? aiderRunning
218
+ return {
219
+ tool: TOOL,
220
+ label: LABEL,
221
+ way: "native",
222
+
223
+ async ready(w: WriteOptions): Promise<Readiness> {
224
+ const v = installed()
225
+ if (!v) return { ok: false, why: "Aider is not installed (`aider --version` did not run)" }
226
+ if (!TESTED_VERSIONS.includes(v)) return { ok: false, why: `Aider ${v} is not a version this writer was checked against (${TESTED_VERSIONS.join(", ")})` }
227
+ if (!isDir(w.cwd)) return { ok: false, why: `${w.cwd} is not a folder; Aider keeps its history in the project` }
228
+ if (running()) return { ok: false, why: "Aider is running; quit it first, or its next lines would land inside the continued session's history" }
229
+ return { ok: true }
230
+ },
231
+
232
+ async write(session: Session, w: WriteOptions): Promise<WriteResult> {
233
+ const cwd = resolve(w.cwd)
234
+ const root = historyRoot(cwd)
235
+ const historyPath = join(root, HISTORY_FILE)
236
+ const inputPath = join(root, INPUT_HISTORY_FILE)
237
+ const plan = planRun(session, new Date())
238
+ let sessionId: string | undefined
239
+ if (!w.dryRun) {
240
+ mkdirSync(root, { recursive: true })
241
+ const historySize = append(historyPath, plan.history)
242
+ // Read back as Aider will: the last run must hold exactly the messages planned.
243
+ const runs = parseRuns(readFileSync(historyPath, "utf8"))
244
+ const got = (runs[runs.length - 1]?.messages ?? []).filter((m) => m.role !== "tool")
245
+ const same =
246
+ got.length === plan.messages.length &&
247
+ got.every((m, i) => m.role === plan.messages[i]!.role && (m.role === "user" ? userText(m.content) : assistantText(m.content)).trim() === plan.messages[i]!.text.trim())
248
+ if (!same) {
249
+ restore(historyPath, historySize)
250
+ throw new Error(`the Aider history did not read back as written (${got.length} of ${plan.messages.length} messages); the file was put back as it was`)
251
+ }
252
+ if (plan.input) append(inputPath, plan.input)
253
+ const run = runs[runs.length - 1]!
254
+ sessionId = runId(historyPath, run, runs)
255
+ }
256
+ return {
257
+ tool: TOOL,
258
+ way: "native",
259
+ ...(sessionId ? { sessionId } : {}),
260
+ written: plan.input ? [historyPath, inputPath] : [historyPath],
261
+ open: `cd ${shellQuote(cwd)} && aider --restore-chat-history`,
262
+ lost: plan.lost,
263
+ dryRun: !!w.dryRun,
264
+ }
265
+ },
266
+ }
267
+ }
268
+
269
+ export const aiderNativeWriter = aiderWriter()
@@ -0,0 +1,8 @@
1
+ import type { Reader, Writer } from "../types.ts"
2
+ import { amazonQCliReader } from "./reader.ts"
3
+ import { amazonQCliNativeWriter } from "./writer.ts"
4
+
5
+ // Amazon Q Developer CLI (`q chat`, superseded by Kiro CLI): reads the per-folder conversations in
6
+ // data.sqlite3 and continues any session as the folder's conversation, which `q chat --resume` restores.
7
+ export const readers: Reader[] = [amazonQCliReader]
8
+ export const writers: Writer[] = [amazonQCliNativeWriter]
@@ -0,0 +1,84 @@
1
+ import type { Database } from "bun:sqlite"
2
+ import { exists, gitInfo } from "../../fsutil.ts"
3
+ import { withSnapshot } from "../../sqlite.ts"
4
+ import type { Session } from "../../session/types.ts"
5
+ import { header, iso } from "../../session/util.ts"
6
+ import type { Reader, SessionRef } from "../types.ts"
7
+ import { type ConversationState, LABEL, TOOL, dbPath, turnsOf } from "./records.ts"
8
+
9
+ // Amazon Q CLI: one conversation per folder in data.sqlite3's `conversations` table, read from a copy with its
10
+ // write-ahead log. Only conversations with history are listed: those are the ones `q chat --resume` restores.
11
+
12
+ const firstPrompt = (s: ConversationState) => {
13
+ for (const e of s.history ?? []) {
14
+ const c = e.user?.content as Record<string, any> | undefined
15
+ const p = c?.Prompt?.prompt ?? c?.CancelledToolUses?.prompt
16
+ if (typeof p === "string" && p.trim()) return p.replace(/\s+/g, " ").trim().slice(0, 80)
17
+ }
18
+ return undefined
19
+ }
20
+
21
+ const lastTime = (s: ConversationState) => {
22
+ for (let i = (s.history?.length ?? 0) - 1; i >= 0; i--) {
23
+ const t = iso(s.history[i]!.user?.timestamp)
24
+ if (t) return t
25
+ }
26
+ return undefined
27
+ }
28
+
29
+ function rows(d: Database): Array<{ key: string; state: ConversationState }> {
30
+ const out: Array<{ key: string; state: ConversationState }> = []
31
+ for (const r of d.query("SELECT key, value FROM conversations").all() as Array<{ key: string; value: string }>) {
32
+ try {
33
+ const state = JSON.parse(r.value) as ConversationState
34
+ if (Array.isArray(state.history) && state.history.length) out.push({ key: r.key, state })
35
+ } catch {}
36
+ }
37
+ return out
38
+ }
39
+
40
+ /** The conversation stored for `key`, as a portable session. */
41
+ export function readQ(d: Database, path: string, key: string, home: string): Session {
42
+ const r = d.query("SELECT value FROM conversations WHERE key = ?").get(key) as { value: string } | null
43
+ if (!r) throw new Error(`no Amazon Q conversation for ${key} in ${path}`)
44
+ const state = JSON.parse(r.value) as ConversationState
45
+ const title = firstPrompt(state)
46
+ const h = header(TOOL, String(state.conversation_id), {
47
+ sourcePath: path,
48
+ ...(title ? { title } : {}),
49
+ project: { cwd: key, ...gitInfo(key, home) },
50
+ })
51
+ const { turns, continued } = turnsOf(state, h)
52
+ if (continued) h.parent = { id: continued, kind: "continued" }
53
+ const times = turns.map((t) => t.at).filter((t): t is string => !!t)
54
+ if (times[0]) h.startedAt = times[0]
55
+ const last = lastTime(state)
56
+ if (last) h.updatedAt = last
57
+ return { header: h, turns }
58
+ }
59
+
60
+ export const amazonQCliReader: Reader = {
61
+ tool: TOOL,
62
+ label: LABEL,
63
+ async list(home) {
64
+ const db = dbPath(home)
65
+ if (!exists(db)) return []
66
+ try {
67
+ return withSnapshot(db, (d) =>
68
+ rows(d).map(({ key, state }): SessionRef => {
69
+ const title = firstPrompt(state)
70
+ const updatedAt = lastTime(state)
71
+ return { tool: TOOL, sourceId: String(state.conversation_id), path: db, cwd: key, ...(title ? { title } : {}), ...(updatedAt ? { updatedAt } : {}), extra: { key } }
72
+ }),
73
+ ).sort((a, b) => (b.updatedAt ?? "").localeCompare(a.updatedAt ?? ""))
74
+ } catch {
75
+ return []
76
+ }
77
+ },
78
+ async read(ref, home) {
79
+ if (!exists(ref.path)) throw new Error(`Amazon Q database not found: ${ref.path}`)
80
+ const key = typeof ref.extra?.key === "string" ? ref.extra.key : ref.cwd
81
+ if (!key) throw new Error("this Amazon Q session reference names no folder")
82
+ return withSnapshot(ref.path, (d) => readQ(d, ref.path, key, home))
83
+ },
84
+ }
@@ -0,0 +1,191 @@
1
+ import { join } from "node:path"
2
+ import { appSupport, versionOf } from "../../fsutil.ts"
3
+ import type { Session, ToolKind, Turn } from "../../session/types.ts"
4
+ import { asText, clip, iso, lossCounter, noteModel, toolKindOf } from "../../session/util.ts"
5
+ import { CONTINUED_NOTE } from "../claude-code/records.ts"
6
+
7
+ // What the Amazon Q Developer CLI keeps, shared by the reader and the writer. Checked against the source of
8
+ // aws/amazon-q-developer-cli at 1.19.7 (crates/chat-cli: database/mod.rs and sqlite_migrations/,
9
+ // cli/chat/conversation.rs, message.rs, parser.rs, context.rs, tools/, and the resume path in cli/chat/mod.rs).
10
+ // Catalogue: docs/sessions/2026-11-08-session-formats.md, "Amazon Q Developer CLI".
11
+ //
12
+ // Q keeps one conversation per folder: a row of the `conversations` table in
13
+ // ~/Library/Application Support/amazon-q/data.sqlite3 whose key is the folder's absolute path and whose value
14
+ // is the ConversationState as serde_json writes it. `q chat --resume` loads the row for the folder it runs
15
+ // in, and only when its history is not empty. The history is a list of entries, each one user message and
16
+ // the assistant's reply to it; tool calls are the reply's tool_uses, answered by the next entry's user
17
+ // message, which carries the results instead of a prompt.
18
+
19
+ export const TOOL = "amazon-q-cli"
20
+ export const LABEL = "Amazon Q CLI"
21
+
22
+ /** Q CLI versions whose conversation JSON the writer was checked against (from source). */
23
+ export const TESTED_VERSIONS = ["1.19.7"]
24
+
25
+ export const dbPath = (home: string) => join(appSupport(home), "amazon-q", "data.sqlite3")
26
+
27
+ /** "1.19.7" from `qchat --version` ("qchat 1.19.7") or `q --version`; undefined when neither runs. */
28
+ export const qVersion = (bins = ["qchat", "q"]) => {
29
+ for (const b of bins) {
30
+ const v = versionOf(b)?.match(/\d+\.\d+\.\d+/)?.[0]
31
+ if (v) return v
32
+ }
33
+ return undefined
34
+ }
35
+
36
+ /** The default agent, whose context manager a new conversation gets (Agent::default, ContextManager::from_agent). */
37
+ export const DEFAULT_AGENT = "q_cli_default"
38
+ export const DEFAULT_CONTEXT_PATHS = ["AmazonQ.md", "AGENTS.md", "README.md", ".amazonq/rules/**/*.md"]
39
+ /** calc_max_context_files_size with no model chosen: 75% of the default 200,000-token window. */
40
+ export const DEFAULT_CONTEXT_FILES_SIZE = 150_000
41
+
42
+ /** Q's reply placeholder for a user message that has none: every history entry needs one. */
43
+ export const NO_REPLY = "[No reply was recorded for this message before the session moved to Amazon Q.]"
44
+
45
+ export type ToolUseResultBlock = { Text: string } | { Json: unknown }
46
+ export interface ToolUseResult {
47
+ tool_use_id: string
48
+ content: ToolUseResultBlock[]
49
+ status: "Success" | "Error"
50
+ }
51
+ export type UserContent =
52
+ | { Prompt: { prompt: string } }
53
+ | { CancelledToolUses: { prompt: string | null; tool_use_results: ToolUseResult[] } }
54
+ | { ToolUseResults: { tool_use_results: ToolUseResult[] } }
55
+ export interface UserMessage {
56
+ additional_context: string
57
+ env_context: { env_state: { operating_system: string | null; current_working_directory: string | null; environment_variables: unknown[] } | null }
58
+ content: UserContent
59
+ timestamp: string | null
60
+ images: Array<{ format: string; source: { Bytes: number[] } }> | null
61
+ }
62
+ export interface AssistantToolUse {
63
+ id: string
64
+ name: string
65
+ orig_name: string
66
+ args: unknown
67
+ orig_args: unknown
68
+ }
69
+ export type AssistantMessage = { Response: { message_id: string | null; content: string } } | { ToolUse: { message_id: string | null; content: string; tool_uses: AssistantToolUse[] } }
70
+ export interface HistoryEntry {
71
+ user: UserMessage
72
+ assistant: AssistantMessage
73
+ request_metadata: unknown
74
+ }
75
+ /** ConversationState as serialized; fields this adapter does not use are left loose. */
76
+ export interface ConversationState {
77
+ conversation_id: string
78
+ next_message: UserMessage | null
79
+ history: HistoryEntry[]
80
+ valid_history_range: [number, number]
81
+ transcript: string[]
82
+ tools: Record<string, unknown>
83
+ context_manager: unknown
84
+ context_message_length: number | null
85
+ latest_summary: [string, unknown] | null
86
+ model?: string | null
87
+ model_info?: { model_id?: string } | null
88
+ file_line_tracker?: Record<string, unknown>
89
+ checkpoint_manager: unknown
90
+ mcp_enabled?: boolean
91
+ [k: string]: unknown
92
+ }
93
+
94
+ /** What a Q tool does. Its built-ins are fs_read (with operations), fs_write (with a command), execute_bash. */
95
+ export function qToolKind(name: string, args: unknown): ToolKind {
96
+ const a = (args && typeof args === "object" ? args : {}) as Record<string, any>
97
+ if (name === "fs_read") {
98
+ const mode = Array.isArray(a.operations) ? a.operations[0]?.mode : a.mode
99
+ return mode === "Directory" ? "list" : mode === "Search" ? "search" : "read"
100
+ }
101
+ if (name === "fs_write") return a.command === "create" ? "write" : "edit"
102
+ if (name === "execute_bash" || name === "execute_cmd") return "shell"
103
+ if (name === "use_aws") return "shell"
104
+ if (name === "todo_list") return "todo"
105
+ if (name === "delegate") return "task"
106
+ return toolKindOf(name)
107
+ }
108
+
109
+ const str = (v: unknown) => (typeof v === "string" ? v : undefined)
110
+
111
+ /** A call as Q's own built-in tool would have been called, when its arguments say what that tool needs. */
112
+ export function qCall(c: Extract<Turn, { kind: "tool_call" }>): { name: string; args: Record<string, unknown> } | undefined {
113
+ const i = (c.input && typeof c.input === "object" && !Array.isArray(c.input) ? c.input : {}) as Record<string, unknown>
114
+ const path = str(i.file_path) ?? str(i.path) ?? str(i.filePath) ?? str(i.target_file) ?? str(i.absolute_path)
115
+ switch (c.toolKind) {
116
+ case "shell": {
117
+ const command = str(i.command) ?? str(i.cmd) ?? (Array.isArray(i.command) ? (i.command as unknown[]).map(String).join(" ") : undefined)
118
+ return command ? { name: "execute_bash", args: { command } } : undefined
119
+ }
120
+ case "write": {
121
+ const text = str(i.content) ?? str(i.file_text) ?? str(i.contents) ?? str(i.text)
122
+ return path && text !== undefined ? { name: "fs_write", args: { command: "create", path, file_text: text } } : undefined
123
+ }
124
+ case "edit": {
125
+ const before = str(i.old_string) ?? str(i.old_str) ?? str(i.oldText) ?? str(i.oldStr)
126
+ const after = str(i.new_string) ?? str(i.new_str) ?? str(i.newText) ?? str(i.newStr)
127
+ return path && before !== undefined && after !== undefined ? { name: "fs_write", args: { command: "str_replace", path, old_str: before, new_str: after } } : undefined
128
+ }
129
+ case "read":
130
+ return path ? { name: "fs_read", args: { operations: [{ mode: "Line", path }] } } : undefined
131
+ case "list":
132
+ return path ? { name: "fs_read", args: { operations: [{ mode: "Directory", path }] } } : undefined
133
+ }
134
+ return undefined
135
+ }
136
+
137
+ const blockText = (b: ToolUseResultBlock) => ("Text" in b ? b.Text : asText(b.Json))
138
+
139
+ /**
140
+ * The portable turns of a Q conversation: the summary of a compaction first (Q keeps only the summary of the
141
+ * turns it replaced), then the history. Returns the id of the session a brain-written conversation continues,
142
+ * when its note is there.
143
+ */
144
+ export function turnsOf(state: ConversationState, h: Session["header"]): { turns: Turn[]; continued?: string } {
145
+ const turns: Turn[] = []
146
+ const lost = lossCounter()
147
+ let continued: string | undefined
148
+ const model = state.model_info?.model_id ?? state.model ?? undefined
149
+ noteModel(h, model ?? undefined)
150
+ if (Array.isArray(state.latest_summary) && typeof state.latest_summary[0] === "string" && state.latest_summary[0].trim())
151
+ turns.push({ kind: "compaction", summary: state.latest_summary[0] })
152
+ const results = (rs: ToolUseResult[], at?: string) => {
153
+ for (const r of rs) {
154
+ const c = clip((r.content ?? []).map(blockText).join("\n"))
155
+ turns.push({ kind: "tool_result", ...(at ? { at } : {}), callId: String(r.tool_use_id), ok: r.status !== "Error", output: c.text, ...(c.truncated ? { truncated: true } : {}) })
156
+ }
157
+ }
158
+ const prompt = (text: string | null | undefined, at?: string) => {
159
+ if (!text?.trim()) return
160
+ const note = text.match(CONTINUED_NOTE)
161
+ if (note) continued = note[1]
162
+ else turns.push({ kind: "user", ...(at ? { at } : {}), text })
163
+ }
164
+ for (const e of state.history ?? []) {
165
+ const u = e.user
166
+ const at = iso(u?.timestamp)
167
+ const note = u?.additional_context?.match(CONTINUED_NOTE)
168
+ if (note) continued = note[1]
169
+ if (u?.images?.length) lost.add("images", u.images.length)
170
+ const content = u?.content ?? ({} as UserContent)
171
+ if ("Prompt" in content) prompt(content.Prompt.prompt, at)
172
+ else if ("ToolUseResults" in content) results(content.ToolUseResults.tool_use_results ?? [], at)
173
+ else if ("CancelledToolUses" in content) {
174
+ results(content.CancelledToolUses.tool_use_results ?? [], at)
175
+ prompt(content.CancelledToolUses.prompt, at)
176
+ }
177
+ const a = e.assistant ?? ({} as AssistantMessage)
178
+ const reply = "Response" in a ? a.Response : "ToolUse" in a ? a.ToolUse : undefined
179
+ if (reply?.content?.trim() && reply.content !== NO_REPLY) turns.push({ kind: "assistant", text: reply.content, ...(model ? { model } : {}) })
180
+ if ("ToolUse" in a)
181
+ for (const t of a.ToolUse.tool_uses ?? []) {
182
+ // Q renames a call to a tool it no longer has to "dummy" when it resumes; orig_name is what was called.
183
+ const name = t.orig_name || t.name
184
+ const input = t.orig_args ?? t.args ?? {}
185
+ turns.push({ kind: "tool_call", id: String(t.id), name, toolKind: qToolKind(name, input), input })
186
+ }
187
+ }
188
+ if (state.next_message) lost.add("an unsent message")
189
+ lost.flush(h)
190
+ return { turns, ...(continued ? { continued } : {}) }
191
+ }