@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,424 @@
|
|
|
1
|
+
import type { Database } from "bun:sqlite"
|
|
2
|
+
import { statSync } from "node:fs"
|
|
3
|
+
import { basename, dirname, join } from "node:path"
|
|
4
|
+
import { fileURLToPath } from "node:url"
|
|
5
|
+
import { exists, gitInfo, ls, mtime } from "../../fsutil.ts"
|
|
6
|
+
import { withSnapshot } from "../../sqlite.ts"
|
|
7
|
+
import type { Session, SessionHeader, Turn } from "../../session/types.ts"
|
|
8
|
+
import { clip, header, lossCounter } from "../../session/util.ts"
|
|
9
|
+
import { CONTINUED_NOTE } from "../claude-code/records.ts"
|
|
10
|
+
import type { Reader, SessionRef } from "../types.ts"
|
|
11
|
+
import { allBytes, bytesOf, decode, numberOf, textOf, type Field } from "./proto.ts"
|
|
12
|
+
import {
|
|
13
|
+
CONVERSATION_FILE,
|
|
14
|
+
LABEL,
|
|
15
|
+
LINK_KEY,
|
|
16
|
+
MAX_DB_BYTES,
|
|
17
|
+
MAX_STEPS,
|
|
18
|
+
PAYLOAD_FIELD,
|
|
19
|
+
STATUS,
|
|
20
|
+
STEP,
|
|
21
|
+
STORES,
|
|
22
|
+
TOOL,
|
|
23
|
+
antigravityToolKind,
|
|
24
|
+
conversationsDir,
|
|
25
|
+
decodeCall,
|
|
26
|
+
entriesOf,
|
|
27
|
+
hasThinking,
|
|
28
|
+
sqlTime,
|
|
29
|
+
summariesPath,
|
|
30
|
+
timestampOf,
|
|
31
|
+
workspaceOf,
|
|
32
|
+
type NativeCall,
|
|
33
|
+
} from "./records.ts"
|
|
34
|
+
|
|
35
|
+
// Reads Antigravity conversations (records.ts) from the CLI's store and the IDE's two. Each conversation
|
|
36
|
+
// database is read from a copy with its write-ahead log, so a live one reads consistently; the summaries
|
|
37
|
+
// database beside it gives the title, folder and time, reading only the columns it needs (the IDE's table has
|
|
38
|
+
// more). The projection follows session-migrate's: user inputs are user turns, planner responses are replies
|
|
39
|
+
// and tool calls, generic tool steps and the run_command, view_file, shell_exec and MCP steps are results.
|
|
40
|
+
// Private thinking never becomes a turn: it is only counted. Every other step type is counted by number.
|
|
41
|
+
|
|
42
|
+
interface Summary {
|
|
43
|
+
title?: string
|
|
44
|
+
cwd?: string
|
|
45
|
+
updatedAt?: string
|
|
46
|
+
parent?: string
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** A local folder from a file:// URI; undefined for anything else. */
|
|
50
|
+
function pathOfUri(uri: unknown): string | undefined {
|
|
51
|
+
if (typeof uri !== "string" || !uri.startsWith("file://")) return undefined
|
|
52
|
+
try {
|
|
53
|
+
const u = new URL(uri)
|
|
54
|
+
return u.host ? undefined : fileURLToPath(u)
|
|
55
|
+
} catch {
|
|
56
|
+
return undefined
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const WANTED = ["conversation_id", "title", "workspace_uris", "last_modified_time", "parent_conversation_id"]
|
|
61
|
+
|
|
62
|
+
/** Summary rows by conversation id, from whichever of the wanted columns the table has. */
|
|
63
|
+
function summaryRows(d: Database, id?: string): Map<string, Summary> {
|
|
64
|
+
const out = new Map<string, Summary>()
|
|
65
|
+
const cols = new Set((d.query("PRAGMA table_info(conversation_summaries)").all() as Array<{ name: string }>).map((c) => c.name))
|
|
66
|
+
if (!cols.has("conversation_id")) return out
|
|
67
|
+
const sql = `SELECT ${WANTED.filter((c) => cols.has(c))
|
|
68
|
+
.map((c) => `"${c}"`)
|
|
69
|
+
.join(", ")} FROM conversation_summaries${id ? " WHERE conversation_id = ?" : ""}`
|
|
70
|
+
const rows = (id ? d.query(sql).all(id) : d.query(sql).all()) as Array<Record<string, unknown>>
|
|
71
|
+
for (const r of rows) {
|
|
72
|
+
let cwd: string | undefined
|
|
73
|
+
try {
|
|
74
|
+
const uris = JSON.parse(String(r.workspace_uris ?? ""))
|
|
75
|
+
if (Array.isArray(uris)) cwd = pathOfUri(uris[0])
|
|
76
|
+
} catch {
|
|
77
|
+
// no folder
|
|
78
|
+
}
|
|
79
|
+
out.set(String(r.conversation_id), {
|
|
80
|
+
...(typeof r.title === "string" && r.title.trim() ? { title: r.title } : {}),
|
|
81
|
+
...(cwd ? { cwd } : {}),
|
|
82
|
+
...(sqlTime(r.last_modified_time) ? { updatedAt: sqlTime(r.last_modified_time)! } : {}),
|
|
83
|
+
...(typeof r.parent_conversation_id === "string" && r.parent_conversation_id ? { parent: r.parent_conversation_id } : {}),
|
|
84
|
+
})
|
|
85
|
+
}
|
|
86
|
+
return out
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** A store's summaries, read from a copy; none when the database is missing or unreadable. */
|
|
90
|
+
function readSummaries(path: string, id?: string): Map<string, Summary> {
|
|
91
|
+
if (!exists(path)) return new Map()
|
|
92
|
+
try {
|
|
93
|
+
return withSnapshot(path, (d) => summaryRows(d, id))
|
|
94
|
+
} catch {
|
|
95
|
+
return new Map()
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export const antigravityReader: Reader = {
|
|
100
|
+
tool: TOOL,
|
|
101
|
+
label: LABEL,
|
|
102
|
+
async list(home) {
|
|
103
|
+
const found = new Map<string, SessionRef>()
|
|
104
|
+
for (const store of STORES) {
|
|
105
|
+
const dir = conversationsDir(home, store)
|
|
106
|
+
// Only the SQLite conversations: the IDE's <uuid>.pb files are encrypted.
|
|
107
|
+
const files = ls(dir).filter((n) => CONVERSATION_FILE.test(n))
|
|
108
|
+
if (!files.length) continue
|
|
109
|
+
const sums = readSummaries(summariesPath(home, store))
|
|
110
|
+
for (const name of files) {
|
|
111
|
+
const id = name.slice(0, -3)
|
|
112
|
+
const path = join(dir, name)
|
|
113
|
+
const s = sums.get(id)
|
|
114
|
+
const updatedAt = s?.updatedAt ?? mtime(path)
|
|
115
|
+
const ref: SessionRef = {
|
|
116
|
+
tool: TOOL,
|
|
117
|
+
sourceId: id,
|
|
118
|
+
path,
|
|
119
|
+
...(s?.title ? { title: s.title } : {}),
|
|
120
|
+
...(s?.cwd ? { cwd: s.cwd } : {}),
|
|
121
|
+
...(updatedAt ? { updatedAt } : {}),
|
|
122
|
+
extra: { store },
|
|
123
|
+
}
|
|
124
|
+
// The same conversation in two stores: the newer copy.
|
|
125
|
+
const prev = found.get(id)
|
|
126
|
+
if (!prev || (ref.updatedAt ?? "") > (prev.updatedAt ?? "")) found.set(id, ref)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return [...found.values()].sort((a, b) => (b.updatedAt ?? "").localeCompare(a.updatedAt ?? ""))
|
|
130
|
+
},
|
|
131
|
+
async read(ref) {
|
|
132
|
+
if (!exists(ref.path)) throw new Error(`Antigravity conversation not found: ${ref.path}`)
|
|
133
|
+
if (statSync(ref.path).size > MAX_DB_BYTES) throw new Error(`Antigravity conversation is larger than ${MAX_DB_BYTES} bytes: ${ref.path}`)
|
|
134
|
+
const id = basename(ref.path).replace(/\.db$/i, "")
|
|
135
|
+
const s = readSummaries(join(dirname(dirname(ref.path)), "conversation_summaries.db"), id).get(id)
|
|
136
|
+
const title = s?.title ?? ref.title
|
|
137
|
+
const cwd = s?.cwd ?? ref.cwd
|
|
138
|
+
const updatedAt = s?.updatedAt ?? ref.updatedAt
|
|
139
|
+
return withSnapshot(ref.path, (d) =>
|
|
140
|
+
readConversation(d, id, { path: ref.path, ...(title ? { title } : {}), ...(cwd ? { cwd } : {}), ...(updatedAt ? { updatedAt } : {}), ...(s?.parent ? { parent: s.parent } : {}) }),
|
|
141
|
+
)
|
|
142
|
+
},
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export interface Where {
|
|
146
|
+
path: string
|
|
147
|
+
title?: string
|
|
148
|
+
cwd?: string
|
|
149
|
+
updatedAt?: string
|
|
150
|
+
/** The conversation this one was spawned from, per the summaries table. */
|
|
151
|
+
parent?: string
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const stamp = (at: string | undefined) => (at ? { at } : {})
|
|
155
|
+
|
|
156
|
+
/** A nested message, or undefined when it is absent or not a message. */
|
|
157
|
+
function tryMessage(fields: Field[] | undefined, no: number): Field[] | undefined {
|
|
158
|
+
const b = fields && bytesOf(fields, no)
|
|
159
|
+
if (!b) return undefined
|
|
160
|
+
try {
|
|
161
|
+
return decode(b)
|
|
162
|
+
} catch {
|
|
163
|
+
return undefined
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function tryCall(b: Uint8Array | undefined): NativeCall | undefined {
|
|
168
|
+
if (!b) return undefined
|
|
169
|
+
try {
|
|
170
|
+
return decodeCall(b)
|
|
171
|
+
} catch {
|
|
172
|
+
return undefined
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Step types read as tool results by `builtin`. */
|
|
177
|
+
const BUILTIN: Record<number, true> = { [STEP.runCommand]: true, [STEP.viewFile]: true, [STEP.shellExec]: true }
|
|
178
|
+
|
|
179
|
+
/** A text from a built-in tool step: what it was given and what it returned. */
|
|
180
|
+
function builtin(type: number, body: Field[]): { name: string; input: Record<string, unknown>; output?: string } {
|
|
181
|
+
if (type === STEP.runCommand) {
|
|
182
|
+
const command = textOf(body, 23) || textOf(body, 25)
|
|
183
|
+
const out = tryMessage(body, 21) ?? tryMessage(body, 26)
|
|
184
|
+
const output = (out && textOf(out, 1)) || textOf(body, 4) || textOf(body, 5)
|
|
185
|
+
return { name: "run_command", input: command ? { command } : {}, ...(output !== undefined ? { output } : {}) }
|
|
186
|
+
}
|
|
187
|
+
if (type === STEP.viewFile) {
|
|
188
|
+
const path = textOf(body, 1)
|
|
189
|
+
const output = textOf(body, 4) || textOf(body, 9)
|
|
190
|
+
return { name: "view_file", input: path ? { AbsolutePath: path } : {}, ...(output !== undefined ? { output } : {}) }
|
|
191
|
+
}
|
|
192
|
+
const command = textOf(body, 1)
|
|
193
|
+
const output = textOf(body, 3) || textOf(body, 4)
|
|
194
|
+
return { name: "shell_exec", input: command ? { command } : {}, ...(output !== undefined ? { output } : {}) }
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** One conversation from an open database (a snapshot, or the writer's own before it is published). */
|
|
198
|
+
export function readConversation(d: Database, id: string, where: Where): Session {
|
|
199
|
+
const tables = new Set((d.query("SELECT name FROM sqlite_master WHERE type = 'table'").all() as Array<{ name: string }>).map((r) => r.name))
|
|
200
|
+
if (!tables.has("steps")) throw new Error(`not an Antigravity conversation database (no steps table): ${where.path}`)
|
|
201
|
+
const count = (d.query("SELECT count(*) AS n FROM steps").get() as { n: number }).n
|
|
202
|
+
if (count > MAX_STEPS) throw new Error(`Antigravity conversation has ${count} steps, more than ${MAX_STEPS}: ${where.path}`)
|
|
203
|
+
|
|
204
|
+
const lost = lossCounter()
|
|
205
|
+
const turns: Turn[] = []
|
|
206
|
+
|
|
207
|
+
// Subagent and battle trajectories are stored beside the conversation's own and are not read.
|
|
208
|
+
if (tables.has("trajectory_meta")) {
|
|
209
|
+
const n = (d.query("SELECT count(*) AS n FROM trajectory_meta").get() as { n: number }).n
|
|
210
|
+
if (n > 1) lost.add("auxiliary trajectories (subagents, battles)", n - 1)
|
|
211
|
+
}
|
|
212
|
+
if (tables.has("battle_mode_infos")) {
|
|
213
|
+
const n = (d.query("SELECT count(*) AS n FROM battle_mode_infos").get() as { n: number }).n
|
|
214
|
+
if (n) lost.add("battle mode records", n)
|
|
215
|
+
}
|
|
216
|
+
let startedAt: string | undefined
|
|
217
|
+
let metaCwd: string | undefined
|
|
218
|
+
if (tables.has("trajectory_metadata_blob")) {
|
|
219
|
+
const row = d.query("SELECT data FROM trajectory_metadata_blob WHERE id = 'main'").get() as { data: unknown } | null
|
|
220
|
+
if (row?.data instanceof Uint8Array)
|
|
221
|
+
try {
|
|
222
|
+
const m = decode(row.data)
|
|
223
|
+
startedAt = timestampOf(tryMessage(m, 2))
|
|
224
|
+
metaCwd = pathOfUri(workspaceOf(m))
|
|
225
|
+
} catch {
|
|
226
|
+
// Metadata that does not decode only costs the start time and folder.
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Calls waiting for their result: the id the store gave them, and the id they have here.
|
|
231
|
+
const pending: Array<{ native: string; id: string }> = []
|
|
232
|
+
const used = new Set<string>()
|
|
233
|
+
let continued: string | undefined
|
|
234
|
+
let firstAt: string | undefined
|
|
235
|
+
let lastAt: string | undefined
|
|
236
|
+
|
|
237
|
+
const parseArgs = (args: string): unknown => {
|
|
238
|
+
if (!args.trim()) return {}
|
|
239
|
+
try {
|
|
240
|
+
return JSON.parse(args)
|
|
241
|
+
} catch {
|
|
242
|
+
lost.add("unreadable tool arguments")
|
|
243
|
+
return { arguments: args }
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
const call = (at: string | undefined, native: string, name: string, input: unknown, kind = antigravityToolKind(name)) => {
|
|
247
|
+
let callId = native
|
|
248
|
+
for (let k = 2; !callId || used.has(callId); k++) callId = `${native || "call"}#${k}`
|
|
249
|
+
used.add(callId)
|
|
250
|
+
turns.push({ kind: "tool_call", ...stamp(at), id: callId, name, toolKind: kind, input })
|
|
251
|
+
return callId
|
|
252
|
+
}
|
|
253
|
+
/** The pending call a result answers: the linked one when the step names it, else the oldest. */
|
|
254
|
+
const take = (link: string | undefined) => {
|
|
255
|
+
if (link === undefined) return pending.shift()
|
|
256
|
+
const i = pending.findIndex((p) => p.native === link)
|
|
257
|
+
return i >= 0 ? pending.splice(i, 1)[0] : undefined
|
|
258
|
+
}
|
|
259
|
+
const result = (at: string | undefined, link: Partial<NativeCall> | undefined, fallback: { name: string; input: unknown }, ok: boolean, output: string) => {
|
|
260
|
+
let callId = take(link?.id)?.id
|
|
261
|
+
if (!callId) {
|
|
262
|
+
// A result whose call was not recorded (or not readable): the call is rebuilt from what the step says.
|
|
263
|
+
callId = call(at, link?.id ?? "", link?.name ?? fallback.name, link?.args !== undefined ? parseArgs(link.args) : fallback.input)
|
|
264
|
+
lost.add("tool calls rebuilt from their results")
|
|
265
|
+
}
|
|
266
|
+
const c = clip(output)
|
|
267
|
+
turns.push({ kind: "tool_result", ...stamp(at), callId, ok, output: c.text, ...(c.truncated ? { truncated: true } : {}) })
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const rows = d.query("SELECT idx, step_type, status, has_subtrajectory, metadata, step_payload, step_format FROM steps ORDER BY idx").all() as Array<Record<string, unknown>>
|
|
271
|
+
for (const r of rows) {
|
|
272
|
+
const type = Number(r.step_type)
|
|
273
|
+
const status = Number(r.status)
|
|
274
|
+
if (Number(r.step_format ?? 0) !== 0) {
|
|
275
|
+
lost.add("Antigravity steps in an unknown encoding")
|
|
276
|
+
continue
|
|
277
|
+
}
|
|
278
|
+
if (!(r.step_payload instanceof Uint8Array)) {
|
|
279
|
+
lost.add("unreadable Antigravity steps")
|
|
280
|
+
continue
|
|
281
|
+
}
|
|
282
|
+
let outer: Field[]
|
|
283
|
+
let body: Field[] | undefined
|
|
284
|
+
try {
|
|
285
|
+
outer = decode(r.step_payload)
|
|
286
|
+
// The row and the serialised step say the same type and status, or the step is not trusted.
|
|
287
|
+
const t = numberOf(outer, 1)
|
|
288
|
+
const s = numberOf(outer, 4)
|
|
289
|
+
if ((t !== undefined && t !== type) || (s !== undefined && s !== status)) throw new Error("step row and payload disagree")
|
|
290
|
+
const field = PAYLOAD_FIELD[type]
|
|
291
|
+
const b = field === undefined ? undefined : bytesOf(outer, field)
|
|
292
|
+
body = b ? decode(b) : undefined
|
|
293
|
+
} catch {
|
|
294
|
+
lost.add("unreadable Antigravity steps")
|
|
295
|
+
continue
|
|
296
|
+
}
|
|
297
|
+
if (Number(r.has_subtrajectory) > 0) lost.add("subtrajectories under steps")
|
|
298
|
+
let meta: Field[] | undefined
|
|
299
|
+
try {
|
|
300
|
+
meta = r.metadata instanceof Uint8Array ? decode(r.metadata) : tryMessage(outer, 5)
|
|
301
|
+
} catch {
|
|
302
|
+
meta = undefined
|
|
303
|
+
}
|
|
304
|
+
const at = timestampOf(tryMessage(meta, 1))
|
|
305
|
+
if (at) {
|
|
306
|
+
firstAt ??= at
|
|
307
|
+
lastAt = at
|
|
308
|
+
}
|
|
309
|
+
// A result step's metadata names the call it answers.
|
|
310
|
+
const linked = tryCall(meta && bytesOf(meta, 4))
|
|
311
|
+
|
|
312
|
+
if (!body && (type === STEP.userInput || type === STEP.plannerResponse || type === STEP.generic || type in BUILTIN || type === STEP.mcpTool)) {
|
|
313
|
+
lost.add("unreadable Antigravity steps")
|
|
314
|
+
continue
|
|
315
|
+
}
|
|
316
|
+
switch (type) {
|
|
317
|
+
case STEP.userInput: {
|
|
318
|
+
let text = textOf(body!, 2) || textOf(body!, 1) || ""
|
|
319
|
+
// The note brain puts before a continued session's first request says where it came from.
|
|
320
|
+
const note = CONTINUED_NOTE.exec(text)
|
|
321
|
+
if (note) {
|
|
322
|
+
continued ??= note[1]
|
|
323
|
+
text = text.replace(/^\[[^\n]*\]\n*/, "")
|
|
324
|
+
}
|
|
325
|
+
const context = allBytes(body!, 3).length + allBytes(body!, 9).length
|
|
326
|
+
if (context) lost.add("context attached to user messages", context)
|
|
327
|
+
if (text.trim()) turns.push({ kind: "user", ...stamp(at), text })
|
|
328
|
+
break
|
|
329
|
+
}
|
|
330
|
+
case STEP.plannerResponse: {
|
|
331
|
+
const thinking = hasThinking(body!)
|
|
332
|
+
if (thinking) lost.add("private reasoning")
|
|
333
|
+
const text = textOf(body!, 1) ?? ""
|
|
334
|
+
if (text.trim()) turns.push({ kind: "assistant", ...stamp(at), text })
|
|
335
|
+
const calls = allBytes(body!, 7)
|
|
336
|
+
for (const b of calls) {
|
|
337
|
+
const c = tryCall(b)
|
|
338
|
+
if (!c) {
|
|
339
|
+
lost.add("unreadable tool calls")
|
|
340
|
+
continue
|
|
341
|
+
}
|
|
342
|
+
pending.push({ native: c.id, id: call(at, c.id, c.name, parseArgs(c.args)) })
|
|
343
|
+
}
|
|
344
|
+
if (!text.trim() && !calls.length && !thinking) lost.add("Antigravity replies with nothing readable")
|
|
345
|
+
break
|
|
346
|
+
}
|
|
347
|
+
case STEP.generic: {
|
|
348
|
+
const res = tryMessage(body, 2)
|
|
349
|
+
if (!res) {
|
|
350
|
+
lost.add("unreadable Antigravity steps")
|
|
351
|
+
break
|
|
352
|
+
}
|
|
353
|
+
const key = entriesOf(res, 2).find(([k]) => k === LINK_KEY)?.[1]
|
|
354
|
+
const args = Object.fromEntries(
|
|
355
|
+
entriesOf(body!, 1).map(([k, v]) => {
|
|
356
|
+
try {
|
|
357
|
+
return [k, JSON.parse(v)]
|
|
358
|
+
} catch {
|
|
359
|
+
return [k, v]
|
|
360
|
+
}
|
|
361
|
+
}),
|
|
362
|
+
)
|
|
363
|
+
result(at, key ? { id: key } : linked, { name: "generic", input: args }, status !== STATUS.error, textOf(res, 1) ?? "")
|
|
364
|
+
break
|
|
365
|
+
}
|
|
366
|
+
case STEP.mcpTool: {
|
|
367
|
+
const c = tryCall(bytesOf(body!, 2))
|
|
368
|
+
let callId: string | undefined
|
|
369
|
+
if (c) {
|
|
370
|
+
const i = pending.findIndex((p) => p.native === c.id)
|
|
371
|
+
callId = i >= 0 ? pending.splice(i, 1)[0]!.id : call(at, c.id, c.name, parseArgs(c.args), "mcp")
|
|
372
|
+
}
|
|
373
|
+
const output = textOf(body!, 3) ?? textOf(body!, 8)
|
|
374
|
+
if (output === undefined) {
|
|
375
|
+
lost.add("MCP steps without a readable result")
|
|
376
|
+
break
|
|
377
|
+
}
|
|
378
|
+
callId ??= call(at, "", "mcp_tool", {}, "mcp")
|
|
379
|
+
const o = clip(output)
|
|
380
|
+
turns.push({ kind: "tool_result", ...stamp(at), callId, ok: status !== STATUS.error, output: o.text, ...(o.truncated ? { truncated: true } : {}) })
|
|
381
|
+
break
|
|
382
|
+
}
|
|
383
|
+
case STEP.runCommand:
|
|
384
|
+
case STEP.viewFile:
|
|
385
|
+
case STEP.shellExec: {
|
|
386
|
+
const b = builtin(type, body!)
|
|
387
|
+
result(at, linked, b, status !== STATUS.error, b.output ?? "")
|
|
388
|
+
break
|
|
389
|
+
}
|
|
390
|
+
default: {
|
|
391
|
+
lost.add(`antigravity step type ${type}`)
|
|
392
|
+
// A step brain cannot read that answers a pending call still closes it, so the call is not left open.
|
|
393
|
+
const i = linked ? pending.findIndex((p) => p.native === linked.id) : -1
|
|
394
|
+
if (i >= 0)
|
|
395
|
+
turns.push({
|
|
396
|
+
kind: "tool_result",
|
|
397
|
+
...stamp(at),
|
|
398
|
+
callId: pending.splice(i, 1)[0]!.id,
|
|
399
|
+
ok: status !== STATUS.error,
|
|
400
|
+
output: `[Antigravity kept this result in a step type brain cannot read (${type}).]`,
|
|
401
|
+
})
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const cwd = where.cwd ?? metaCwd
|
|
407
|
+
const started = startedAt ?? firstAt
|
|
408
|
+
const updated = where.updatedAt ?? lastAt
|
|
409
|
+
const parent: SessionHeader["parent"] = continued
|
|
410
|
+
? { id: continued, kind: "continued" }
|
|
411
|
+
: where.parent
|
|
412
|
+
? { id: `${TOOL}:${where.parent}`, kind: "subagent" }
|
|
413
|
+
: undefined
|
|
414
|
+
const h = header(TOOL, id, {
|
|
415
|
+
sourcePath: where.path,
|
|
416
|
+
...(where.title ? { title: where.title } : {}),
|
|
417
|
+
project: { ...(cwd ? { cwd } : {}), ...gitInfo(cwd) },
|
|
418
|
+
...(started ? { startedAt: started } : {}),
|
|
419
|
+
...(updated ? { updatedAt: updated } : {}),
|
|
420
|
+
...(parent ? { parent } : {}),
|
|
421
|
+
})
|
|
422
|
+
lost.flush(h)
|
|
423
|
+
return { header: h, turns }
|
|
424
|
+
}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { join } from "node:path"
|
|
2
|
+
import type { ToolKind } from "../../session/types.ts"
|
|
3
|
+
import { toolKindOf } from "../../session/util.ts"
|
|
4
|
+
import { allBytes, asText, concat, decode, messageField, numberOf, textField, textOf, varintField, type Field } from "./proto.ts"
|
|
5
|
+
|
|
6
|
+
// Antigravity (Google): where its conversations are and what their rows look like. Ported from the clean-room
|
|
7
|
+
// format notes and reference adapter of session-migrate (MIT, xhluca: docs/antigravity-format.md,
|
|
8
|
+
// src/session_migrate/formats/antigravity.py), which pin Antigravity CLI 1.1.16, and checked against the real
|
|
9
|
+
// 1.1.16 capture in test/fixtures/native-corpus/antigravity.
|
|
10
|
+
//
|
|
11
|
+
// Every conversation is its own SQLite database, ~/.gemini/<store>/conversations/<uuid>.db, with seven tables:
|
|
12
|
+
// trajectory_meta (the conversation's trajectory: type 4 cascade, source 17 for the CLI), steps (one row per
|
|
13
|
+
// step, idx 0..n-1, each with its type and status both as columns and inside the serialised Step protobuf),
|
|
14
|
+
// trajectory_metadata_blob (one "main" row), and four runtime tables. Beside the folder,
|
|
15
|
+
// conversation_summaries.db is the picker's index: title, preview, workspace, times. The CLI keeps its store in
|
|
16
|
+
// antigravity-cli; the IDE uses antigravity and antigravity-ide, the same layout, a wider summaries table, and
|
|
17
|
+
// encrypted <uuid>.pb files that nothing here reads.
|
|
18
|
+
//
|
|
19
|
+
// A Step carries its type in field 1, its status in field 4 (3 done, 7 error), its metadata (times, the tool
|
|
20
|
+
// call a result answers) in field 5, and its body in a field that depends on the type (PAYLOAD_FIELD). A user
|
|
21
|
+
// input keeps the person's text in field 2; a planner response keeps the reply in field 1, a message id in
|
|
22
|
+
// field 6 and its tool calls (id 1, name 2, JSON arguments 3) in repeated field 7, and private thinking in
|
|
23
|
+
// fields 3, 4, 14 and 16; a generic tool step keeps the arguments as {key, JSON value} entries in repeated field
|
|
24
|
+
// 1 and the result in field 2 (text in its field 1, {key, value} metadata in its repeated field 2).
|
|
25
|
+
|
|
26
|
+
export const TOOL = "antigravity"
|
|
27
|
+
export const LABEL = "Antigravity"
|
|
28
|
+
/** Antigravity CLI versions whose store the writer was checked against (by session-migrate's live checks). */
|
|
29
|
+
export const TESTED_VERSIONS = ["1.1.16"]
|
|
30
|
+
|
|
31
|
+
/** The CLI's store, then the IDE's two. */
|
|
32
|
+
export const STORES = ["antigravity-cli", "antigravity", "antigravity-ide"] as const
|
|
33
|
+
export type Store = (typeof STORES)[number]
|
|
34
|
+
export const CLI_STORE: Store = "antigravity-cli"
|
|
35
|
+
|
|
36
|
+
export const storeDir = (home: string, store: Store) => join(home, ".gemini", store)
|
|
37
|
+
export const conversationsDir = (home: string, store: Store) => join(storeDir(home, store), "conversations")
|
|
38
|
+
export const summariesPath = (home: string, store: Store) => join(storeDir(home, store), "conversation_summaries.db")
|
|
39
|
+
|
|
40
|
+
/** A conversation database's file name: its UUID and .db. */
|
|
41
|
+
export const CONVERSATION_FILE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.db$/i
|
|
42
|
+
|
|
43
|
+
export const STEP = {
|
|
44
|
+
viewFile: 8,
|
|
45
|
+
listDirectory: 9,
|
|
46
|
+
userInput: 14,
|
|
47
|
+
plannerResponse: 15,
|
|
48
|
+
runCommand: 21,
|
|
49
|
+
checkpoint: 23,
|
|
50
|
+
mcpTool: 38,
|
|
51
|
+
fileChange: 86,
|
|
52
|
+
conversationHistory: 98,
|
|
53
|
+
shellExec: 112,
|
|
54
|
+
writeBlob: 128,
|
|
55
|
+
generic: 132,
|
|
56
|
+
} as const
|
|
57
|
+
|
|
58
|
+
/** The Step field holding each type's body, as observed in 1.1.16. */
|
|
59
|
+
export const PAYLOAD_FIELD: Record<number, number> = { 8: 14, 9: 15, 14: 19, 15: 20, 21: 28, 23: 30, 38: 47, 86: 98, 98: 111, 112: 127, 128: 142, 132: 140 }
|
|
60
|
+
|
|
61
|
+
export const STATUS = { done: 3, error: 7 } as const
|
|
62
|
+
export const TRAJECTORY = { cascade: 4, cli: 17 } as const
|
|
63
|
+
/** The project the CLI files conversations under when it has no other. */
|
|
64
|
+
export const PROJECT_ID = "default-cli-project"
|
|
65
|
+
/** The generic result's metadata key linking it to its call (session-migrate's, kept for interoperability). */
|
|
66
|
+
export const LINK_KEY = "session_migrate_call_id"
|
|
67
|
+
|
|
68
|
+
/** Bounds on what a reader accepts from a store. */
|
|
69
|
+
export const MAX_DB_BYTES = 256 * 1024 * 1024
|
|
70
|
+
export const MAX_STEPS = 100_000
|
|
71
|
+
|
|
72
|
+
/** The conversation database as the 1.1.16 CLI creates it (from the capture's sqlite_master). */
|
|
73
|
+
export const SCHEMA = `
|
|
74
|
+
CREATE TABLE \`trajectory_meta\` (\`trajectory_id\` text,\`cascade_id\` text,\`trajectory_type\` integer,\`source\` integer,PRIMARY KEY (\`trajectory_id\`));
|
|
75
|
+
CREATE TABLE \`steps\` (\`idx\` integer,\`step_type\` integer NOT NULL DEFAULT 0,\`status\` integer NOT NULL DEFAULT 0,\`has_subtrajectory\` numeric NOT NULL DEFAULT false,\`metadata\` blob,\`error_details\` blob,\`permissions\` blob,\`task_details\` blob,\`render_info\` blob,\`step_payload\` blob,\`step_format\` integer NOT NULL DEFAULT 0,PRIMARY KEY (\`idx\`));
|
|
76
|
+
CREATE TABLE \`gen_metadata\` (\`idx\` integer,\`data\` blob,\`size\` integer NOT NULL DEFAULT 0,PRIMARY KEY (\`idx\`));
|
|
77
|
+
CREATE TABLE \`executor_metadata\` (\`idx\` integer,\`data\` blob,PRIMARY KEY (\`idx\`));
|
|
78
|
+
CREATE TABLE \`parent_references\` (\`idx\` integer,\`data\` blob,PRIMARY KEY (\`idx\`));
|
|
79
|
+
CREATE TABLE \`trajectory_metadata_blob\` (\`id\` text DEFAULT "main",\`data\` blob,PRIMARY KEY (\`id\`));
|
|
80
|
+
CREATE TABLE \`battle_mode_infos\` (\`idx\` integer,\`data\` blob,PRIMARY KEY (\`idx\`));
|
|
81
|
+
CREATE INDEX \`idx_steps_status\` ON \`steps\`(\`status\`);
|
|
82
|
+
CREATE INDEX \`idx_steps_step_type\` ON \`steps\`(\`step_type\`);
|
|
83
|
+
`
|
|
84
|
+
|
|
85
|
+
/** The picker index as 1.1.16 creates it. */
|
|
86
|
+
export const SUMMARY_SCHEMA = `
|
|
87
|
+
CREATE TABLE \`conversation_summaries\` (\`conversation_id\` text,\`title\` text NOT NULL DEFAULT "",\`preview\` text NOT NULL DEFAULT "",\`step_count\` integer NOT NULL DEFAULT 0,\`last_modified_time\` datetime NOT NULL,\`workspace_uris\` text NOT NULL,\`status\` text NOT NULL DEFAULT "",\`source\` text NOT NULL DEFAULT "",\`project_id\` text NOT NULL DEFAULT "",\`agent_name\` text NOT NULL DEFAULT "",\`parent_conversation_id\` text NOT NULL DEFAULT "",\`nesting_depth\` integer NOT NULL DEFAULT 0,\`battle_id\` text NOT NULL DEFAULT "",\`winning_conversation_id\` text NOT NULL DEFAULT "",\`not_fully_idle\` numeric NOT NULL DEFAULT false,\`killed\` numeric NOT NULL DEFAULT false,\`last_user_input_time\` datetime NOT NULL,\`last_user_input_step_index\` integer NOT NULL DEFAULT -1,\`app_data_dir\` text NOT NULL DEFAULT "",PRIMARY KEY (\`conversation_id\`));
|
|
88
|
+
CREATE INDEX \`idx_conversation_summaries_last_user_input_time\` ON \`conversation_summaries\`(\`last_user_input_time\`);
|
|
89
|
+
CREATE INDEX \`idx_conversation_summaries_last_modified_time\` ON \`conversation_summaries\`(\`last_modified_time\`);
|
|
90
|
+
`
|
|
91
|
+
|
|
92
|
+
/** The summary columns 1.1.16 has, all of which the writer fills. Newer builds and the IDE add more. */
|
|
93
|
+
export const SUMMARY_COLUMNS = [
|
|
94
|
+
"conversation_id",
|
|
95
|
+
"title",
|
|
96
|
+
"preview",
|
|
97
|
+
"step_count",
|
|
98
|
+
"last_modified_time",
|
|
99
|
+
"workspace_uris",
|
|
100
|
+
"status",
|
|
101
|
+
"source",
|
|
102
|
+
"project_id",
|
|
103
|
+
"agent_name",
|
|
104
|
+
"parent_conversation_id",
|
|
105
|
+
"nesting_depth",
|
|
106
|
+
"battle_id",
|
|
107
|
+
"winning_conversation_id",
|
|
108
|
+
"not_fully_idle",
|
|
109
|
+
"killed",
|
|
110
|
+
"last_user_input_time",
|
|
111
|
+
"last_user_input_step_index",
|
|
112
|
+
"app_data_dir",
|
|
113
|
+
] as const
|
|
114
|
+
|
|
115
|
+
/** A google.protobuf.Timestamp ({1: seconds, 2: nanos}) as ISO time. */
|
|
116
|
+
export function timestampOf(fields: Field[] | undefined): string | undefined {
|
|
117
|
+
const seconds = fields && numberOf(fields, 1)
|
|
118
|
+
if (!seconds || seconds <= 0) return undefined
|
|
119
|
+
const nanos = numberOf(fields!, 2) ?? 0
|
|
120
|
+
const ms = seconds * 1000 + Math.floor(nanos / 1e6)
|
|
121
|
+
return Number.isFinite(ms) && ms < 8.64e15 ? new Date(ms).toISOString() : undefined
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** A summaries datetime ("2026-09-01 00:08:05.04913412+00:00", as Go writes it) as ISO time; Go's zero time is none. */
|
|
125
|
+
export function sqlTime(v: unknown): string | undefined {
|
|
126
|
+
if (typeof v !== "string") return undefined
|
|
127
|
+
const m = /^(\d{4}-\d\d-\d\d)[ T](\d\d:\d\d:\d\d)(?:\.(\d+))?\s*(Z|[+-]\d\d:?\d\d)?$/.exec(v.trim())
|
|
128
|
+
if (!m || m[1]!.startsWith("0001-")) return undefined
|
|
129
|
+
const frac = (m[3] ?? "").slice(0, 3).padEnd(3, "0")
|
|
130
|
+
const t = Date.parse(`${m[1]}T${m[2]}.${frac}${m[4] ?? "Z"}`)
|
|
131
|
+
return Number.isFinite(t) ? new Date(t).toISOString() : undefined
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** A time as the summaries table stores it. */
|
|
135
|
+
export const toSqlTime = (d: Date) => `${d.toISOString().slice(0, 10)} ${d.toISOString().slice(11, 23)}+00:00`
|
|
136
|
+
|
|
137
|
+
/** A tool call as a planner response stores it. */
|
|
138
|
+
export interface NativeCall {
|
|
139
|
+
id: string
|
|
140
|
+
name: string
|
|
141
|
+
/** The arguments JSON as stored. */
|
|
142
|
+
args: string
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function decodeCall(b: Uint8Array): NativeCall | undefined {
|
|
146
|
+
const f = decode(b)
|
|
147
|
+
const id = textOf(f, 1)
|
|
148
|
+
const name = textOf(f, 2)
|
|
149
|
+
if (!id || !name) return undefined
|
|
150
|
+
return { id, name, args: textOf(f, 3) ?? "" }
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Whether a planner response holds private thinking (visible or raw thinking, a signature, or the thinking flag). */
|
|
154
|
+
export const hasThinking = (planner: Field[]) => [3, 4, 14, 16].some((n) => allBytes(planner, n).length > 0) || numberOf(planner, 5) === 1
|
|
155
|
+
|
|
156
|
+
/** The {key, value} entries of a repeated map field. */
|
|
157
|
+
export function entriesOf(fields: Field[], no: number): Array<[string, string]> {
|
|
158
|
+
const out: Array<[string, string]> = []
|
|
159
|
+
for (const e of allBytes(fields, no)) {
|
|
160
|
+
try {
|
|
161
|
+
const f = decode(e)
|
|
162
|
+
const k = textOf(f, 1)
|
|
163
|
+
if (k !== undefined) out.push([k, textOf(f, 2) ?? ""])
|
|
164
|
+
} catch {
|
|
165
|
+
// An entry that is not a {key, value} message is skipped.
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return out
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** What Antigravity's own tools do; other names go through the shared guess. */
|
|
172
|
+
const KINDS: Record<string, ToolKind> = {
|
|
173
|
+
view_file: "read",
|
|
174
|
+
view_file_outline: "read",
|
|
175
|
+
view_code_item: "read",
|
|
176
|
+
list_dir: "list",
|
|
177
|
+
find_by_name: "search",
|
|
178
|
+
grep_search: "search",
|
|
179
|
+
codebase_search: "search",
|
|
180
|
+
run_command: "shell",
|
|
181
|
+
command_status: "shell",
|
|
182
|
+
send_command_input: "shell",
|
|
183
|
+
shell_exec: "shell",
|
|
184
|
+
write_to_file: "write",
|
|
185
|
+
replace_file_content: "edit",
|
|
186
|
+
multi_replace_file_content: "edit",
|
|
187
|
+
read_url_content: "web",
|
|
188
|
+
search_web: "web",
|
|
189
|
+
}
|
|
190
|
+
export const antigravityToolKind = (name: string): ToolKind => KINDS[name] ?? toolKindOf(name)
|
|
191
|
+
|
|
192
|
+
// Encoding the four messages the writer emits.
|
|
193
|
+
|
|
194
|
+
const uuidText = (no: number, id: string) => textField(no, id)
|
|
195
|
+
|
|
196
|
+
/** A whole Step: type, status, and the body in its type's field. */
|
|
197
|
+
export function stepBytes(type: number, status: number, body: Uint8Array): Uint8Array {
|
|
198
|
+
const field = PAYLOAD_FIELD[type]
|
|
199
|
+
if (field === undefined) throw new Error(`no payload field is known for Antigravity step type ${type}`)
|
|
200
|
+
return concat([varintField(1, type), varintField(4, status), messageField(field, [body])])
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export const userInputBody = (text: string) => textField(2, text)
|
|
204
|
+
|
|
205
|
+
export function plannerBody(o: { text?: string; messageId: string; calls?: NativeCall[] }): Uint8Array {
|
|
206
|
+
return concat([
|
|
207
|
+
...(o.text ? [textField(1, o.text)] : []),
|
|
208
|
+
uuidText(6, o.messageId),
|
|
209
|
+
...(o.calls ?? []).map((c) => messageField(7, [textField(1, c.id), textField(2, c.name), textField(3, c.args)])),
|
|
210
|
+
])
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function genericBody(o: { args: Array<[string, string]>; result: string; callId: string }): Uint8Array {
|
|
214
|
+
return concat([
|
|
215
|
+
...o.args.map(([k, v]) => messageField(1, [textField(1, k), textField(2, v)])),
|
|
216
|
+
messageField(2, [textField(1, o.result), messageField(2, [textField(1, LINK_KEY), textField(2, o.callId)])]),
|
|
217
|
+
])
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** The minimal "main" trajectory metadata: creation time, root conversation id, project. */
|
|
221
|
+
export const trajectoryMetadata = (seconds: number, conversationId: string) =>
|
|
222
|
+
concat([messageField(2, [varintField(1, seconds)]), textField(6, conversationId), textField(18, PROJECT_ID)])
|
|
223
|
+
|
|
224
|
+
/** The first workspace folder in trajectory metadata (field 1's folder URI, else field 7). */
|
|
225
|
+
export function workspaceOf(meta: Field[]): string | undefined {
|
|
226
|
+
for (const w of allBytes(meta, 1)) {
|
|
227
|
+
try {
|
|
228
|
+
const uri = textOf(decode(w), 1)
|
|
229
|
+
if (uri) return uri
|
|
230
|
+
} catch {
|
|
231
|
+
// not a workspace message
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
const first = allBytes(meta, 7)[0]
|
|
235
|
+
return first ? asText(first) : undefined
|
|
236
|
+
}
|