@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,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]
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
// A small, bounded protobuf wire codec: enough to read and write the few Antigravity messages brain needs,
|
|
2
|
+
// with no generated classes and no descriptors. It takes varints (canonical, at most 64 bits), fixed64,
|
|
3
|
+
// length-delimited values and fixed32; it rejects groups and unknown wire types, caps every length and the
|
|
4
|
+
// number of fields in one message, and never recurses on its own (callers decode nested messages they know).
|
|
5
|
+
// Ported from the wire subset session-migrate (MIT, xhluca) pins for Antigravity CLI 1.1.16.
|
|
6
|
+
|
|
7
|
+
export const WIRE = { varint: 0, fixed64: 1, bytes: 2, fixed32: 5 } as const
|
|
8
|
+
|
|
9
|
+
export interface Field {
|
|
10
|
+
no: number
|
|
11
|
+
wire: 0 | 1 | 2 | 5
|
|
12
|
+
/** A varint as a number when it is a safe integer, else a bigint; raw bytes for the other wire types. */
|
|
13
|
+
value: number | bigint | Uint8Array
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface Limits {
|
|
17
|
+
/** Longest length-delimited value, and longest message. */
|
|
18
|
+
maxBytes: number
|
|
19
|
+
/** Most fields in one message. */
|
|
20
|
+
maxFields: number
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const LIMITS: Limits = { maxBytes: 64 * 1024 * 1024, maxFields: 100_000 }
|
|
24
|
+
|
|
25
|
+
export class ProtoError extends Error {
|
|
26
|
+
constructor(why: string) {
|
|
27
|
+
super(`protobuf: ${why}`)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const MAX_FIELD_NO = 536_870_911
|
|
32
|
+
|
|
33
|
+
/** A varint at `at`: its value and where the next byte is. Rejects truncated, over-long and non-canonical encodings. */
|
|
34
|
+
function readVarint(b: Uint8Array, at: number): [bigint, number] {
|
|
35
|
+
let v = 0n
|
|
36
|
+
for (let i = 0; i < 10; i++) {
|
|
37
|
+
if (at >= b.length) throw new ProtoError("varint is truncated")
|
|
38
|
+
const byte = b[at++]!
|
|
39
|
+
if (i === 9 && byte > 1) throw new ProtoError("varint exceeds 64 bits")
|
|
40
|
+
v |= BigInt(byte & 0x7f) << BigInt(7 * i)
|
|
41
|
+
if (byte < 0x80) {
|
|
42
|
+
// Canonical: no trailing zero byte (0x80 0x00 is 0 written long).
|
|
43
|
+
if (i > 0 && byte === 0) throw new ProtoError("varint is not canonical")
|
|
44
|
+
return [v, at]
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
throw new ProtoError("varint exceeds 64 bits")
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const small = (v: bigint): number | bigint => (v <= BigInt(Number.MAX_SAFE_INTEGER) ? Number(v) : v)
|
|
51
|
+
|
|
52
|
+
/** The fields of one message, in wire order. Throws ProtoError on anything outside the subset or the limits. */
|
|
53
|
+
export function decode(b: Uint8Array, limits: Limits = LIMITS): Field[] {
|
|
54
|
+
if (b.length > limits.maxBytes) throw new ProtoError("message exceeds the size limit")
|
|
55
|
+
const out: Field[] = []
|
|
56
|
+
let at = 0
|
|
57
|
+
while (at < b.length) {
|
|
58
|
+
if (out.length >= limits.maxFields) throw new ProtoError("message has too many fields")
|
|
59
|
+
const [key, next] = readVarint(b, at)
|
|
60
|
+
at = next
|
|
61
|
+
const no = Number(key >> 3n)
|
|
62
|
+
const wire = Number(key & 7n)
|
|
63
|
+
if (no <= 0 || no > MAX_FIELD_NO) throw new ProtoError(`invalid field number ${no}`)
|
|
64
|
+
switch (wire) {
|
|
65
|
+
case 0: {
|
|
66
|
+
const [v, n] = readVarint(b, at)
|
|
67
|
+
at = n
|
|
68
|
+
out.push({ no, wire: 0, value: small(v) })
|
|
69
|
+
break
|
|
70
|
+
}
|
|
71
|
+
case 1:
|
|
72
|
+
case 5: {
|
|
73
|
+
const size = wire === 1 ? 8 : 4
|
|
74
|
+
if (at + size > b.length) throw new ProtoError(`fixed${size * 8} value is truncated`)
|
|
75
|
+
out.push({ no, wire, value: b.slice(at, at + size) })
|
|
76
|
+
at += size
|
|
77
|
+
break
|
|
78
|
+
}
|
|
79
|
+
case 2: {
|
|
80
|
+
const [len, n] = readVarint(b, at)
|
|
81
|
+
at = n
|
|
82
|
+
if (len > BigInt(limits.maxBytes)) throw new ProtoError("value exceeds the size limit")
|
|
83
|
+
const end = at + Number(len)
|
|
84
|
+
if (end > b.length) throw new ProtoError("length-delimited value is truncated")
|
|
85
|
+
out.push({ no, wire: 2, value: b.slice(at, end) })
|
|
86
|
+
at = end
|
|
87
|
+
break
|
|
88
|
+
}
|
|
89
|
+
case 3:
|
|
90
|
+
case 4:
|
|
91
|
+
throw new ProtoError("groups are not supported")
|
|
92
|
+
default:
|
|
93
|
+
throw new ProtoError(`unknown wire type ${wire}`)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return out
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Encoding.
|
|
100
|
+
|
|
101
|
+
function varintBytes(v: number | bigint): Uint8Array {
|
|
102
|
+
if (typeof v === "number" && (!Number.isSafeInteger(v) || v < 0)) throw new ProtoError("cannot encode a negative, fractional or unsafe varint")
|
|
103
|
+
let n = typeof v === "bigint" ? v : BigInt(v)
|
|
104
|
+
if (n < 0n || n >= 1n << 64n) throw new ProtoError("varint out of range")
|
|
105
|
+
const out: number[] = []
|
|
106
|
+
while (n >= 0x80n) {
|
|
107
|
+
out.push(Number(n & 0x7fn) | 0x80)
|
|
108
|
+
n >>= 7n
|
|
109
|
+
}
|
|
110
|
+
out.push(Number(n))
|
|
111
|
+
return Uint8Array.from(out)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function concat(parts: Uint8Array[]): Uint8Array {
|
|
115
|
+
const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0))
|
|
116
|
+
let at = 0
|
|
117
|
+
for (const p of parts) {
|
|
118
|
+
out.set(p, at)
|
|
119
|
+
at += p.length
|
|
120
|
+
}
|
|
121
|
+
return out
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const key = (no: number, wire: number) => {
|
|
125
|
+
if (!Number.isInteger(no) || no <= 0 || no > MAX_FIELD_NO) throw new ProtoError(`invalid field number ${no}`)
|
|
126
|
+
return varintBytes(BigInt(no) * 8n + BigInt(wire))
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export const varintField = (no: number, v: number | bigint) => concat([key(no, 0), varintBytes(v)])
|
|
130
|
+
|
|
131
|
+
export function bytesField(no: number, v: Uint8Array, limits: Limits = LIMITS): Uint8Array {
|
|
132
|
+
if (v.length > limits.maxBytes) throw new ProtoError("value exceeds the size limit")
|
|
133
|
+
return concat([key(no, 2), varintBytes(v.length), v])
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export const textField = (no: number, s: string) => bytesField(no, new TextEncoder().encode(s))
|
|
137
|
+
|
|
138
|
+
/** A nested message as a length-delimited field. */
|
|
139
|
+
export const messageField = (no: number, parts: Uint8Array[]) => bytesField(no, concat(parts))
|
|
140
|
+
|
|
141
|
+
export function fixedField(no: number, v: Uint8Array): Uint8Array {
|
|
142
|
+
if (v.length !== 4 && v.length !== 8) throw new ProtoError("a fixed value is 4 or 8 bytes")
|
|
143
|
+
return concat([key(no, v.length === 8 ? 1 : 5), v])
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Decoded fields back to bytes: encode(decode(b)) is b for any canonical message. */
|
|
147
|
+
export function encode(fields: Field[]): Uint8Array {
|
|
148
|
+
return concat(
|
|
149
|
+
fields.map((f) => {
|
|
150
|
+
if (f.wire === 0) return varintField(f.no, f.value as number | bigint)
|
|
151
|
+
if (f.wire === 2) return bytesField(f.no, f.value as Uint8Array)
|
|
152
|
+
return fixedField(f.no, f.value as Uint8Array)
|
|
153
|
+
}),
|
|
154
|
+
)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Reading fields. Protobuf lets a singular field repeat (the last one wins), so these take the last.
|
|
158
|
+
|
|
159
|
+
const last = (fields: Field[], no: number, wire: number) => {
|
|
160
|
+
for (let i = fields.length - 1; i >= 0; i--) if (fields[i]!.no === no && fields[i]!.wire === wire) return fields[i]!
|
|
161
|
+
return undefined
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Every length-delimited value of a repeated field, in order. */
|
|
165
|
+
export const allBytes = (fields: Field[], no: number): Uint8Array[] => fields.filter((f) => f.no === no && f.wire === 2).map((f) => f.value as Uint8Array)
|
|
166
|
+
|
|
167
|
+
export const bytesOf = (fields: Field[], no: number): Uint8Array | undefined => last(fields, no, 2)?.value as Uint8Array | undefined
|
|
168
|
+
|
|
169
|
+
export function numberOf(fields: Field[], no: number): number | undefined {
|
|
170
|
+
const v = last(fields, no, 0)?.value
|
|
171
|
+
return v === undefined ? undefined : Number(v)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const utf8 = new TextDecoder("utf-8", { fatal: true })
|
|
175
|
+
|
|
176
|
+
/** A value as UTF-8 text; undefined when absent or not valid UTF-8. */
|
|
177
|
+
export function textOf(fields: Field[], no: number): string | undefined {
|
|
178
|
+
const b = bytesOf(fields, no)
|
|
179
|
+
return b === undefined ? undefined : asText(b)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function asText(b: Uint8Array): string | undefined {
|
|
183
|
+
try {
|
|
184
|
+
return utf8.decode(b)
|
|
185
|
+
} catch {
|
|
186
|
+
return undefined
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** A nested message, decoded; undefined when absent. */
|
|
191
|
+
export function messageOf(fields: Field[], no: number, limits: Limits = LIMITS): Field[] | undefined {
|
|
192
|
+
const b = bytesOf(fields, no)
|
|
193
|
+
return b === undefined ? undefined : decode(b, limits)
|
|
194
|
+
}
|