@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,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
+ }
@@ -0,0 +1,370 @@
1
+ import { Database } from "bun:sqlite"
2
+ import { randomUUID } from "node:crypto"
3
+ import { copyFileSync, mkdirSync, readFileSync, realpathSync, statSync } from "node:fs"
4
+ import { extname, join, resolve } from "node:path"
5
+ import { toolLabel } from "../../brief.ts"
6
+ import { exists, isDir, isRunning } from "../../fsutil.ts"
7
+ import { withSnapshot } from "../../sqlite.ts"
8
+ import type { Session, Turn } from "../../session/types.ts"
9
+ import { countTurns, unpaired } from "../../session/util.ts"
10
+ import { continuedNote } from "../claude-code/records.ts"
11
+ import type { Readiness, WriteOptions, WriteResult, Writer } from "../types.ts"
12
+ import { readQ } from "./reader.ts"
13
+ import {
14
+ type AssistantToolUse,
15
+ type ConversationState,
16
+ DEFAULT_AGENT,
17
+ DEFAULT_CONTEXT_FILES_SIZE,
18
+ DEFAULT_CONTEXT_PATHS,
19
+ type HistoryEntry,
20
+ LABEL,
21
+ NO_REPLY,
22
+ TESTED_VERSIONS,
23
+ TOOL,
24
+ type ToolUseResult,
25
+ type UserMessage,
26
+ dbPath,
27
+ qCall,
28
+ qVersion,
29
+ } from "./records.ts"
30
+
31
+ // Continues a portable session as the Amazon Q CLI conversation of a folder: a new row of data.sqlite3's
32
+ // `conversations` table, keyed by the folder's real path (what std::env::current_dir gives `q chat`), holding
33
+ // the ConversationState JSON Q itself saves. History entries pair a user message with the reply: a prompt, or
34
+ // the results of the previous reply's tool uses (both, as CancelledToolUses, when the person typed before the
35
+ // model answered the results). Calls are mapped onto Q's built-ins (execute_bash, fs_read, fs_write) when their
36
+ // arguments carry what those need; Q shows the model any other tool's calls under the name "dummy" when it
37
+ // resumes. A compaction is written as /compact leaves it: its summary in latest_summary and only the turns
38
+ // after it in the history. The note saying where the session came from is the first message's
39
+ // additional_context, which Q sends the model with the message and never shows.
40
+ //
41
+ // Q keeps one conversation per folder, so this refuses a folder whose conversation has history rather than
42
+ // replace it, writes only with Q closed, and copies the database to ~/.brain/backups/amazon-q-cli/<time>/ first.
43
+
44
+ const IMAGE_FORMATS: Record<string, string> = { ".png": "Png", ".jpg": "Jpeg", ".jpeg": "Jpeg", ".gif": "Gif", ".webp": "Webp" }
45
+ const MIME_FORMATS: Record<string, string> = { "image/png": "Png", "image/jpeg": "Jpeg", "image/gif": "Gif", "image/webp": "Webp" }
46
+ /** MAX_IMAGE_SIZE in Q's chat consts. */
47
+ const IMAGE_LIMIT = 10 * 1024 * 1024
48
+ const NO_RESULT = "No result was recorded for this call; the session moved before it finished."
49
+
50
+ type Call = Extract<Turn, { kind: "tool_call" }>
51
+ type QImage = { format: string; source: { Bytes: number[] } }
52
+ type UserDraft = { role: "user"; prompt?: string; results: ToolUseResult[]; images: QImage[]; at?: string }
53
+ type ReplyDraft = { role: "assistant"; text: string; uses: AssistantToolUse[] }
54
+
55
+ const OS = process.platform === "darwin" ? "macos" : process.platform === "win32" ? "windows" : process.platform
56
+ /** A time as chrono serializes a DateTime<FixedOffset> at UTC: "Z", and no fraction when it is zero. */
57
+ const qTime = (at?: string) => (at && Number.isFinite(Date.parse(at)) ? new Date(at).toISOString().replace(".000Z", "Z") : null)
58
+
59
+ export interface QPlan {
60
+ state: ConversationState
61
+ lost: string[]
62
+ /** What a read-back must find. */
63
+ expect: { users: number; assistants: number; calls: number }
64
+ }
65
+
66
+ /** The ConversationState for `session` continued in `cwd` (a real path). Pure apart from reading attached images. */
67
+ export function planQ(session: Session, opts: { cwd: string; now: Date }): QPlan {
68
+ const label = toolLabel(session.header.tool)
69
+ const drops = new Map<string, number>()
70
+ const drop = (what: string, n = 1) => drops.set(what, (drops.get(what) ?? 0) + n)
71
+ const lastCompaction = session.turns.map((t) => t.kind).lastIndexOf("compaction")
72
+ const summary = lastCompaction >= 0 ? (session.turns[lastCompaction] as Extract<Turn, { kind: "compaction" }>).summary : undefined
73
+ if (lastCompaction > 0) drop("turns before the last compaction, which Q keeps only as its summary", lastCompaction)
74
+ const turns = session.turns.slice(lastCompaction + 1)
75
+ const answered = new Set(turns.filter((t) => t.kind === "tool_result").map((t) => (t as Extract<Turn, { kind: "tool_result" }>).callId))
76
+
77
+ // Tool use ids: the service takes [A-Za-z0-9_-]; others are mapped once, the same way for call and result.
78
+ const ids = new Map<string, string>()
79
+ const used = new Set<string>()
80
+ let remapped = 0
81
+ const callId = (raw: string) => {
82
+ let v = ids.get(raw)
83
+ if (v) return v
84
+ const base = /^[A-Za-z0-9_-]{1,64}$/.test(raw) ? raw : `tooluse_${raw.replace(/[^A-Za-z0-9]/g, "").slice(-40) || "x"}`
85
+ v = base
86
+ for (let n = 2; used.has(v); n++) v = `${base}_${n}`
87
+ ids.set(raw, v)
88
+ used.add(v)
89
+ if (v !== raw) remapped++
90
+ return v
91
+ }
92
+
93
+ // Regroup the flat turns into alternating user messages and replies.
94
+ const msgs: Array<UserDraft | ReplyDraft> = []
95
+ const last = () => msgs[msgs.length - 1]
96
+ const userMsg = (at?: string): UserDraft => {
97
+ const l = last()
98
+ if (l?.role === "user") return l
99
+ const u: UserDraft = { role: "user", results: [], images: [], ...(at ? { at } : {}) }
100
+ msgs.push(u)
101
+ return u
102
+ }
103
+ const reply = (): ReplyDraft => {
104
+ const l = last()
105
+ if (l?.role === "assistant") return l
106
+ const a: ReplyDraft = { role: "assistant", text: "", uses: [] }
107
+ msgs.push(a)
108
+ return a
109
+ }
110
+ const result = (raw: string, ok: boolean, output: string, at?: string) =>
111
+ userMsg(at).results.push({ tool_use_id: callId(raw), content: [{ Text: output || "(no output)" }], status: ok ? "Success" : "Error" })
112
+ const written = new Set<string>()
113
+ const done = new Set<string>()
114
+ let joined = 0
115
+ let dummies = 0
116
+
117
+ for (const t of turns) {
118
+ switch (t.kind) {
119
+ case "user": {
120
+ let text = t.text
121
+ const u = userMsg(t.at)
122
+ for (const a of t.attachments ?? []) {
123
+ if (!a.path) continue // bytes the reader could not keep; it already counted them
124
+ if (a.kind === "file") {
125
+ text += `\n\n[Attached file: ${a.path}]`
126
+ continue
127
+ }
128
+ const format = (a.mime ? MIME_FORMATS[a.mime] : undefined) ?? IMAGE_FORMATS[extname(a.path).toLowerCase()]
129
+ let size = Number.POSITIVE_INFINITY
130
+ try {
131
+ size = statSync(a.path).size
132
+ } catch {}
133
+ if (format && size <= IMAGE_LIMIT) u.images.push({ format, source: { Bytes: [...readFileSync(a.path)] } })
134
+ else drop("images that could not be attached")
135
+ }
136
+ if (!text.trim()) break
137
+ if (u.prompt !== undefined) joined++
138
+ u.prompt = u.prompt === undefined ? text : `${u.prompt}\n\n${text}`
139
+ break
140
+ }
141
+ case "assistant": {
142
+ if (!t.text.trim()) break
143
+ const a = reply()
144
+ if (a.text) joined++
145
+ a.text = a.text ? `${a.text}\n\n${t.text}` : t.text
146
+ break
147
+ }
148
+ case "reasoning":
149
+ drop("reasoning (Q has no record for it)")
150
+ break
151
+ case "tool_call": {
152
+ if (written.has(t.id)) {
153
+ drop("repeated tool call ids")
154
+ break
155
+ }
156
+ written.add(t.id)
157
+ const mapped = qCall(t as Call)
158
+ const args = mapped?.args ?? (t.input && typeof t.input === "object" && !Array.isArray(t.input) ? t.input : { input: t.input ?? null })
159
+ if (!mapped) dummies++
160
+ const name = mapped?.name ?? t.name
161
+ reply().uses.push({ id: callId(t.id), name, orig_name: name, args, orig_args: args })
162
+ if (!answered.has(t.id)) {
163
+ result(t.id, false, NO_RESULT, t.at)
164
+ done.add(t.id)
165
+ drop("tool calls without a result (closed as errors)")
166
+ }
167
+ break
168
+ }
169
+ case "tool_result": {
170
+ if (!written.has(t.callId)) {
171
+ drop("tool results without a call")
172
+ break
173
+ }
174
+ if (done.has(t.callId)) {
175
+ drop("repeated tool results")
176
+ break
177
+ }
178
+ done.add(t.callId)
179
+ result(t.callId, t.ok, t.fullPath ? `${t.output}\n\n[Full output: ${t.fullPath}]` : t.output, t.at)
180
+ break
181
+ }
182
+ case "compaction":
183
+ break // only the last one is kept, as latest_summary
184
+ case "file_edit":
185
+ if (!t.callId || !written.has(t.callId)) drop("file edits outside a tool call")
186
+ break
187
+ case "subagent":
188
+ drop("subagent transcripts (their results stay in the parent's tool results)")
189
+ break
190
+ }
191
+ }
192
+
193
+ // Pair them into history entries; the first message carries the note.
194
+ const note = continuedNote(label, session.header.id, opts.now.toISOString().slice(0, 10))
195
+ const envContext = { env_state: { operating_system: OS, current_working_directory: opts.cwd, environment_variables: [] } }
196
+ const toUser = (u: UserDraft | undefined, first: boolean): UserMessage => {
197
+ const prompt = u?.prompt ?? (u?.results.length ? undefined : first ? note : "")
198
+ const content: UserMessage["content"] =
199
+ u?.results.length && prompt !== undefined
200
+ ? { CancelledToolUses: { prompt, tool_use_results: u.results } }
201
+ : u?.results.length
202
+ ? { ToolUseResults: { tool_use_results: u.results } }
203
+ : { Prompt: { prompt: prompt ?? "" } }
204
+ return {
205
+ additional_context: first && prompt !== note ? note : "",
206
+ env_context: envContext,
207
+ content,
208
+ timestamp: qTime(u?.at),
209
+ images: u?.images.length ? u.images : null,
210
+ }
211
+ }
212
+ const history: HistoryEntry[] = []
213
+ const transcript: string[] = []
214
+ let pending: UserDraft | undefined
215
+ let first = true
216
+ const entry = (u: UserDraft | undefined, a: ReplyDraft | undefined) => {
217
+ const user = toUser(u, first)
218
+ first = false
219
+ const content = a?.text ?? NO_REPLY
220
+ const assistant: HistoryEntry["assistant"] = a?.uses.length
221
+ ? { ToolUse: { message_id: randomUUID(), content, tool_uses: a.uses } }
222
+ : { Response: { message_id: randomUUID(), content } }
223
+ history.push({ user, assistant, request_metadata: null })
224
+ const c = user.content as Record<string, any>
225
+ const typed = c.Prompt?.prompt ?? c.CancelledToolUses?.prompt
226
+ if (typeof typed === "string" && typed && typed !== note) transcript.push(`> ${typed.replace(/\n/g, "> \n")}`)
227
+ transcript.push(`${content}\n[Tool uses: ${a?.uses.length ? a.uses.map((x) => x.name).join(",") : "none"}]`)
228
+ }
229
+ for (const m of msgs) {
230
+ if (m.role === "user") pending = m
231
+ else {
232
+ entry(pending, m)
233
+ pending = undefined
234
+ }
235
+ }
236
+ if (pending) entry(pending, undefined)
237
+
238
+ const nowMs = opts.now.getTime()
239
+ const state: ConversationState = {
240
+ conversation_id: randomUUID(),
241
+ next_message: null,
242
+ history,
243
+ valid_history_range: [0, history.length],
244
+ transcript,
245
+ tools: {},
246
+ context_manager: { max_context_files_size: DEFAULT_CONTEXT_FILES_SIZE, current_profile: DEFAULT_AGENT, paths: DEFAULT_CONTEXT_PATHS, hooks: {} },
247
+ context_message_length: null,
248
+ latest_summary:
249
+ summary !== undefined
250
+ ? [
251
+ `[Summary of the earlier conversation in ${label}, which replaced the turns before it]\n\n${summary}`,
252
+ {
253
+ request_id: null,
254
+ message_id: randomUUID(),
255
+ request_start_timestamp_ms: nowMs,
256
+ stream_end_timestamp_ms: nowMs,
257
+ time_to_first_chunk: null,
258
+ time_between_chunks: [],
259
+ user_prompt_length: 0,
260
+ response_size: summary.length,
261
+ chat_conversation_type: null,
262
+ tool_use_ids_and_names: [],
263
+ model_id: null,
264
+ message_meta_tags: [],
265
+ },
266
+ ]
267
+ : null,
268
+ file_line_tracker: {},
269
+ checkpoint_manager: null,
270
+ mcp_enabled: true,
271
+ }
272
+ const expect = {
273
+ users: msgs.filter((m) => m.role === "user" && m.prompt !== undefined).length,
274
+ assistants: msgs.filter((m) => m.role === "assistant" && m.text.trim()).length,
275
+ calls: written.size,
276
+ }
277
+ const lost = [...drops].map(([what, n]) => `${what} (${n})`)
278
+ if (joined) lost.push(`consecutive messages joined into one, as Q alternates them (${joined})`)
279
+ if (dummies) lost.push(`calls to tools Q does not have, which Q shows the model as "dummy" (${dummies})`)
280
+ if (remapped) lost.push(`tool call ids, remapped to ids Q accepts (${remapped})`)
281
+ return { state, lost, expect }
282
+ }
283
+
284
+ /** The history length of the conversation already stored for `key`, or undefined when there is none. */
285
+ function storedHistory(d: Database, key: string): number | undefined {
286
+ const r = d.query("SELECT value FROM conversations WHERE key = ?").get(key) as { value: string } | null
287
+ if (!r) return undefined
288
+ try {
289
+ const h = (JSON.parse(r.value) as ConversationState).history
290
+ return Array.isArray(h) ? h.length : 0
291
+ } catch {
292
+ return 1 // unreadable: treat as a conversation, never overwrite it
293
+ }
294
+ }
295
+
296
+ const hasTable = (d: Database) => !!d.query("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'conversations'").get()
297
+ const realCwd = (cwd: string) => {
298
+ try {
299
+ return realpathSync(cwd)
300
+ } catch {
301
+ return resolve(cwd)
302
+ }
303
+ }
304
+ const sh = (s: string) => (/^[\w./@%+=:,-]+$/.test(s) ? s : `'${s.replace(/'/g, `'\\''`)}'`)
305
+ const stamp = (d: Date) => d.toISOString().replace(/[:.]/g, "-")
306
+
307
+ /** The Amazon Q CLI writer; `version` and `running` are for tests, which must not depend on an installed Q. */
308
+ export function amazonQCliWriter(opts: { version?: string; running?: () => boolean } = {}): Writer {
309
+ const running = opts.running ?? (() => isRunning("qchat") || isRunning("q"))
310
+ return {
311
+ tool: TOOL,
312
+ label: LABEL,
313
+ way: "native",
314
+
315
+ async ready(w: WriteOptions): Promise<Readiness> {
316
+ const v = opts.version ?? qVersion()
317
+ if (!v) return { ok: false, why: "Amazon Q CLI is not installed (neither `qchat --version` nor `q --version` ran)" }
318
+ if (!TESTED_VERSIONS.includes(v)) return { ok: false, why: `Amazon Q CLI ${v} is not a version this writer was checked against (${TESTED_VERSIONS.join(", ")})` }
319
+ if (!isDir(w.cwd)) return { ok: false, why: `the folder to continue in does not exist: ${w.cwd}` }
320
+ const db = dbPath(w.home)
321
+ if (!exists(db)) return { ok: false, why: "Amazon Q CLI has no database yet; run `q chat` once first" }
322
+ if (running()) return { ok: false, why: "Amazon Q CLI is running; close it first, since it writes the same database" }
323
+ const key = realCwd(w.cwd)
324
+ const n = withSnapshot(db, (d) => (hasTable(d) ? storedHistory(d, key) : -1))
325
+ if (n === -1) return { ok: false, why: `${db} has no conversations table; it is not a database this writer knows` }
326
+ if (n) return { ok: false, why: `Amazon Q keeps one conversation per folder and ${key} already has one; continue in another folder` }
327
+ return { ok: true }
328
+ },
329
+
330
+ async write(session: Session, w: WriteOptions): Promise<WriteResult> {
331
+ const now = new Date()
332
+ const key = realCwd(w.cwd)
333
+ const plan = planQ(session, { cwd: key, now })
334
+ const db = dbPath(w.home)
335
+ const backup = join(w.home, ".brain", "backups", TOOL, stamp(now))
336
+ const result = { tool: TOOL, way: "native" as const, sessionId: plan.state.conversation_id, written: [db, backup], open: `cd ${sh(key)} && q chat --resume`, lost: plan.lost }
337
+ if (w.dryRun) return { ...result, dryRun: true }
338
+
339
+ if (!exists(db)) throw new Error("Amazon Q CLI has no database yet; run `q chat` once first")
340
+ mkdirSync(backup, { recursive: true })
341
+ for (const side of ["", "-wal", "-shm"]) if (exists(db + side)) copyFileSync(db + side, join(backup, `data.sqlite3${side}`))
342
+
343
+ const d = new Database(db)
344
+ try {
345
+ d.exec("PRAGMA busy_timeout = 5000")
346
+ if (!hasTable(d)) throw new Error(`${db} has no conversations table; it is not a database this writer knows`)
347
+ d.transaction(() => {
348
+ const n = storedHistory(d, key)
349
+ if (n) throw new Error(`Amazon Q keeps one conversation per folder and ${key} already has one; nothing was written`)
350
+ // An empty conversation (what /clear leaves) is replaced, as Q replaces it on the next message.
351
+ d.prepare("INSERT INTO conversations (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value").run(key, JSON.stringify(plan.state))
352
+ // Read back as the reader reads Q; throwing here rolls the transaction back.
353
+ const back = readQ(d, db, key, w.home)
354
+ const c = countTurns(back)
355
+ const pairs = unpaired(back)
356
+ const e = plan.expect
357
+ if (c.user !== e.users || c.assistant !== e.assistants || c.tool_call !== e.calls || pairs.calls.length || pairs.results.length)
358
+ throw new Error(
359
+ `the Amazon Q conversation did not read back as written (users ${c.user}/${e.users}, replies ${c.assistant}/${e.assistants}, tool calls ${c.tool_call}/${e.calls}, unpaired ${pairs.calls.length + pairs.results.length}); nothing was written`,
360
+ )
361
+ })()
362
+ } finally {
363
+ d.close()
364
+ }
365
+ return { ...result, dryRun: false }
366
+ },
367
+ }
368
+ }
369
+
370
+ export const amazonQCliNativeWriter = amazonQCliWriter()
@@ -0,0 +1,9 @@
1
+ import type { Reader, Writer } from "../types.ts"
2
+ import { amazonQReader } from "./reader.ts"
3
+ import { amazonQNativeWriter } from "./writer.ts"
4
+
5
+ // Amazon Q Developer's IDE chat (VS Code-family extension and JetBrains plugin, one shared language
6
+ // server): reads the per-workspace history databases and continues any session as a new conversation in
7
+ // the workspace's database, opened by Amazon Q when the editor next opens the folder.
8
+ export const readers: Reader[] = [amazonQReader]
9
+ export const writers: Writer[] = [amazonQNativeWriter]