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