@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,131 @@
|
|
|
1
|
+
import { join } from "node:path"
|
|
2
|
+
import { ls, readJson } from "../../fsutil.ts"
|
|
3
|
+
import type { Session, Turn } from "../../session/types.ts"
|
|
4
|
+
import { asText, clip, header, iso, lossCounter, toolKindOf } from "../../session/util.ts"
|
|
5
|
+
import type { Reader, SessionRef } from "../types.ts"
|
|
6
|
+
import { CONTINUED_NOTE, DUMMY_ANSWER, LABEL, SUMMARY_PREFIX, TOOL, historyDir, qKind } from "./records.ts"
|
|
7
|
+
|
|
8
|
+
// Reads Amazon Q's IDE chat history: one LokiJS file per workspace, each tab (conversation) a document
|
|
9
|
+
// in its "tabs" collection. Messages alternate prompt / answer; an answer's toolUses are answered by the
|
|
10
|
+
// next prompt's userInputMessageContext.toolResults (a prompt with an empty body carries only results).
|
|
11
|
+
// Prompts and answers Amazon Q hides (shouldDisplayMessage false) are its own: placeholder answers,
|
|
12
|
+
// retries, generated instructions.
|
|
13
|
+
|
|
14
|
+
type Row = Record<string, any>
|
|
15
|
+
|
|
16
|
+
export interface LokiDb {
|
|
17
|
+
collections: Array<{ name: string; data: Row[]; maxId?: number; idIndex?: number[] | null; binaryIndices?: Record<string, { name: string; dirty: boolean; values: number[] }> }>
|
|
18
|
+
[k: string]: unknown
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const tabsOf = (db: LokiDb | undefined): Row[] => db?.collections?.find((c) => c.name === "tabs")?.data ?? []
|
|
22
|
+
|
|
23
|
+
/** A tool result's content blocks as text: text blocks as they are, JSON blocks ({output} or {error}) unwrapped. */
|
|
24
|
+
function resultText(content: unknown): string {
|
|
25
|
+
const parts: string[] = []
|
|
26
|
+
for (const b of Array.isArray(content) ? (content as Row[]) : []) {
|
|
27
|
+
if (typeof b?.text === "string") parts.push(b.text)
|
|
28
|
+
else if (b?.json !== undefined) {
|
|
29
|
+
const j = b.json
|
|
30
|
+
parts.push(j && typeof j === "object" && !Array.isArray(j) && Object.keys(j).length === 1 && ("output" in j || "error" in j) ? asText(j.output ?? j.error) : asText(j))
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return parts.join("\n")
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function readTab(file: string, tab: Row): Session {
|
|
37
|
+
const messages = (Array.isArray(tab.conversations) ? (tab.conversations as Row[]) : []).flatMap((c) => (Array.isArray(c?.messages) ? (c.messages as Row[]) : []))
|
|
38
|
+
let cwd: string | undefined
|
|
39
|
+
for (const m of messages) {
|
|
40
|
+
const folders = m?.userInputMessageContext?.editorState?.workspaceFolders
|
|
41
|
+
if (Array.isArray(folders) && typeof folders[0] === "string") {
|
|
42
|
+
cwd = folders[0]
|
|
43
|
+
break
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const stamps = messages.map((m) => iso(m?.timestamp)).filter((t): t is string => !!t)
|
|
47
|
+
const h = header(TOOL, String(tab.historyId), {
|
|
48
|
+
sourcePath: file,
|
|
49
|
+
...(typeof tab.title === "string" && tab.title.trim() ? { title: tab.title } : {}),
|
|
50
|
+
project: cwd ? { cwd } : {},
|
|
51
|
+
...(stamps[0] ? { startedAt: stamps[0] } : {}),
|
|
52
|
+
...(iso(tab.updatedAt) ?? stamps[stamps.length - 1] ? { updatedAt: iso(tab.updatedAt) ?? stamps[stamps.length - 1] } : {}),
|
|
53
|
+
})
|
|
54
|
+
if (typeof tab.modelId === "string") h.models.push(tab.modelId)
|
|
55
|
+
const lost = lossCounter()
|
|
56
|
+
const turns: Turn[] = []
|
|
57
|
+
const calls = new Map<string, string>()
|
|
58
|
+
for (const m of messages) {
|
|
59
|
+
const at = iso(m?.timestamp)
|
|
60
|
+
const body = typeof m?.body === "string" ? m.body : ""
|
|
61
|
+
if (m?.type === "prompt") {
|
|
62
|
+
for (const r of (m.userInputMessageContext?.toolResults as Row[] | undefined) ?? []) {
|
|
63
|
+
if (typeof r?.toolUseId !== "string") continue
|
|
64
|
+
const { text, truncated } = clip(resultText(r.content))
|
|
65
|
+
turns.push({ kind: "tool_result", ...(at ? { at } : {}), callId: r.toolUseId, ok: r.status !== "error", output: text, ...(truncated ? { truncated } : {}) })
|
|
66
|
+
}
|
|
67
|
+
if (Array.isArray(m.images) && m.images.length) lost.add("images", m.images.length)
|
|
68
|
+
if (!body.trim()) continue
|
|
69
|
+
// A summary a continued session starts from (shown, as Amazon Q shows its own compaction summary).
|
|
70
|
+
if (body.startsWith(SUMMARY_PREFIX)) {
|
|
71
|
+
turns.push({ kind: "compaction", ...(at ? { at } : {}), summary: body.replace(/^\[[^\]]*\]\s*/, "") })
|
|
72
|
+
continue
|
|
73
|
+
}
|
|
74
|
+
if (m.shouldDisplayMessage === false) {
|
|
75
|
+
const from = body.match(CONTINUED_NOTE)
|
|
76
|
+
if (from) h.parent = { id: from[1]!, kind: "continued" }
|
|
77
|
+
else lost.add("Amazon Q's hidden prompts (not carried)")
|
|
78
|
+
continue
|
|
79
|
+
}
|
|
80
|
+
turns.push({ kind: "user", ...(at ? { at } : {}), text: body })
|
|
81
|
+
} else if (m?.type === "answer") {
|
|
82
|
+
if (m.shouldDisplayMessage === false && body === DUMMY_ANSWER) continue
|
|
83
|
+
if (body.trim()) turns.push({ kind: "assistant", ...(at ? { at } : {}), text: body })
|
|
84
|
+
for (const u of (m.toolUses as Row[] | undefined) ?? []) {
|
|
85
|
+
if (typeof u?.toolUseId !== "string" || typeof u?.name !== "string" || calls.has(u.toolUseId)) continue
|
|
86
|
+
calls.set(u.toolUseId, u.name)
|
|
87
|
+
turns.push({ kind: "tool_call", ...(at ? { at } : {}), id: u.toolUseId, name: u.name, toolKind: qKind(u.name) ?? toolKindOf(u.name), input: u.input ?? {} })
|
|
88
|
+
}
|
|
89
|
+
} else lost.add(`unknown message types: ${String(m?.type)}`)
|
|
90
|
+
}
|
|
91
|
+
lost.flush(h)
|
|
92
|
+
return { header: h, turns }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export const amazonQReader: Reader = {
|
|
96
|
+
tool: TOOL,
|
|
97
|
+
label: LABEL,
|
|
98
|
+
|
|
99
|
+
async list(home: string): Promise<SessionRef[]> {
|
|
100
|
+
const refs: SessionRef[] = []
|
|
101
|
+
const dir = historyDir(home)
|
|
102
|
+
for (const name of ls(dir)) {
|
|
103
|
+
if (!/^chat-history-.+\.json$/.test(name)) continue
|
|
104
|
+
const file = join(dir, name)
|
|
105
|
+
for (const tab of tabsOf(readJson<LokiDb>(file))) {
|
|
106
|
+
if (typeof tab.historyId !== "string") continue
|
|
107
|
+
const hasMessages = (tab.conversations as Row[] | undefined)?.some((c) => Array.isArray(c?.messages) && c.messages.length)
|
|
108
|
+
if (!hasMessages) continue
|
|
109
|
+
const updatedAt = iso(tab.updatedAt)
|
|
110
|
+
const folders = (tab.conversations as Row[]).flatMap((c) => c.messages as Row[]).map((m) => m?.userInputMessageContext?.editorState?.workspaceFolders).find((f) => Array.isArray(f) && typeof f[0] === "string")
|
|
111
|
+
refs.push({
|
|
112
|
+
tool: TOOL,
|
|
113
|
+
sourceId: tab.historyId,
|
|
114
|
+
path: file,
|
|
115
|
+
...(typeof tab.title === "string" ? { title: tab.title } : {}),
|
|
116
|
+
...(folders ? { cwd: folders[0] as string } : {}),
|
|
117
|
+
...(updatedAt ? { updatedAt } : {}),
|
|
118
|
+
})
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return refs.sort((a, b) => (b.updatedAt ?? "").localeCompare(a.updatedAt ?? ""))
|
|
122
|
+
},
|
|
123
|
+
|
|
124
|
+
async read(ref: SessionRef): Promise<Session> {
|
|
125
|
+
const db = readJson<LokiDb>(ref.path)
|
|
126
|
+
if (!db) throw new Error(`${ref.path} is not a readable Amazon Q history file`)
|
|
127
|
+
const tab = tabsOf(db).find((t) => t.historyId === ref.sourceId)
|
|
128
|
+
if (!tab) throw new Error(`conversation ${ref.sourceId} is not in ${ref.path}`)
|
|
129
|
+
return readTab(ref.path, tab)
|
|
130
|
+
},
|
|
131
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { createHash } from "node:crypto"
|
|
2
|
+
import { join } from "node:path"
|
|
3
|
+
import { isDir, ls } from "../../fsutil.ts"
|
|
4
|
+
import type { ToolKind } from "../../session/types.ts"
|
|
5
|
+
|
|
6
|
+
// What Amazon Q Developer's IDE chat (the VS Code extension, and JetBrains, through the same language
|
|
7
|
+
// server) keeps on disk, as far as this adapter relies on it. Checked against aws/language-servers at
|
|
8
|
+
// ba9d3b1 (2026-11-06: agenticChat/tools/chatDb/chatDb.ts, util.ts, tabBarController.ts,
|
|
9
|
+
// agenticChatController.ts), the language server 1.85.0 bundle installed here, LokiJS 1.5.12, and the
|
|
10
|
+
// history files on the author's Mac (structure and counts only).
|
|
11
|
+
//
|
|
12
|
+
// Each workspace has one LokiJS database, ~/.aws/amazonq/history/chat-history-<id>.json, holding every
|
|
13
|
+
// conversation ("tab") of that workspace: <id> is the MD5 of the workspace folder's URI path (several
|
|
14
|
+
// folders: their sorted paths joined by |; a saved .code-workspace file: SHA-256 of its path). A tab's
|
|
15
|
+
// messages are both what the History panel restores and what the agent sends as conversation history
|
|
16
|
+
// (fixAndGetHistory reads them from the database on every request; conversation ids are client-made).
|
|
17
|
+
|
|
18
|
+
export const TOOL = "amazon-q-ide"
|
|
19
|
+
export const LABEL = "Amazon Q (IDE)"
|
|
20
|
+
|
|
21
|
+
/** Language server versions the writer's records were checked against. */
|
|
22
|
+
export const TESTED_SERVERS = ["1.85.0"]
|
|
23
|
+
|
|
24
|
+
export const historyDir = (home: string) => join(home, ".aws", "amazonq", "history")
|
|
25
|
+
export const dbName = (workspaceId: string) => `chat-history-${workspaceId}.json`
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The path part of the folder's URI as VS Code sends it (vscode-uri encodes everything outside RFC 3986's
|
|
29
|
+
* unreserved set except '/'), which the server MD5s (`new URL(uri).pathname`).
|
|
30
|
+
*/
|
|
31
|
+
export function uriPath(folder: string): string {
|
|
32
|
+
return folder
|
|
33
|
+
.split("/")
|
|
34
|
+
.map((seg) => encodeURIComponent(seg).replace(/[!'()*]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`))
|
|
35
|
+
.join("/")
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The database id of a single-folder workspace. */
|
|
39
|
+
export const workspaceId = (folder: string) => createHash("md5").update(uriPath(folder)).digest("hex")
|
|
40
|
+
|
|
41
|
+
/** The newest Amazon Q language server downloaded to this home, e.g. "1.85.0". */
|
|
42
|
+
export function serverVersion(home: string): string | undefined {
|
|
43
|
+
const dir = join(home, "Library", "Caches", "aws", "toolkits", "language-servers", "AmazonQ")
|
|
44
|
+
const versions = ls(dir).filter((v) => /^\d+\.\d+\.\d+$/.test(v) && isDir(join(dir, v)))
|
|
45
|
+
const num = (v: string) => v.split(".").map(Number)
|
|
46
|
+
versions.sort((a, b) => {
|
|
47
|
+
const x = num(a)
|
|
48
|
+
const y = num(b)
|
|
49
|
+
for (let i = 0; i < 3; i++) if (x[i] !== y[i]) return x[i]! - y[i]!
|
|
50
|
+
return 0
|
|
51
|
+
})
|
|
52
|
+
return versions[versions.length - 1]
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** The editors with the Amazon Q extension installed here, as the command that opens a folder in them. */
|
|
56
|
+
export function editors(home: string): string[] {
|
|
57
|
+
const out: string[] = []
|
|
58
|
+
for (const [dir, cmd] of [[".vscode", "code"], [".cursor", "cursor"], [".windsurf", "windsurf"], [".kiro", "kiro"]] as const)
|
|
59
|
+
if (ls(join(home, dir, "extensions")).some((e) => e.startsWith("amazonwebservices.amazon-q-vscode-"))) out.push(cmd)
|
|
60
|
+
return out
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Whether Amazon Q's language server is running: it keeps the database in memory and saves it every second. */
|
|
64
|
+
export function serverRunning(): boolean {
|
|
65
|
+
const p = Bun.spawnSync(["pgrep", "-f", "aws-lsp-codewhisperer"], { stdout: "pipe", stderr: "pipe" })
|
|
66
|
+
return p.exitCode === 0
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Amazon Q's tools, by what they do (toolConstants.ts). */
|
|
70
|
+
const KIND_OF: Record<string, ToolKind> = {
|
|
71
|
+
fsRead: "read",
|
|
72
|
+
fsWrite: "write",
|
|
73
|
+
fsReplace: "edit",
|
|
74
|
+
listDirectory: "list",
|
|
75
|
+
grepSearch: "search",
|
|
76
|
+
fileSearch: "search",
|
|
77
|
+
executeBash: "shell",
|
|
78
|
+
codeReview: "other",
|
|
79
|
+
}
|
|
80
|
+
export const qKind = (name: string): ToolKind | undefined => KIND_OF[name]
|
|
81
|
+
|
|
82
|
+
const pick = (o: Record<string, unknown>, keys: string[]): unknown => {
|
|
83
|
+
for (const k of keys) if (o[k] !== undefined && o[k] !== null && o[k] !== "") return o[k]
|
|
84
|
+
return undefined
|
|
85
|
+
}
|
|
86
|
+
const str = (v: unknown) => (typeof v === "string" ? v : Array.isArray(v) && v.every((x) => typeof x === "string") ? v.join(" ") : undefined)
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* A call from another tool as the Amazon Q tool that does the same thing, with Q's input shape, so the
|
|
90
|
+
* history panel renders it like its own (toolUseToToolChatMessage) and the model sees calls it knows.
|
|
91
|
+
* Calls whose input does not map cleanly keep their own name and input.
|
|
92
|
+
*/
|
|
93
|
+
export function asQCall(kind: ToolKind, name: string, input: unknown): { name: string; input: Record<string, unknown> } {
|
|
94
|
+
const obj = input && typeof input === "object" && !Array.isArray(input) ? (input as Record<string, unknown>) : { input: input ?? null }
|
|
95
|
+
if (KIND_OF[name]) return { name, input: obj }
|
|
96
|
+
const path = str(pick(obj, ["file_path", "target_file", "path", "filePath", "absolute_path", "filename"]))
|
|
97
|
+
switch (kind) {
|
|
98
|
+
case "read":
|
|
99
|
+
if (path) return { name: "fsRead", input: { paths: [path] } }
|
|
100
|
+
break
|
|
101
|
+
case "write": {
|
|
102
|
+
const text = pick(obj, ["content", "contents", "file_text", "fileText", "text"])
|
|
103
|
+
if (path && typeof text === "string") return { name: "fsWrite", input: { command: "create", path, fileText: text } }
|
|
104
|
+
break
|
|
105
|
+
}
|
|
106
|
+
case "edit": {
|
|
107
|
+
const oldStr = str(pick(obj, ["old_string", "old_str", "oldText", "old_text"]))
|
|
108
|
+
const newStr = pick(obj, ["new_string", "new_str", "newText", "new_text"])
|
|
109
|
+
if (path && oldStr !== undefined && typeof newStr === "string") return { name: "fsReplace", input: { path, diffs: [{ oldStr, newStr }] } }
|
|
110
|
+
break
|
|
111
|
+
}
|
|
112
|
+
case "list": {
|
|
113
|
+
const dir = str(pick(obj, ["path", "target_directory", "dir_path", "directory", "relative_workspace_path"]))
|
|
114
|
+
if (dir) return { name: "listDirectory", input: { path: dir } }
|
|
115
|
+
break
|
|
116
|
+
}
|
|
117
|
+
case "shell": {
|
|
118
|
+
const command = str(pick(obj, ["command", "cmd", "commandLine"]))
|
|
119
|
+
if (command) return { name: "executeBash", input: { command } }
|
|
120
|
+
break
|
|
121
|
+
}
|
|
122
|
+
case "search": {
|
|
123
|
+
const query = str(pick(obj, ["pattern", "regex", "query"]))
|
|
124
|
+
if (/grep|ripgrep|search_files/i.test(name) && query) return { name: "grepSearch", input: { query, ...(path ? { path } : {}) } }
|
|
125
|
+
break
|
|
126
|
+
}
|
|
127
|
+
default:
|
|
128
|
+
break
|
|
129
|
+
}
|
|
130
|
+
return { name, input: obj }
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** The placeholder answer Amazon Q itself stores to keep prompts and answers alternating; never shown. */
|
|
134
|
+
export const DUMMY_ANSWER = "Working..."
|
|
135
|
+
|
|
136
|
+
/** The note the writer puts first; the reader recognises it and records where the session came from. */
|
|
137
|
+
export const continuedNote = (label: string, id: string, when: string) =>
|
|
138
|
+
`[This conversation continues ${label} session ${id}, carried over by brain session on ${when}. The messages after this note are its history; reasoning and anything listed as lost did not carry over. Check files before relying on details, since they may have changed.]`
|
|
139
|
+
export const CONTINUED_NOTE = /^\[This conversation continues .+? session (\S+), carried over by brain session/
|
|
140
|
+
|
|
141
|
+
export const SUMMARY_PREFIX = "[Summary of the earlier conversation"
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
import { randomBytes, randomUUID } from "node:crypto"
|
|
2
|
+
import { copyFileSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs"
|
|
3
|
+
import { basename, join, resolve } from "node:path"
|
|
4
|
+
import { toolLabel } from "../../brief.ts"
|
|
5
|
+
import { exists, isDir } from "../../fsutil.ts"
|
|
6
|
+
import type { Session, Turn } from "../../session/types.ts"
|
|
7
|
+
import { countTurns, firstUserText, unpaired } from "../../session/util.ts"
|
|
8
|
+
import type { Readiness, WriteOptions, WriteResult, Writer } from "../types.ts"
|
|
9
|
+
import { type LokiDb, amazonQReader, tabsOf } from "./reader.ts"
|
|
10
|
+
import {
|
|
11
|
+
DUMMY_ANSWER, LABEL, SUMMARY_PREFIX, TESTED_SERVERS, TOOL,
|
|
12
|
+
asQCall, continuedNote, dbName, editors, historyDir, serverRunning, serverVersion, workspaceId,
|
|
13
|
+
} from "./records.ts"
|
|
14
|
+
|
|
15
|
+
// Continues a portable session in Amazon Q's IDE chat as a new conversation (tab) in the workspace's
|
|
16
|
+
// history database, the document ChatDatabase.addMessage builds: prompts and answers alternating, each
|
|
17
|
+
// answer's toolUses answered by the next prompt's toolResults, ids in Q's style. The tab is marked open,
|
|
18
|
+
// so Amazon Q restores it when the editor next opens the folder (tabBarController.loadChats); it is also
|
|
19
|
+
// in Amazon Q's History list. Its messages are the conversation history the agent sends with the next
|
|
20
|
+
// request, so the model has the whole conversation.
|
|
21
|
+
//
|
|
22
|
+
// The database holds the workspace's other conversations too, so the writer only adds a document and
|
|
23
|
+
// never changes another, refuses while Amazon Q's language server runs (it keeps the database in memory
|
|
24
|
+
// and saves it every second), and copies the file to ~/.brain/backups/amazon-q-ide/<time>/ first.
|
|
25
|
+
//
|
|
26
|
+
// Amazon Q's own conventions decide the rest: the note saying where the session came from is a prompt
|
|
27
|
+
// the panel does not show (shouldDisplayMessage false) followed by Q's hidden "Working..." answer, the
|
|
28
|
+
// pair Q itself stores to keep prompts and answers alternating; a compaction starts the conversation from
|
|
29
|
+
// its summary, as Q's replaceWithSummary does. Reasoning has no place in Q's history and is dropped.
|
|
30
|
+
|
|
31
|
+
type Row = Record<string, unknown>
|
|
32
|
+
type Call = Extract<Turn, { kind: "tool_call" }>
|
|
33
|
+
type Result = Extract<Turn, { kind: "tool_result" }>
|
|
34
|
+
|
|
35
|
+
interface QMessage {
|
|
36
|
+
body: string
|
|
37
|
+
type: "prompt" | "answer"
|
|
38
|
+
timestamp: string
|
|
39
|
+
shouldDisplayMessage: boolean
|
|
40
|
+
origin?: "IDE"
|
|
41
|
+
userInputMessageContext?: { toolResults?: Row[] }
|
|
42
|
+
images?: unknown[]
|
|
43
|
+
toolUses?: Array<{ toolUseId: string; name: string; input: Record<string, unknown> }>
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const ALNUM = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
|
|
47
|
+
/** Amazon Q's tool use ids: tooluse_ and 22 characters. */
|
|
48
|
+
const toolUseId = () => `tooluse_${[...randomBytes(22)].map((b) => ALNUM[b % ALNUM.length]).join("")}`
|
|
49
|
+
|
|
50
|
+
const shellQuote = (s: string) => (/^[\w./@%+=:,-]+$/.test(s) ? s : `'${s.replace(/'/g, `'\\''`)}'`)
|
|
51
|
+
|
|
52
|
+
export interface QPlan {
|
|
53
|
+
tab: Row
|
|
54
|
+
lost: string[]
|
|
55
|
+
expect: { users: number; calls: number }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** The tab document of a new Amazon Q conversation continuing `session` (without $loki and meta). Pure. */
|
|
59
|
+
export function planTab(session: Session, opts: { historyId: string; conversationId: string; now: Date }): QPlan {
|
|
60
|
+
const drops = new Map<string, number>()
|
|
61
|
+
const drop = (what: string, n = 1) => drops.set(what, (drops.get(what) ?? 0) + n)
|
|
62
|
+
const label = toolLabel(session.header.tool)
|
|
63
|
+
const turns = session.turns
|
|
64
|
+
const lastCompaction = turns.map((t) => t.kind).lastIndexOf("compaction")
|
|
65
|
+
if (lastCompaction > 0) drop("turns before the last compaction (Amazon Q keeps only the summary)", turns.slice(0, lastCompaction).filter((t) => t.kind === "user" || t.kind === "assistant").length)
|
|
66
|
+
|
|
67
|
+
const results = new Map<string, Result>()
|
|
68
|
+
const callIds = new Set<string>()
|
|
69
|
+
for (const t of turns) {
|
|
70
|
+
if (t.kind === "tool_result" && !results.has(t.callId)) results.set(t.callId, t)
|
|
71
|
+
if (t.kind === "tool_call") callIds.add(t.id)
|
|
72
|
+
}
|
|
73
|
+
const ids = new Map<string, string>()
|
|
74
|
+
const qId = (id: string) => {
|
|
75
|
+
let v = ids.get(id)
|
|
76
|
+
if (!v) ids.set(id, (v = toolUseId()))
|
|
77
|
+
return v
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const known = turns.map((t) => (t.at ? Date.parse(t.at) : Number.NaN)).find(Number.isFinite)
|
|
81
|
+
let clock = (known ?? opts.now.getTime()) - 1
|
|
82
|
+
const stamp = (at?: string) => {
|
|
83
|
+
const t = at ? Date.parse(at) : Number.NaN
|
|
84
|
+
clock = Number.isFinite(t) && t > clock ? t : clock + 1
|
|
85
|
+
return new Date(clock).toISOString()
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const msgs: QMessage[] = []
|
|
89
|
+
const prompt = (body: string, shown: boolean, at?: string, toolResults?: Row[]): QMessage => ({
|
|
90
|
+
body,
|
|
91
|
+
type: "prompt",
|
|
92
|
+
origin: "IDE",
|
|
93
|
+
userInputMessageContext: toolResults ? { toolResults } : {},
|
|
94
|
+
shouldDisplayMessage: shown,
|
|
95
|
+
timestamp: stamp(at),
|
|
96
|
+
images: [],
|
|
97
|
+
})
|
|
98
|
+
const dummy = (): QMessage => ({ body: DUMMY_ANSWER, type: "answer", shouldDisplayMessage: false, timestamp: stamp() })
|
|
99
|
+
/** Appends keeping Q's invariant: prompts and answers alternate, starting with a prompt. */
|
|
100
|
+
const push = (m: QMessage) => {
|
|
101
|
+
const last = msgs[msgs.length - 1]
|
|
102
|
+
if (last?.type === "prompt" && m.type === "prompt") {
|
|
103
|
+
// A request typed right after tool results travels with them, as Q sends it; otherwise Q's hidden placeholder answer goes between.
|
|
104
|
+
if (!last.body && last.userInputMessageContext?.toolResults && !m.userInputMessageContext?.toolResults && m.shouldDisplayMessage) {
|
|
105
|
+
last.body = m.body
|
|
106
|
+
return
|
|
107
|
+
}
|
|
108
|
+
msgs.push(dummy())
|
|
109
|
+
} else if (last?.type === "answer" && m.type === "answer" && !last.toolUses?.length) {
|
|
110
|
+
last.body = [last.body, m.body].filter((b) => b.trim()).join("\n\n")
|
|
111
|
+
last.toolUses = m.toolUses ?? []
|
|
112
|
+
return
|
|
113
|
+
}
|
|
114
|
+
msgs.push(m)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
push(prompt(continuedNote(label, session.header.id, opts.now.toISOString().slice(0, 10)), false))
|
|
118
|
+
|
|
119
|
+
let cur: { text: string; at?: string; calls: Call[] } | undefined
|
|
120
|
+
const flush = () => {
|
|
121
|
+
if (!cur) return
|
|
122
|
+
const a = cur
|
|
123
|
+
cur = undefined
|
|
124
|
+
if (!a.text.trim() && !a.calls.length) return
|
|
125
|
+
push({
|
|
126
|
+
body: a.text,
|
|
127
|
+
type: "answer",
|
|
128
|
+
toolUses: a.calls.map((c) => {
|
|
129
|
+
const q = asQCall(c.toolKind, c.name, c.input)
|
|
130
|
+
return { toolUseId: qId(c.id), name: q.name, input: q.input }
|
|
131
|
+
}),
|
|
132
|
+
shouldDisplayMessage: true,
|
|
133
|
+
timestamp: stamp(a.at),
|
|
134
|
+
})
|
|
135
|
+
if (!a.calls.length) return
|
|
136
|
+
const toolResults = a.calls.map((c) => {
|
|
137
|
+
const r = results.get(c.id)
|
|
138
|
+
if (!r) drop("tool calls without a result (closed as errors)")
|
|
139
|
+
const text = r ? r.output + (r.fullPath ? `\n\n[Full output: ${r.fullPath}]` : "") : "No result was recorded for this call."
|
|
140
|
+
return { toolUseId: qId(c.id), status: r?.ok ? "success" : "error", content: [{ text: text || "(no output)" }] }
|
|
141
|
+
})
|
|
142
|
+
push(prompt("", true, results.get(a.calls[a.calls.length - 1]!.id)?.at, toolResults))
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const expect = { users: 0, calls: 0 }
|
|
146
|
+
const seen = new Set<string>()
|
|
147
|
+
for (const [i, t] of turns.entries()) {
|
|
148
|
+
if (i < lastCompaction) continue
|
|
149
|
+
switch (t.kind) {
|
|
150
|
+
case "compaction":
|
|
151
|
+
push(prompt(`${SUMMARY_PREFIX} in ${label}, which replaced the turns before it]\n\n${t.summary}`, true, t.at))
|
|
152
|
+
break
|
|
153
|
+
case "user":
|
|
154
|
+
if (t.attachments?.length) drop("attachments", t.attachments.length)
|
|
155
|
+
if (!t.text.trim()) break
|
|
156
|
+
flush()
|
|
157
|
+
push(prompt(t.text, true, t.at))
|
|
158
|
+
expect.users++
|
|
159
|
+
break
|
|
160
|
+
case "assistant":
|
|
161
|
+
if (!t.text.trim()) break
|
|
162
|
+
// Text after a call is a new answer: it was written after the call's result came back.
|
|
163
|
+
if (cur?.calls.length) flush()
|
|
164
|
+
cur ??= { text: "", ...(t.at ? { at: t.at } : {}), calls: [] }
|
|
165
|
+
cur.text = cur.text ? `${cur.text}\n\n${t.text}` : t.text
|
|
166
|
+
break
|
|
167
|
+
case "tool_call":
|
|
168
|
+
if (seen.has(t.id)) {
|
|
169
|
+
drop("repeated tool call ids")
|
|
170
|
+
break
|
|
171
|
+
}
|
|
172
|
+
seen.add(t.id)
|
|
173
|
+
cur ??= { text: "", ...(t.at ? { at: t.at } : {}), calls: [] }
|
|
174
|
+
cur.calls.push(t)
|
|
175
|
+
expect.calls++
|
|
176
|
+
break
|
|
177
|
+
case "tool_result":
|
|
178
|
+
if (!callIds.has(t.callId)) drop("tool results without a call")
|
|
179
|
+
// A result closes the answer it belongs to, so a call made after it starts a new answer.
|
|
180
|
+
else if (cur?.calls.some((c) => c.id === t.callId)) flush()
|
|
181
|
+
break
|
|
182
|
+
case "reasoning":
|
|
183
|
+
drop("reasoning")
|
|
184
|
+
break
|
|
185
|
+
case "file_edit":
|
|
186
|
+
if (!t.callId || !callIds.has(t.callId)) drop("file edits outside a tool call")
|
|
187
|
+
break
|
|
188
|
+
case "subagent":
|
|
189
|
+
drop("subagent sessions")
|
|
190
|
+
break
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
flush()
|
|
194
|
+
// Q ends a stored conversation on an answer; a trailing request gets its hidden placeholder, as Q adds one.
|
|
195
|
+
if (msgs[msgs.length - 1]?.type === "prompt") msgs.push(dummy())
|
|
196
|
+
|
|
197
|
+
const title = session.header.title ?? firstUserText(session)?.replace(/\s+/g, " ").trim().slice(0, 60) ?? "Session"
|
|
198
|
+
const tab: Row = {
|
|
199
|
+
historyId: opts.historyId,
|
|
200
|
+
// LokiJS stores a top-level Date as epoch milliseconds; nested ones as ISO strings.
|
|
201
|
+
updatedAt: opts.now.getTime(),
|
|
202
|
+
isOpen: true,
|
|
203
|
+
tabType: "cwc",
|
|
204
|
+
title: `${title} (from ${label})`,
|
|
205
|
+
conversations: [{ conversationId: opts.conversationId, clientType: "vscode", updatedAt: opts.now.toISOString(), messages: msgs }],
|
|
206
|
+
}
|
|
207
|
+
return { tab, lost: [...drops].map(([what, n]) => `${what} (${n})`), expect }
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** A database file as LokiJS 1.5 writes one for a workspace whose history is still empty (copied from Q's own). */
|
|
211
|
+
export function emptyDb(file: string, now: Date): LokiDb {
|
|
212
|
+
const events = { insert: [], update: [], "pre-insert": [], "pre-update": [], close: [], flushbuffer: [], error: [], delete: [null], warning: [null] }
|
|
213
|
+
const collection = (name: string, extra: Row) => ({
|
|
214
|
+
name, data: [], idIndex: null, binaryIndices: {}, constraints: null, uniqueNames: [], transforms: {}, objType: name, dirty: false, cachedIndex: null,
|
|
215
|
+
cachedBinaryIndex: null, cachedData: null, adaptiveBinaryIndices: true, transactional: false, cloneObjects: false, cloneMethod: "parse-stringify",
|
|
216
|
+
asyncListeners: false, disableMeta: false, disableChangesApi: true, disableDeltaChangesApi: true, autoupdate: false, serializableIndices: true,
|
|
217
|
+
disableFreeze: true, ttl: null, maxId: 0, DynamicViews: [], events, changes: [], dirtyIds: [], isIncremental: false, ...extra,
|
|
218
|
+
})
|
|
219
|
+
return {
|
|
220
|
+
filename: basename(file),
|
|
221
|
+
collections: [
|
|
222
|
+
collection("tabs", { binaryIndices: { updatedAt: { name: "updatedAt", dirty: false, values: [] }, isOpen: { name: "isOpen", dirty: false, values: [] } }, uniqueNames: ["historyId"] }),
|
|
223
|
+
collection("settings", {}),
|
|
224
|
+
],
|
|
225
|
+
databaseVersion: 1.5,
|
|
226
|
+
engineVersion: 1.5,
|
|
227
|
+
autosave: true,
|
|
228
|
+
autosaveInterval: 1000,
|
|
229
|
+
autosaveHandle: null,
|
|
230
|
+
throttledSaves: true,
|
|
231
|
+
options: { adapter: null, autosave: true, autoload: true, autosaveInterval: 1000, persistenceMethod: "fs", serializationMethod: "normal", destructureDelimiter: "$<\n", recursiveWait: true, recursiveWaitLimit: false, recursiveWaitLimitDuration: 2000, started: now.getTime() },
|
|
232
|
+
persistenceMethod: "adapter",
|
|
233
|
+
persistenceAdapter: null,
|
|
234
|
+
verbose: false,
|
|
235
|
+
events: { init: [null], loaded: [], flushChanges: [], close: [], changes: [], warning: [] },
|
|
236
|
+
ENV: "NODEJS",
|
|
237
|
+
isIncremental: false,
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** Adds `tab` to the database as LokiJS's insert would: the next $loki id, fresh meta, indices left for LokiJS to rebuild. */
|
|
242
|
+
export function insertTab(db: LokiDb, tab: Row, now: Date): void {
|
|
243
|
+
const tabs = db.collections.find((c) => c.name === "tabs")
|
|
244
|
+
if (!tabs) throw new Error("the history file has no tabs collection")
|
|
245
|
+
const id = (tabs.maxId ?? 0) + 1
|
|
246
|
+
tabs.maxId = id
|
|
247
|
+
tabs.data.push({ ...tab, meta: { revision: 0, created: now.getTime(), version: 0 }, $loki: id })
|
|
248
|
+
if (Array.isArray(tabs.idIndex)) tabs.idIndex.push(id)
|
|
249
|
+
// Marked dirty, LokiJS rebuilds a binary index before its next use, as it does after its own inserts.
|
|
250
|
+
for (const index of Object.values(tabs.binaryIndices ?? {})) index.dirty = true
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** The Amazon Q writer; `server` and `running` are for tests, which must not depend on this machine. */
|
|
254
|
+
export function amazonQWriter(opts: { server?: string; running?: () => boolean } = {}): Writer {
|
|
255
|
+
const installed = (home: string) => opts.server ?? serverVersion(home)
|
|
256
|
+
const running = opts.running ?? serverRunning
|
|
257
|
+
return {
|
|
258
|
+
tool: TOOL,
|
|
259
|
+
label: LABEL,
|
|
260
|
+
way: "native",
|
|
261
|
+
|
|
262
|
+
async ready(w: WriteOptions): Promise<Readiness> {
|
|
263
|
+
const v = installed(w.home)
|
|
264
|
+
if (!v) return { ok: false, why: "Amazon Q's language server was not found (it is downloaded when the Amazon Q extension first starts)" }
|
|
265
|
+
if (!TESTED_SERVERS.includes(v)) return { ok: false, why: `Amazon Q language server ${v} is not a version this writer was checked against (${TESTED_SERVERS.join(", ")})` }
|
|
266
|
+
if (running()) return { ok: false, why: "Amazon Q is running (its language server keeps the chat history in memory and would overwrite the new conversation); close VS Code, Cursor or the JetBrains IDE first" }
|
|
267
|
+
if (!isDir(w.cwd)) return { ok: false, why: `${w.cwd} is not a folder; Amazon Q keeps chat history per workspace folder` }
|
|
268
|
+
return { ok: true }
|
|
269
|
+
},
|
|
270
|
+
|
|
271
|
+
async write(session: Session, w: WriteOptions): Promise<WriteResult> {
|
|
272
|
+
const cwd = resolve(w.cwd)
|
|
273
|
+
const now = new Date()
|
|
274
|
+
const file = join(historyDir(w.home), dbName(workspaceId(cwd)))
|
|
275
|
+
const backupDir = join(w.home, ".brain", "backups", TOOL, now.toISOString().replace(/[:.]/g, "-"))
|
|
276
|
+
const had = exists(file)
|
|
277
|
+
let db: LokiDb
|
|
278
|
+
if (had) {
|
|
279
|
+
try {
|
|
280
|
+
db = JSON.parse(readFileSync(file, "utf8")) as LokiDb
|
|
281
|
+
} catch {
|
|
282
|
+
throw new Error(`${file} is not a readable Amazon Q history file; nothing was changed`)
|
|
283
|
+
}
|
|
284
|
+
} else db = emptyDb(file, now)
|
|
285
|
+
let historyId = randomUUID()
|
|
286
|
+
while (tabsOf(db).some((t) => t.historyId === historyId)) historyId = randomUUID()
|
|
287
|
+
const plan = planTab(session, { historyId, conversationId: randomUUID(), now })
|
|
288
|
+
insertTab(db, plan.tab, now)
|
|
289
|
+
|
|
290
|
+
if (!w.dryRun) {
|
|
291
|
+
mkdirSync(historyDir(w.home), { recursive: true })
|
|
292
|
+
if (had) {
|
|
293
|
+
mkdirSync(backupDir, { recursive: true })
|
|
294
|
+
copyFileSync(file, join(backupDir, basename(file)))
|
|
295
|
+
}
|
|
296
|
+
const tmp = `${file}.brain-${process.pid}`
|
|
297
|
+
writeFileSync(tmp, JSON.stringify(db), { mode: 0o600 })
|
|
298
|
+
renameSync(tmp, file)
|
|
299
|
+
try {
|
|
300
|
+
const back = await amazonQReader.read({ tool: TOOL, sourceId: historyId, path: file }, w.home)
|
|
301
|
+
const n = countTurns(back)
|
|
302
|
+
const pairs = unpaired(back)
|
|
303
|
+
if (n.user !== plan.expect.users || n.tool_call !== plan.expect.calls || pairs.calls.length || pairs.results.length)
|
|
304
|
+
throw new Error(`the new Amazon Q conversation did not read back intact (requests ${n.user}/${plan.expect.users}, tool calls ${n.tool_call}/${plan.expect.calls}, unpaired ${pairs.calls.length + pairs.results.length})`)
|
|
305
|
+
} catch (e) {
|
|
306
|
+
if (had) copyFileSync(join(backupDir, basename(file)), file)
|
|
307
|
+
else rmSync(file, { force: true })
|
|
308
|
+
throw new Error(`${(e as Error).message}; the history file was restored`)
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
const editor = editors(w.home)[0] ?? "code"
|
|
312
|
+
return {
|
|
313
|
+
tool: TOOL,
|
|
314
|
+
way: "native",
|
|
315
|
+
sessionId: historyId,
|
|
316
|
+
written: had ? [file, join(backupDir, basename(file))] : [file],
|
|
317
|
+
open: `${editor} ${shellQuote(cwd)} # Amazon Q reopens "${plan.tab.title as string}" when the folder opens; it is also in Amazon Q's History`,
|
|
318
|
+
lost: plan.lost,
|
|
319
|
+
dryRun: !!w.dryRun,
|
|
320
|
+
}
|
|
321
|
+
},
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
export const amazonQNativeWriter = amazonQWriter()
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Reader, Writer } from "../types.ts"
|
|
2
|
+
import { antigravityReader } from "./reader.ts"
|
|
3
|
+
import { antigravityWriter } from "./writer.ts"
|
|
4
|
+
|
|
5
|
+
// Antigravity (Google): reads the CLI's conversations and the IDE's SQLite ones under ~/.gemini, and continues
|
|
6
|
+
// any session as a new native conversation of the Antigravity CLI (1.1.16). The IDE has no native writer; it is a
|
|
7
|
+
// target through the continuation brief.
|
|
8
|
+
export const readers: Reader[] = [antigravityReader]
|
|
9
|
+
export const writers: Writer[] = [antigravityWriter]
|