@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.
- package/LICENSE +48 -0
- package/README.md +235 -2
- package/package.json +48 -4
- package/src/adapters/aider/index.ts +8 -0
- package/src/adapters/aider/reader.ts +129 -0
- package/src/adapters/aider/records.ts +220 -0
- package/src/adapters/aider/writer.ts +269 -0
- package/src/adapters/amazon-q-cli/index.ts +8 -0
- package/src/adapters/amazon-q-cli/reader.ts +84 -0
- package/src/adapters/amazon-q-cli/records.ts +191 -0
- package/src/adapters/amazon-q-cli/writer.ts +370 -0
- package/src/adapters/amazon-q-ide/index.ts +9 -0
- package/src/adapters/amazon-q-ide/reader.ts +131 -0
- package/src/adapters/amazon-q-ide/records.ts +141 -0
- package/src/adapters/amazon-q-ide/writer.ts +325 -0
- package/src/adapters/antigravity/index.ts +9 -0
- package/src/adapters/antigravity/proto.ts +194 -0
- package/src/adapters/antigravity/reader.ts +424 -0
- package/src/adapters/antigravity/records.ts +236 -0
- package/src/adapters/antigravity/writer.ts +463 -0
- package/src/adapters/auggie/index.ts +36 -0
- package/src/adapters/claude-code/index.ts +8 -0
- package/src/adapters/claude-code/reader.ts +639 -0
- package/src/adapters/claude-code/records.ts +88 -0
- package/src/adapters/claude-code/writer.ts +329 -0
- package/src/adapters/cline/index.ts +12 -0
- package/src/adapters/cline/reader.ts +432 -0
- package/src/adapters/cline/records.ts +151 -0
- package/src/adapters/cline/vscdb.ts +56 -0
- package/src/adapters/cline/writer.ts +656 -0
- package/src/adapters/cline/xml.ts +177 -0
- package/src/adapters/cline4/index.ts +9 -0
- package/src/adapters/cline4/reader.ts +179 -0
- package/src/adapters/cline4/records.ts +92 -0
- package/src/adapters/cline4/writer.ts +361 -0
- package/src/adapters/codebuff/index.ts +8 -0
- package/src/adapters/codebuff/project.ts +207 -0
- package/src/adapters/codebuff/reader.ts +215 -0
- package/src/adapters/codebuff/records.ts +173 -0
- package/src/adapters/codebuff/writer.ts +383 -0
- package/src/adapters/codex/index.ts +8 -0
- package/src/adapters/codex/native.ts +239 -0
- package/src/adapters/codex/reader.ts +747 -0
- package/src/adapters/codex/records.ts +114 -0
- package/src/adapters/codex/writer.ts +479 -0
- package/src/adapters/continue/index.ts +9 -0
- package/src/adapters/continue/reader.ts +68 -0
- package/src/adapters/continue/records.ts +418 -0
- package/src/adapters/continue/writer.ts +162 -0
- package/src/adapters/copilot-cli/index.ts +9 -0
- package/src/adapters/copilot-cli/reader.ts +186 -0
- package/src/adapters/copilot-cli/records.ts +241 -0
- package/src/adapters/copilot-cli/writer.ts +353 -0
- package/src/adapters/crush/index.ts +8 -0
- package/src/adapters/crush/reader.ts +180 -0
- package/src/adapters/crush/records.ts +169 -0
- package/src/adapters/crush/writer.ts +333 -0
- package/src/adapters/cursor-cli/index.ts +405 -0
- package/src/adapters/cursor-ide/agentkv.ts +95 -0
- package/src/adapters/cursor-ide/index.ts +501 -0
- package/src/adapters/cursor-ide/transcripts.ts +122 -0
- package/src/adapters/cursor-ide/workspaces.ts +119 -0
- package/src/adapters/cursor-ide/writer.ts +170 -0
- package/src/adapters/devin/index.ts +8 -0
- package/src/adapters/devin/reader.ts +305 -0
- package/src/adapters/devin/records.ts +277 -0
- package/src/adapters/devin/writer.ts +523 -0
- package/src/adapters/factory-droid/index.ts +12 -0
- package/src/adapters/factory-droid/reader.ts +184 -0
- package/src/adapters/factory-droid/records.ts +79 -0
- package/src/adapters/factory-droid/writer.ts +350 -0
- package/src/adapters/gemini-cli/exchanges.ts +228 -0
- package/src/adapters/gemini-cli/index.ts +396 -0
- package/src/adapters/gemini-cli/records.ts +521 -0
- package/src/adapters/gemini-cli/writer.ts +354 -0
- package/src/adapters/goose/index.ts +8 -0
- package/src/adapters/goose/reader.ts +138 -0
- package/src/adapters/goose/records.ts +199 -0
- package/src/adapters/goose/writer.ts +340 -0
- package/src/adapters/grok-cli/image.ts +44 -0
- package/src/adapters/grok-cli/index.ts +8 -0
- package/src/adapters/grok-cli/reader.ts +302 -0
- package/src/adapters/grok-cli/records.ts +172 -0
- package/src/adapters/grok-cli/writer.ts +329 -0
- package/src/adapters/hermes/index.ts +8 -0
- package/src/adapters/hermes/reader.ts +249 -0
- package/src/adapters/hermes/records.ts +285 -0
- package/src/adapters/hermes/writer.ts +452 -0
- package/src/adapters/iflow-cli/index.ts +72 -0
- package/src/adapters/iflow-cli/records.ts +215 -0
- package/src/adapters/iflow-cli/writer.ts +204 -0
- package/src/adapters/index.ts +112 -0
- package/src/adapters/kilo-cli/index.ts +21 -0
- package/src/adapters/kilo-code/index.ts +11 -0
- package/src/adapters/kilo-code/writer.ts +92 -0
- package/src/adapters/kimi/index.ts +9 -0
- package/src/adapters/kimi/reader.ts +92 -0
- package/src/adapters/kimi/records.ts +221 -0
- package/src/adapters/kimi/writer.ts +367 -0
- package/src/adapters/kiro-cli/index.ts +8 -0
- package/src/adapters/kiro-cli/reader.ts +87 -0
- package/src/adapters/kiro-cli/records.ts +260 -0
- package/src/adapters/kiro-cli/writer.ts +305 -0
- package/src/adapters/kiro-ide/index.ts +8 -0
- package/src/adapters/kiro-ide/writer.ts +52 -0
- package/src/adapters/letta/index.ts +8 -0
- package/src/adapters/letta/reader.ts +177 -0
- package/src/adapters/letta/records.ts +119 -0
- package/src/adapters/letta/writer.ts +413 -0
- package/src/adapters/mastracode/index.ts +8 -0
- package/src/adapters/mastracode/reader.ts +323 -0
- package/src/adapters/mastracode/records.ts +336 -0
- package/src/adapters/mastracode/writer.ts +385 -0
- package/src/adapters/mistral-vibe/index.ts +9 -0
- package/src/adapters/mistral-vibe/reader.ts +62 -0
- package/src/adapters/mistral-vibe/records.ts +104 -0
- package/src/adapters/mistral-vibe/writer.ts +359 -0
- package/src/adapters/muse/index.ts +10 -0
- package/src/adapters/muse/reader.ts +332 -0
- package/src/adapters/muse/records.ts +104 -0
- package/src/adapters/muse/writer.ts +387 -0
- package/src/adapters/omp/index.ts +13 -0
- package/src/adapters/omp/reader.ts +314 -0
- package/src/adapters/omp/records.ts +153 -0
- package/src/adapters/omp/writer.ts +216 -0
- package/src/adapters/opencode/ids.ts +52 -0
- package/src/adapters/opencode/index.ts +31 -0
- package/src/adapters/opencode/note.ts +8 -0
- package/src/adapters/opencode/reader.ts +452 -0
- package/src/adapters/opencode/tools.ts +174 -0
- package/src/adapters/opencode/v2.ts +367 -0
- package/src/adapters/opencode/writer.ts +379 -0
- package/src/adapters/openhands/index.ts +8 -0
- package/src/adapters/openhands/reader.ts +222 -0
- package/src/adapters/openhands/records.ts +136 -0
- package/src/adapters/openhands/writer.ts +404 -0
- package/src/adapters/pearai/index.ts +10 -0
- package/src/adapters/pi/index.ts +28 -0
- package/src/adapters/pi/reader.ts +261 -0
- package/src/adapters/pi/records.ts +99 -0
- package/src/adapters/pi/writer.ts +362 -0
- package/src/adapters/qoder-cli/index.ts +8 -0
- package/src/adapters/qoder-cli/reader.ts +83 -0
- package/src/adapters/qoder-cli/records.ts +45 -0
- package/src/adapters/qoder-cli/writer.ts +72 -0
- package/src/adapters/qwen-code/index.ts +84 -0
- package/src/adapters/qwen-code/records.ts +194 -0
- package/src/adapters/qwen-code/writer.ts +224 -0
- package/src/adapters/tabnine-cli/index.ts +8 -0
- package/src/adapters/tabnine-cli/reader.ts +106 -0
- package/src/adapters/tabnine-cli/records.ts +33 -0
- package/src/adapters/tabnine-cli/writer.ts +156 -0
- package/src/adapters/types.ts +73 -0
- package/src/adapters/void/index.ts +9 -0
- package/src/adapters/void/reader.ts +48 -0
- package/src/adapters/void/records.ts +285 -0
- package/src/adapters/void/writer.ts +93 -0
- package/src/adapters/vscode-copilot/index.ts +9 -0
- package/src/adapters/vscode-copilot/reader.ts +212 -0
- package/src/adapters/vscode-copilot/records.ts +388 -0
- package/src/adapters/vscode-copilot/shared.ts +68 -0
- package/src/adapters/vscode-copilot/writer.ts +546 -0
- package/src/adapters/zed/index.ts +8 -0
- package/src/adapters/zed/reader.ts +198 -0
- package/src/adapters/zed/records.ts +87 -0
- package/src/adapters/zed/writer.ts +341 -0
- package/src/brief-writers.ts +71 -0
- package/src/brief.ts +238 -0
- package/src/cli.ts +351 -0
- package/src/dashboard/cli.ts +148 -0
- package/src/dashboard/conversions.ts +170 -0
- package/src/dashboard/report.ts +261 -0
- package/src/dashboard/send.ts +213 -0
- package/src/dashboard/state.ts +95 -0
- package/src/fsutil.ts +133 -0
- package/src/gateway/common.ts +232 -0
- package/src/gateway/connect.ts +847 -0
- package/src/gateway/connectors/aider.ts +356 -0
- package/src/gateway/connectors/amazon-q-cli.ts +6 -0
- package/src/gateway/connectors/amazon-q-ide.ts +6 -0
- package/src/gateway/connectors/antigravity.ts +6 -0
- package/src/gateway/connectors/auggie.ts +6 -0
- package/src/gateway/connectors/cline-sdk.ts +85 -0
- package/src/gateway/connectors/cline.ts +34 -0
- package/src/gateway/connectors/codebuff.ts +6 -0
- package/src/gateway/connectors/continue.ts +579 -0
- package/src/gateway/connectors/copilot-cli.ts +138 -0
- package/src/gateway/connectors/crush.ts +313 -0
- package/src/gateway/connectors/cursor-cli.ts +6 -0
- package/src/gateway/connectors/cursor.ts +6 -0
- package/src/gateway/connectors/devin.ts +6 -0
- package/src/gateway/connectors/factory-droid.ts +308 -0
- package/src/gateway/connectors/goose.ts +79 -0
- package/src/gateway/connectors/grok-cli.ts +82 -0
- package/src/gateway/connectors/hermes.ts +230 -0
- package/src/gateway/connectors/iflow-cli.ts +50 -0
- package/src/gateway/connectors/index.ts +86 -0
- package/src/gateway/connectors/kilo-cli.ts +434 -0
- package/src/gateway/connectors/kilo-code.ts +32 -0
- package/src/gateway/connectors/kimi-code.ts +241 -0
- package/src/gateway/connectors/kiro-cli.ts +6 -0
- package/src/gateway/connectors/kiro-ide.ts +6 -0
- package/src/gateway/connectors/letta-code.ts +170 -0
- package/src/gateway/connectors/mastracode.ts +91 -0
- package/src/gateway/connectors/mistral-vibe.ts +495 -0
- package/src/gateway/connectors/muse.ts +27 -0
- package/src/gateway/connectors/omp.ts +279 -0
- package/src/gateway/connectors/openhands.ts +296 -0
- package/src/gateway/connectors/pearai.ts +52 -0
- package/src/gateway/connectors/pi.ts +357 -0
- package/src/gateway/connectors/qoder-cli.ts +6 -0
- package/src/gateway/connectors/qwen-code.ts +411 -0
- package/src/gateway/connectors/roo.ts +43 -0
- package/src/gateway/connectors/tabnine-cli.ts +6 -0
- package/src/gateway/connectors/void.ts +6 -0
- package/src/gateway/connectors/vscode-copilot.ts +286 -0
- package/src/gateway/connectors/windsurf.ts +6 -0
- package/src/gateway/connectors/zed.ts +29 -0
- package/src/gateway/signin.ts +323 -0
- package/src/handoff.ts +104 -0
- package/src/hooks.ts +455 -0
- package/src/memory/audit.ts +514 -0
- package/src/memory/blobs.ts +94 -0
- package/src/memory/cli.ts +117 -0
- package/src/memory/distill.ts +381 -0
- package/src/memory/externalize.ts +435 -0
- package/src/memory/lookups.ts +70 -0
- package/src/memory/mcp-install.ts +401 -0
- package/src/memory/mcp-server.ts +296 -0
- package/src/memory/prepare.ts +179 -0
- package/src/memory/preview.ts +154 -0
- package/src/memory/project.ts +35 -0
- package/src/memory/redact.ts +72 -0
- package/src/memory/relevance.ts +187 -0
- package/src/memory/repomap.ts +516 -0
- package/src/memory/store.ts +270 -0
- package/src/memory/sync.ts +166 -0
- package/src/memory/tidy.ts +193 -0
- package/src/memory/types.ts +118 -0
- package/src/org/admin-ops.ts +96 -0
- package/src/org/auth.ts +112 -0
- package/src/org/backup.ts +115 -0
- package/src/org/cli.ts +438 -0
- package/src/org/client.ts +404 -0
- package/src/org/config.ts +44 -0
- package/src/org/crypto.ts +537 -0
- package/src/org/db.ts +435 -0
- package/src/org/graph.ts +318 -0
- package/src/org/oidc.ts +435 -0
- package/src/org/policy.ts +398 -0
- package/src/org/server.ts +920 -0
- package/src/org/sso.ts +316 -0
- package/src/org/tls.ts +66 -0
- package/src/org/types.ts +193 -0
- package/src/self.ts +68 -0
- package/src/session/attachments.ts +53 -0
- package/src/session/peekcache.ts +50 -0
- package/src/session/types.ts +104 -0
- package/src/session/util.ts +130 -0
- 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]
|