@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,463 @@
|
|
|
1
|
+
import { Database } from "bun:sqlite"
|
|
2
|
+
import { randomBytes, randomUUID } from "node:crypto"
|
|
3
|
+
import { linkSync, lstatSync, mkdirSync, statSync, unlinkSync, writeFileSync } from "node:fs"
|
|
4
|
+
import { join, resolve } from "node:path"
|
|
5
|
+
import { pathToFileURL } from "node:url"
|
|
6
|
+
import { toolLabel } from "../../brief.ts"
|
|
7
|
+
import { exists, isDir } from "../../fsutil.ts"
|
|
8
|
+
import { withSnapshot } from "../../sqlite.ts"
|
|
9
|
+
import type { Session } from "../../session/types.ts"
|
|
10
|
+
import { countTurns, firstUserText, unpaired } from "../../session/util.ts"
|
|
11
|
+
import { continuedNote } from "../claude-code/records.ts"
|
|
12
|
+
import { continuedTitle, sh } from "../opencode/writer.ts"
|
|
13
|
+
import type { Readiness, WriteOptions, WriteResult, Writer } from "../types.ts"
|
|
14
|
+
import { readConversation } from "./reader.ts"
|
|
15
|
+
import {
|
|
16
|
+
CLI_STORE,
|
|
17
|
+
LABEL,
|
|
18
|
+
PROJECT_ID,
|
|
19
|
+
SCHEMA,
|
|
20
|
+
STATUS,
|
|
21
|
+
STEP,
|
|
22
|
+
SUMMARY_COLUMNS,
|
|
23
|
+
SUMMARY_SCHEMA,
|
|
24
|
+
TESTED_VERSIONS,
|
|
25
|
+
TOOL,
|
|
26
|
+
TRAJECTORY,
|
|
27
|
+
conversationsDir,
|
|
28
|
+
genericBody,
|
|
29
|
+
plannerBody,
|
|
30
|
+
stepBytes,
|
|
31
|
+
summariesPath,
|
|
32
|
+
toSqlTime,
|
|
33
|
+
trajectoryMetadata,
|
|
34
|
+
userInputBody,
|
|
35
|
+
type NativeCall,
|
|
36
|
+
} from "./records.ts"
|
|
37
|
+
|
|
38
|
+
// Continues a portable session in the Antigravity CLI as a new conversation of its own store, built the way
|
|
39
|
+
// session-migrate's clean-room writer builds one for CLI 1.1.16 (docs/antigravity-format.md, "Conversion
|
|
40
|
+
// policy"): a done user-input step per user turn, a done planner-response step per reply, the tool calls of one
|
|
41
|
+
// response together in one planner step, and a generic tool step after it per result (done, or error when the
|
|
42
|
+
// call failed), linked to its call by id. The tools keep their own names and JSON arguments: they are not
|
|
43
|
+
// passed off as Antigravity's built-in tools. Private reasoning, compaction summaries, attachments and the
|
|
44
|
+
// runtime tables are not written. The note saying where the session came from opens the first request.
|
|
45
|
+
//
|
|
46
|
+
// The new database is built in memory, read back with the reader, and only then published under
|
|
47
|
+
// ~/.gemini/antigravity-cli/conversations/<uuid>.db with create-if-absent semantics (a hard link), while an
|
|
48
|
+
// immediate transaction on conversation_summaries.db reserves the id and adds the picker's row; a failure
|
|
49
|
+
// removes only the new file. An existing conversation is never overwritten or merged.
|
|
50
|
+
|
|
51
|
+
const NO_RESULT = "No result was recorded for this call; the session moved before it finished."
|
|
52
|
+
|
|
53
|
+
export interface PlannedStep {
|
|
54
|
+
type: number
|
|
55
|
+
status: number
|
|
56
|
+
payload: Uint8Array
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface AntigravityPlan {
|
|
60
|
+
id: string
|
|
61
|
+
trajectoryId: string
|
|
62
|
+
steps: PlannedStep[]
|
|
63
|
+
title: string
|
|
64
|
+
preview: string
|
|
65
|
+
/** Index of the last user-input step. */
|
|
66
|
+
lastUserStep: number
|
|
67
|
+
lost: string[]
|
|
68
|
+
/** What a read-back must find. */
|
|
69
|
+
expect: { users: number; assistants: number; calls: number; results: number }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const isObject = (v: unknown): v is Record<string, unknown> => !!v && typeof v === "object" && !Array.isArray(v)
|
|
73
|
+
|
|
74
|
+
/** The steps of a new conversation `opts.id` continuing `session`. Pure: writes nothing. */
|
|
75
|
+
export function planAntigravity(session: Session, opts: { id: string; trajectoryId?: string; now: Date }): AntigravityPlan {
|
|
76
|
+
const drops = new Map<string, number>()
|
|
77
|
+
const drop = (what: string, n = 1) => drops.set(what, (drops.get(what) ?? 0) + n)
|
|
78
|
+
const steps: PlannedStep[] = []
|
|
79
|
+
const expect = { users: 0, assistants: 0, calls: 0, results: 0 }
|
|
80
|
+
const push = (type: number, status: number, body: Uint8Array) => steps.push({ type, status, payload: stepBytes(type, status, body) })
|
|
81
|
+
|
|
82
|
+
const note = continuedNote(toolLabel(session.header.tool), session.header.id, opts.now.toISOString().slice(0, 10))
|
|
83
|
+
let noted = false
|
|
84
|
+
const userStep = (text: string) => {
|
|
85
|
+
push(STEP.userInput, STATUS.done, userInputBody(noted ? text : text ? `${note}\n\n${text}` : note))
|
|
86
|
+
noted = true
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Calls of the response being built, and calls still waiting for a result (by the source's id, in order).
|
|
90
|
+
let open: NativeCall[] = []
|
|
91
|
+
const pending: Array<{ src: string; call: NativeCall; input: Record<string, unknown> }> = []
|
|
92
|
+
const used = new Set<string>()
|
|
93
|
+
let made = 0
|
|
94
|
+
const freshId = () => {
|
|
95
|
+
let id = ""
|
|
96
|
+
do id = `call_brain_${++made}`
|
|
97
|
+
while (used.has(id))
|
|
98
|
+
return id
|
|
99
|
+
}
|
|
100
|
+
const flushCalls = () => {
|
|
101
|
+
if (!open.length) return
|
|
102
|
+
if (!noted) userStep("")
|
|
103
|
+
push(STEP.plannerResponse, STATUS.done, plannerBody({ messageId: randomUUID(), calls: open }))
|
|
104
|
+
open = []
|
|
105
|
+
}
|
|
106
|
+
const resultStep = (call: NativeCall, input: Record<string, unknown>, ok: boolean, output: string) => {
|
|
107
|
+
const args = Object.entries(input)
|
|
108
|
+
.filter(([k]) => k)
|
|
109
|
+
.map(([k, v]): [string, string] => [k, JSON.stringify(v) ?? "null"])
|
|
110
|
+
push(STEP.generic, ok ? STATUS.done : STATUS.error, genericBody({ args, result: output, callId: call.id }))
|
|
111
|
+
expect.results++
|
|
112
|
+
}
|
|
113
|
+
// Calls still open when the person speaks again are closed as failed, so no call is left without a result.
|
|
114
|
+
const closeDangling = () => {
|
|
115
|
+
flushCalls()
|
|
116
|
+
for (const p of pending.splice(0)) {
|
|
117
|
+
resultStep(p.call, p.input, false, NO_RESULT)
|
|
118
|
+
drop("tool calls with no result, closed as failed")
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
let timed = 0
|
|
123
|
+
for (const t of session.turns) {
|
|
124
|
+
if (t.at) timed++
|
|
125
|
+
switch (t.kind) {
|
|
126
|
+
case "user": {
|
|
127
|
+
const n = t.attachments?.length ?? 0
|
|
128
|
+
const images = t.attachments?.filter((a) => a.kind === "image").length ?? 0
|
|
129
|
+
if (images) drop("images", images)
|
|
130
|
+
if (n - images) drop("attached files", n - images)
|
|
131
|
+
const text = t.text.trim() ? t.text : n ? `(${n} attachment${n > 1 ? "s" : ""} not carried over)` : ""
|
|
132
|
+
if (!text) break
|
|
133
|
+
closeDangling()
|
|
134
|
+
userStep(text)
|
|
135
|
+
expect.users++
|
|
136
|
+
break
|
|
137
|
+
}
|
|
138
|
+
case "assistant": {
|
|
139
|
+
if (!t.text.trim()) break
|
|
140
|
+
flushCalls()
|
|
141
|
+
if (!noted) userStep("")
|
|
142
|
+
push(STEP.plannerResponse, STATUS.done, plannerBody({ text: t.text, messageId: randomUUID() }))
|
|
143
|
+
expect.assistants++
|
|
144
|
+
if (t.model || t.usage) drop("model names and token usage on replies")
|
|
145
|
+
break
|
|
146
|
+
}
|
|
147
|
+
case "tool_call": {
|
|
148
|
+
let id = t.id
|
|
149
|
+
if (!id || used.has(id)) {
|
|
150
|
+
id = freshId()
|
|
151
|
+
drop("missing or repeated tool call ids, replaced")
|
|
152
|
+
}
|
|
153
|
+
used.add(id)
|
|
154
|
+
let input: Record<string, unknown> = {}
|
|
155
|
+
if (isObject(t.input)) input = t.input
|
|
156
|
+
else if (t.input !== undefined && t.input !== null) {
|
|
157
|
+
input = { value: t.input }
|
|
158
|
+
drop("tool inputs that were not objects, wrapped")
|
|
159
|
+
}
|
|
160
|
+
let args = "{}"
|
|
161
|
+
try {
|
|
162
|
+
args = JSON.stringify(input)
|
|
163
|
+
} catch {
|
|
164
|
+
input = {}
|
|
165
|
+
drop("tool inputs that are not JSON")
|
|
166
|
+
}
|
|
167
|
+
const call: NativeCall = { id, name: t.name || "unknown_tool", args }
|
|
168
|
+
open.push(call)
|
|
169
|
+
pending.push({ src: t.id, call, input })
|
|
170
|
+
expect.calls++
|
|
171
|
+
break
|
|
172
|
+
}
|
|
173
|
+
case "tool_result": {
|
|
174
|
+
flushCalls()
|
|
175
|
+
const i = pending.findIndex((p) => p.src === t.callId)
|
|
176
|
+
let p = i >= 0 ? pending.splice(i, 1)[0]! : undefined
|
|
177
|
+
if (!p) {
|
|
178
|
+
// A result with no open call before it gets a call of its own: the store links every result to one.
|
|
179
|
+
const id = t.callId && !used.has(t.callId) ? t.callId : freshId()
|
|
180
|
+
used.add(id)
|
|
181
|
+
p = { src: t.callId, call: { id, name: "unknown_tool", args: "{}" }, input: {} }
|
|
182
|
+
if (!noted) userStep("")
|
|
183
|
+
push(STEP.plannerResponse, STATUS.done, plannerBody({ messageId: randomUUID(), calls: [p.call] }))
|
|
184
|
+
expect.calls++
|
|
185
|
+
drop("tool results without a call (given a placeholder call)")
|
|
186
|
+
}
|
|
187
|
+
resultStep(p.call, p.input, t.ok, t.output)
|
|
188
|
+
break
|
|
189
|
+
}
|
|
190
|
+
case "reasoning":
|
|
191
|
+
drop("reasoning")
|
|
192
|
+
break
|
|
193
|
+
case "compaction":
|
|
194
|
+
drop("compaction summaries")
|
|
195
|
+
break
|
|
196
|
+
case "file_edit":
|
|
197
|
+
if (!t.callId) drop("file edits not made by a tool call")
|
|
198
|
+
break
|
|
199
|
+
case "subagent":
|
|
200
|
+
drop("subagent sessions (their call and result are kept)")
|
|
201
|
+
break
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
closeDangling()
|
|
205
|
+
// The store needs at least one user input; a session without one still gets the note.
|
|
206
|
+
if (!noted) userStep("")
|
|
207
|
+
if (timed) drop("turn times", timed)
|
|
208
|
+
|
|
209
|
+
let lastUserStep = -1
|
|
210
|
+
steps.forEach((s, i) => {
|
|
211
|
+
if (s.type === STEP.userInput) lastUserStep = i
|
|
212
|
+
})
|
|
213
|
+
const trajectoryId = opts.trajectoryId ?? randomUUID()
|
|
214
|
+
if (trajectoryId === opts.id) throw new Error("the Antigravity conversation and trajectory ids must differ")
|
|
215
|
+
return {
|
|
216
|
+
id: opts.id,
|
|
217
|
+
trajectoryId,
|
|
218
|
+
steps,
|
|
219
|
+
title: continuedTitle(session).slice(0, 4_096),
|
|
220
|
+
preview: (firstUserText(session) ?? "Imported conversation").slice(0, 16_384),
|
|
221
|
+
lastUserStep,
|
|
222
|
+
lost: [...drops].map(([what, n]) => `${what} (${n})`),
|
|
223
|
+
expect,
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** The conversation database's bytes, built in memory and read back before anything is written. */
|
|
228
|
+
export function buildConversation(plan: AntigravityPlan, now: Date): Uint8Array {
|
|
229
|
+
const db = new Database(":memory:")
|
|
230
|
+
try {
|
|
231
|
+
db.exec(SCHEMA)
|
|
232
|
+
db.transaction(() => {
|
|
233
|
+
db.prepare("INSERT INTO trajectory_meta (trajectory_id, cascade_id, trajectory_type, source) VALUES (?, ?, ?, ?)").run(plan.trajectoryId, plan.id, TRAJECTORY.cascade, TRAJECTORY.cli)
|
|
234
|
+
const add = db.prepare("INSERT INTO steps (idx, step_type, status, has_subtrajectory, metadata, step_payload, step_format) VALUES (?, ?, ?, 0, NULL, ?, 0)")
|
|
235
|
+
plan.steps.forEach((s, i) => add.run(i, s.type, s.status, s.payload))
|
|
236
|
+
db.prepare("INSERT INTO trajectory_metadata_blob (id, data) VALUES ('main', ?)").run(trajectoryMetadata(Math.floor(now.getTime() / 1000), plan.id))
|
|
237
|
+
})()
|
|
238
|
+
const check = db.query("PRAGMA integrity_check").get() as { integrity_check?: string } | null
|
|
239
|
+
if (check?.integrity_check !== "ok") throw new Error("the new Antigravity conversation failed SQLite's integrity check")
|
|
240
|
+
const back = readConversation(db, plan.id, { path: `${plan.id}.db` })
|
|
241
|
+
const n = countTurns(back)
|
|
242
|
+
const pairs = unpaired(back)
|
|
243
|
+
const e = plan.expect
|
|
244
|
+
if (
|
|
245
|
+
n.user !== e.users ||
|
|
246
|
+
n.assistant !== e.assistants ||
|
|
247
|
+
n.tool_call !== e.calls ||
|
|
248
|
+
n.tool_result !== e.results ||
|
|
249
|
+
n.reasoning ||
|
|
250
|
+
pairs.calls.length ||
|
|
251
|
+
pairs.results.length ||
|
|
252
|
+
back.header.lost.length
|
|
253
|
+
)
|
|
254
|
+
throw new Error(
|
|
255
|
+
`the Antigravity conversation did not read back as written (users ${n.user}/${e.users}, replies ${n.assistant}/${e.assistants}, tool calls ${n.tool_call}/${e.calls}, results ${n.tool_result}/${e.results}, unpaired ${pairs.calls.length + pairs.results.length}, lost ${back.header.lost.join("; ") || "none"}); nothing was written`,
|
|
256
|
+
)
|
|
257
|
+
return db.serialize()
|
|
258
|
+
} finally {
|
|
259
|
+
db.close()
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** Summary columns the writer fills that a summaries table lacks. Extra columns are fine. */
|
|
264
|
+
function missingColumns(d: Database): string[] {
|
|
265
|
+
const have = new Set((d.query("PRAGMA table_info(conversation_summaries)").all() as Array<{ name: string }>).map((c) => c.name))
|
|
266
|
+
return have.size ? SUMMARY_COLUMNS.filter((c) => !have.has(c)) : ["conversation_summaries (no such table)"]
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const lexists = (p: string) => {
|
|
270
|
+
try {
|
|
271
|
+
lstatSync(p)
|
|
272
|
+
return true
|
|
273
|
+
} catch {
|
|
274
|
+
return false
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** The binary on PATH (an explicit PATH in the environment is the one searched). */
|
|
279
|
+
export const agyBin = () => Bun.which("agy", { PATH: process.env.PATH ?? "" }) ?? undefined
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* "1.1.16" from `agy --version`; undefined when it does not answer. Auto-update is turned off for the call:
|
|
283
|
+
* session-migrate found that even `agy --version` starts the 1.1.16 updater.
|
|
284
|
+
*/
|
|
285
|
+
export function agyVersion(bin: string): string | undefined {
|
|
286
|
+
try {
|
|
287
|
+
const p = Bun.spawnSync([bin, "--version"], {
|
|
288
|
+
env: { ...process.env, AGY_CLI_DISABLE_AUTO_UPDATE: "1" },
|
|
289
|
+
stdin: "ignore",
|
|
290
|
+
stdout: "pipe",
|
|
291
|
+
stderr: "pipe",
|
|
292
|
+
timeout: 15_000,
|
|
293
|
+
})
|
|
294
|
+
return p.exitCode === 0 ? p.stdout.toString().trim().split("\n")[0]?.match(/\d+\.\d+\.\d+/)?.[0] : undefined
|
|
295
|
+
} catch {
|
|
296
|
+
return undefined
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
export interface InstallOptions extends WriteOptions {
|
|
301
|
+
/** The new conversation's id (a UUID); a fresh one when omitted. */
|
|
302
|
+
id?: string
|
|
303
|
+
trajectoryId?: string
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/** Writes the session as a new CLI conversation (or, in a dry run, only checks it would). */
|
|
307
|
+
export async function installConversation(session: Session, opts: InstallOptions): Promise<WriteResult> {
|
|
308
|
+
const cwd = resolve(opts.cwd)
|
|
309
|
+
const now = new Date()
|
|
310
|
+
const id = opts.id ?? randomUUID()
|
|
311
|
+
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(id)) throw new Error(`an Antigravity conversation id is a lowercase UUIDv4, not ${id}`)
|
|
312
|
+
const plan = planAntigravity(session, { id, now, ...(opts.trajectoryId ? { trajectoryId: opts.trajectoryId } : {}) })
|
|
313
|
+
const bytes = buildConversation(plan, now)
|
|
314
|
+
const dir = conversationsDir(opts.home, CLI_STORE)
|
|
315
|
+
const file = join(dir, `${id}.db`)
|
|
316
|
+
const sums = summariesPath(opts.home, CLI_STORE)
|
|
317
|
+
const taken = () => new Error(`Antigravity conversation ${id} already exists; brain never overwrites or merges a conversation`)
|
|
318
|
+
|
|
319
|
+
if (lexists(file)) throw taken()
|
|
320
|
+
if (exists(sums)) {
|
|
321
|
+
const { missing, row } = withSnapshot(sums, (d) => {
|
|
322
|
+
const missing = missingColumns(d)
|
|
323
|
+
return { missing, row: missing.length ? null : d.query("SELECT 1 FROM conversation_summaries WHERE conversation_id = ?").get(id) }
|
|
324
|
+
})
|
|
325
|
+
if (missing.length) throw new Error(`the Antigravity summaries database at ${sums} lacks ${missing.join(", ")}; it is not one this writer knows`)
|
|
326
|
+
if (row) throw taken()
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (!opts.dryRun) {
|
|
330
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 })
|
|
331
|
+
ensureSummaries(sums)
|
|
332
|
+
const summary = [
|
|
333
|
+
id,
|
|
334
|
+
plan.title,
|
|
335
|
+
plan.preview,
|
|
336
|
+
plan.steps.length,
|
|
337
|
+
toSqlTime(now),
|
|
338
|
+
JSON.stringify([pathToFileURL(cwd).href]),
|
|
339
|
+
"",
|
|
340
|
+
"antigravity-cli",
|
|
341
|
+
PROJECT_ID,
|
|
342
|
+
"",
|
|
343
|
+
"",
|
|
344
|
+
0,
|
|
345
|
+
"",
|
|
346
|
+
"",
|
|
347
|
+
0,
|
|
348
|
+
0,
|
|
349
|
+
toSqlTime(now),
|
|
350
|
+
plan.lastUserStep,
|
|
351
|
+
"antigravity-cli",
|
|
352
|
+
]
|
|
353
|
+
publish(bytes, file, sums, id, summary, taken)
|
|
354
|
+
}
|
|
355
|
+
return {
|
|
356
|
+
tool: TOOL,
|
|
357
|
+
way: "native",
|
|
358
|
+
sessionId: id,
|
|
359
|
+
written: [file, sums],
|
|
360
|
+
open: `cd ${sh(cwd)} && agy --conversation ${id}`,
|
|
361
|
+
lost: plan.lost,
|
|
362
|
+
dryRun: !!opts.dryRun,
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/** Creates the summaries database as 1.1.16 does when there is none; one made meanwhile by someone else is kept. */
|
|
367
|
+
function ensureSummaries(path: string) {
|
|
368
|
+
if (lexists(path)) return
|
|
369
|
+
const mem = new Database(":memory:")
|
|
370
|
+
let bytes: Uint8Array
|
|
371
|
+
try {
|
|
372
|
+
mem.exec(SUMMARY_SCHEMA)
|
|
373
|
+
bytes = mem.serialize()
|
|
374
|
+
} finally {
|
|
375
|
+
mem.close()
|
|
376
|
+
}
|
|
377
|
+
const tmp = `${path}.brain-${randomBytes(6).toString("hex")}`
|
|
378
|
+
writeFileSync(tmp, bytes, { flag: "wx", mode: 0o600 })
|
|
379
|
+
try {
|
|
380
|
+
linkSync(tmp, path)
|
|
381
|
+
} catch (e) {
|
|
382
|
+
if ((e as NodeJS.ErrnoException).code !== "EEXIST") throw e
|
|
383
|
+
} finally {
|
|
384
|
+
unlinkSync(tmp)
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Publishes the conversation and its summary row together: the id is reserved in the summaries table with an
|
|
390
|
+
* immediate transaction, the database appears under its final name only if that name is free (a hard link
|
|
391
|
+
* from a private temporary file), and a failure before the commit removes only the file this call created.
|
|
392
|
+
*/
|
|
393
|
+
function publish(bytes: Uint8Array, file: string, sums: string, id: string, summary: unknown[], taken: () => Error) {
|
|
394
|
+
const tmp = join(resolve(file, ".."), `.${id}.db.brain-${randomBytes(6).toString("hex")}`)
|
|
395
|
+
writeFileSync(tmp, bytes, { flag: "wx", mode: 0o600 })
|
|
396
|
+
const ino = statSync(tmp).ino
|
|
397
|
+
const db = new Database(sums)
|
|
398
|
+
let linked = false
|
|
399
|
+
try {
|
|
400
|
+
db.exec("PRAGMA busy_timeout = 15000")
|
|
401
|
+
const missing = missingColumns(db)
|
|
402
|
+
if (missing.length) throw new Error(`the Antigravity summaries database at ${sums} lacks ${missing.join(", ")}`)
|
|
403
|
+
db.exec("BEGIN IMMEDIATE")
|
|
404
|
+
try {
|
|
405
|
+
if (db.query("SELECT 1 FROM conversation_summaries WHERE conversation_id = ?").get(id) || lexists(file)) throw taken()
|
|
406
|
+
try {
|
|
407
|
+
linkSync(tmp, file)
|
|
408
|
+
} catch (e) {
|
|
409
|
+
throw (e as NodeJS.ErrnoException).code === "EEXIST" ? taken() : e
|
|
410
|
+
}
|
|
411
|
+
linked = true
|
|
412
|
+
db.prepare(`INSERT INTO conversation_summaries (${SUMMARY_COLUMNS.join(", ")}) VALUES (${SUMMARY_COLUMNS.map(() => "?").join(", ")})`).run(...(summary as Array<string | number>))
|
|
413
|
+
db.exec("COMMIT")
|
|
414
|
+
} catch (e) {
|
|
415
|
+
db.exec("ROLLBACK")
|
|
416
|
+
if (linked)
|
|
417
|
+
try {
|
|
418
|
+
if (statSync(file).ino === ino) unlinkSync(file)
|
|
419
|
+
} catch {
|
|
420
|
+
// already gone
|
|
421
|
+
}
|
|
422
|
+
throw e
|
|
423
|
+
}
|
|
424
|
+
} finally {
|
|
425
|
+
db.close()
|
|
426
|
+
try {
|
|
427
|
+
unlinkSync(tmp)
|
|
428
|
+
} catch {
|
|
429
|
+
// already gone
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
export const antigravityWriter: Writer = {
|
|
435
|
+
tool: TOOL,
|
|
436
|
+
label: LABEL,
|
|
437
|
+
way: "native",
|
|
438
|
+
|
|
439
|
+
async ready(opts: WriteOptions): Promise<Readiness> {
|
|
440
|
+
const bin = agyBin()
|
|
441
|
+
if (!bin) return { ok: false, why: "the Antigravity CLI is not installed (no agy on PATH)" }
|
|
442
|
+
const v = agyVersion(bin)
|
|
443
|
+
if (!v) return { ok: false, why: `could not read the Antigravity CLI's version (${bin} --version failed)` }
|
|
444
|
+
if (!TESTED_VERSIONS.includes(v))
|
|
445
|
+
return { ok: false, why: `Antigravity CLI ${v} has not been tested; its private conversation store was checked on ${TESTED_VERSIONS.join(", ")} only` }
|
|
446
|
+
if (!isDir(opts.cwd)) return { ok: false, why: `the folder to continue in does not exist: ${opts.cwd}` }
|
|
447
|
+
const sums = summariesPath(opts.home, CLI_STORE)
|
|
448
|
+
if (exists(sums)) {
|
|
449
|
+
let missing: string[]
|
|
450
|
+
try {
|
|
451
|
+
missing = withSnapshot(sums, missingColumns)
|
|
452
|
+
} catch (e) {
|
|
453
|
+
return { ok: false, why: `the Antigravity summaries database cannot be read: ${(e as Error).message}` }
|
|
454
|
+
}
|
|
455
|
+
if (missing.length) return { ok: false, why: `the Antigravity summaries database lacks ${missing.join(", ")}; it is not one this writer knows` }
|
|
456
|
+
}
|
|
457
|
+
return { ok: true }
|
|
458
|
+
},
|
|
459
|
+
|
|
460
|
+
write(session: Session, opts: WriteOptions): Promise<WriteResult> {
|
|
461
|
+
return installConversation(session, opts)
|
|
462
|
+
},
|
|
463
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto"
|
|
2
|
+
import { join } from "node:path"
|
|
3
|
+
import { piFamilyReader } from "../pi/reader.ts"
|
|
4
|
+
import type { PiFlavor } from "../pi/records.ts"
|
|
5
|
+
import { piFamilyWriter } from "../pi/writer.ts"
|
|
6
|
+
import type { Reader, Writer } from "../types.ts"
|
|
7
|
+
|
|
8
|
+
// Auggie CLI (Augment) 1.x: a rewrite built on Pi, so its sessions are Pi's format version 3 in its own
|
|
9
|
+
// folder: ~/.augment/sessions-v2/--<cwd>--/<uuid>.jsonl (AUGGIE_CODING_AGENT_DIR / AUGMENT_CACHE_DIR move it,
|
|
10
|
+
// which brain does not follow). Auggie adds a queue_state record, a compaction field (protectedHeadEntryId)
|
|
11
|
+
// and two user-side roles (slash-command invocations, rule references); the Pi reader reads all of them.
|
|
12
|
+
// Continues any session as a new native Auggie session, `auggie --session <id>`. Auggie 0.x keeps JSON
|
|
13
|
+
// conversations in ~/.augment/sessions instead and is not written.
|
|
14
|
+
|
|
15
|
+
/** Auggie versions whose session format this writer was checked against (1.x is published as daily prereleases). */
|
|
16
|
+
export const AUGGIE_TESTED_VERSIONS = ["1.2.0-prerelease.202611061509"]
|
|
17
|
+
|
|
18
|
+
export const auggieFlavor: PiFlavor = {
|
|
19
|
+
tool: "auggie",
|
|
20
|
+
label: "Auggie",
|
|
21
|
+
bin: "auggie",
|
|
22
|
+
sessionsRoot: (home) => join(home, ".augment", "sessions-v2"),
|
|
23
|
+
// <id>.jsonl: Auggie's SessionManager does not prefix the start time.
|
|
24
|
+
fileName: (id) => `${id}.jsonl`,
|
|
25
|
+
newId: () => randomUUID(),
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Auggie 0.x stores another format; say so instead of only "untested". */
|
|
29
|
+
const v0 = (v: string) =>
|
|
30
|
+
/^0\./.test(v) ? `Auggie ${v} keeps sessions as JSON in ~/.augment/sessions; this writer writes the 1.x format (install @augmentcode/auggie@prerelease)` : undefined
|
|
31
|
+
|
|
32
|
+
export const auggieReader = piFamilyReader(auggieFlavor)
|
|
33
|
+
export const auggieWriter = (version?: string) => piFamilyWriter(auggieFlavor, { tested: AUGGIE_TESTED_VERSIONS, why: v0, ...(version ? { version } : {}) })
|
|
34
|
+
|
|
35
|
+
export const readers: Reader[] = [auggieReader]
|
|
36
|
+
export const writers: Writer[] = [auggieWriter()]
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Reader, Writer } from "../types.ts"
|
|
2
|
+
import { claudeCodeReader } from "./reader.ts"
|
|
3
|
+
import { claudeCodeNativeWriter } from "./writer.ts"
|
|
4
|
+
|
|
5
|
+
// Claude Code (CLI, Agent SDK, and Claude Desktop's Code sessions, which keep the same transcripts):
|
|
6
|
+
// reads every session with its subagents, and continues any session as a new native Claude Code session.
|
|
7
|
+
export const readers: Reader[] = [claudeCodeReader]
|
|
8
|
+
export const writers: Writer[] = [claudeCodeNativeWriter]
|