diff --git a/docs/parser-documentation/access-recipes/14-antigravity.md b/docs/parser-documentation/access-recipes/14-antigravity.md index 3b7d8e9..d80ee4f 100644 --- a/docs/parser-documentation/access-recipes/14-antigravity.md +++ b/docs/parser-documentation/access-recipes/14-antigravity.md @@ -3,8 +3,10 @@ ## Raw Sources - Primary root: `~/.gemini/antigravity/` -- Session discovery: `conversations/*.pb`, `brain//`, and `state.vscdb` trajectory summaries. +- CLI root: `~/.gemini/antigravity-cli/` +- Session discovery: CLI `conversations/*.db`, IDE `conversations/*.pb`, `brain//`, and `state.vscdb` trajectory summaries. - Context extraction: + - CLI: read-only SQLite step extraction from `antigravity-cli/conversations/.db`. - Offline: `brain//task.md`, `implementation_plan.md`, `walkthrough.md`, and `.resolved*` variants. - Live: local Antigravity language-server RPC when the app is running. - Legacy fallback: chat-shaped JSON/JSONL under `code_tracker/`; snapshot-only files are ignored. @@ -14,10 +16,17 @@ ### Inspect current session IDs ```bash +find ~/.gemini/antigravity-cli/conversations -maxdepth 1 -name '*.db' -print find ~/.gemini/antigravity/conversations -maxdepth 1 -name '*.pb' -print find ~/.gemini/antigravity/brain -maxdepth 1 -type d -print ``` +### Resume a CLI conversation natively + +```bash +agy --conversation +``` + ### Inspect offline handoff artifacts ```bash @@ -41,6 +50,7 @@ find ~/.gemini/antigravity/code_tracker -type f \( -name '*.json' -o -name '*.js ## Current Parser Comparison - The parser now indexes current Antigravity installs even when `code_tracker` contains only file snapshots. +- Antigravity CLI conversations are indexed from `~/.gemini/antigravity-cli/conversations/*.db`. - Offline handoffs are artifact-backed and explicitly note that full raw transcript extraction requires live Antigravity. - Legacy JSONL remains supported only for files with real user/assistant chat entries. diff --git a/docs/parser-documentation/storage-format/14-antigravity.md b/docs/parser-documentation/storage-format/14-antigravity.md index a724637..c058309 100644 --- a/docs/parser-documentation/storage-format/14-antigravity.md +++ b/docs/parser-documentation/storage-format/14-antigravity.md @@ -5,6 +5,8 @@ Accessed: 2026-04-28 ## Observed Storage - Root: `~/.gemini/antigravity/` +- CLI root: `~/.gemini/antigravity-cli/` +- CLI conversations: `antigravity-cli/conversations/.db` - Persisted conversation IDs: `conversations/*.pb` - Handoff artifacts: `brain//task.md`, `implementation_plan.md`, `walkthrough.md`, plus `.resolved*` variants. - UI/index metadata: Antigravity global storage SQLite at `~/Library/Application Support/Antigravity/User/globalStorage/state.vscdb` on macOS. @@ -12,7 +14,8 @@ Accessed: 2026-04-28 ## Parser Behavior -- `src/parsers/antigravity.ts` now discovers sessions from the union of `conversations/*.pb`, `brain//`, `state.vscdb` trajectory summaries, and optional live language-server RPC. +- `src/parsers/antigravity.ts` now discovers sessions from the union of CLI `conversations/*.db`, IDE `conversations/*.pb`, `brain//`, `state.vscdb` trajectory summaries, and optional live language-server RPC. +- CLI `.db` extraction reads `steps` read-only through `node:sqlite` and decodes embedded protobuf/JSON strings best-effort for messages and tool activity. - `code_tracker/` is no longer treated as canonical. It is parsed only as a legacy fallback when a file actually contains chat-shaped `{type, content, timestamp}` records. - Offline extraction does not decrypt `.pb`; it builds a useful handoff from brain artifacts and state metadata. - When Antigravity is running, the parser attempts read-only RPC extraction for full steps, messages, tool activity, and modified files. @@ -26,6 +29,7 @@ Accessed: 2026-04-28 ## Direct Access Recipe ```bash +find ~/.gemini/antigravity-cli/conversations -name '*.db' find ~/.gemini/antigravity/conversations -name '*.pb' find ~/.gemini/antigravity/brain -maxdepth 2 -type f | head -n 80 sqlite3 "$HOME/Library/Application Support/Antigravity/User/globalStorage/state.vscdb" \ diff --git a/src/__tests__/antigravity-parser.test.ts b/src/__tests__/antigravity-parser.test.ts index 2811664..12664c7 100644 --- a/src/__tests__/antigravity-parser.test.ts +++ b/src/__tests__/antigravity-parser.test.ts @@ -54,6 +54,7 @@ function makeRoot(): string { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'continues-antigravity-')); tempRoots.push(root); vi.stubEnv('ANTIGRAVITY_HOME', root); + vi.stubEnv('ANTIGRAVITY_CLI_HOME', path.join(root, 'missing-antigravity-cli')); vi.stubEnv('ANTIGRAVITY_STATE_DB', path.join(root, 'missing-state.vscdb')); vi.stubEnv('ANTIGRAVITY_DISABLE_RPC', '1'); return root; @@ -120,6 +121,28 @@ function createStateDb(dbPath: string, key: string, value: string): void { } } +function createCliConversationDb( + dbPath: string, + rows: Array<{ idx: number; stepType: number; payload: string }>, +): void { + const sqliteModule = require('node:sqlite') as { + DatabaseSync: new (database: string) => SqliteDatabase; + }; + fs.mkdirSync(path.dirname(dbPath), { recursive: true }); + const db = new sqliteModule.DatabaseSync(dbPath); + try { + db.exec( + 'CREATE TABLE steps (idx integer PRIMARY KEY, step_type integer NOT NULL DEFAULT 0, metadata blob, task_details blob, render_info blob, step_payload blob)', + ); + const insert = db.prepare('INSERT INTO steps (idx, step_type, step_payload) VALUES (?, ?, ?)'); + for (const row of rows) { + insert.run(row.idx, row.stepType, Buffer.from(row.payload, 'utf8')); + } + } finally { + db.close(); + } +} + afterEach(() => { vi.unstubAllEnvs(); spawnMock.mockClear(); @@ -237,6 +260,64 @@ describe('Antigravity parser', () => { expect(sessions[0].updatedAt.toISOString()).toBe(updatedAt.toISOString()); }); + it('discovers Antigravity CLI sqlite conversations under antigravity-cli', async () => { + const root = makeRoot(); + const cliRoot = path.join(root, 'antigravity-cli'); + const id = 'dddddddd-bbbb-4ccc-8ddd-eeeeeeeeeeee'; + vi.stubEnv('ANTIGRAVITY_CLI_HOME', cliRoot); + writeFile(path.join(cliRoot, 'cache', 'last_conversations.json'), JSON.stringify({ '/home/user/project': id })); + createCliConversationDb(path.join(cliRoot, 'conversations', `${id}.db`), [ + { idx: 0, stepType: 14, payload: 'Fix the CLI resume flow for Antigravity' }, + { + idx: 1, + stepType: 15, + payload: + 'I will inspect the resume adapter. run_command {"CommandLine":"git status --short","Cwd":"/home/user/project"}', + }, + ]); + + const sessions = await parseAntigravitySessions(); + + expect(sessions).toHaveLength(1); + expect(sessions[0]).toMatchObject({ + id, + source: 'antigravity', + cwd: '/home/user/project', + repo: 'user/project', + lines: 2, + summary: 'Fix the CLI resume flow for Antigravity', + originalPath: path.join(cliRoot, 'conversations', `${id}.db`), + }); + }); + + it('extracts Antigravity CLI context from sqlite steps without launching the IDE', async () => { + const root = makeRoot(); + const cliRoot = path.join(root, 'antigravity-cli'); + const id = 'eeeeeeee-bbbb-4ccc-8ddd-eeeeeeeeeeee'; + vi.stubEnv('ANTIGRAVITY_CLI_HOME', cliRoot); + createCliConversationDb(path.join(cliRoot, 'conversations', `${id}.db`), [ + { idx: 0, stepType: 14, payload: 'Fix the Antigravity CLI parser' }, + { + idx: 1, + stepType: 15, + payload: + 'I will inspect the registry. run_command {"CommandLine":"rg antigravity src/parsers/registry.ts","Cwd":"/home/user/project"}', + }, + ]); + + const [session] = await parseAntigravitySessions(); + const context = await extractAntigravityContext(session); + + expect(context.recentMessages.map((message) => message.role)).toContain('user'); + expect(context.recentMessages.map((message) => message.role)).toContain('assistant'); + expect( + context.toolSummaries.some((summary) => + summary.samples.some((sample) => sample.summary.includes('rg antigravity')), + ), + ).toBe(true); + expect(spawnMock).not.toHaveBeenCalled(); + }); + it('extracts an offline handoff from brain artifacts when live RPC is unavailable', async () => { const root = makeRoot(); const id = 'cccccccc-bbbb-4ccc-8ddd-eeeeeeeeeeee'; diff --git a/src/__tests__/forward-flags.test.ts b/src/__tests__/forward-flags.test.ts index 8e63b75..cae0f78 100644 --- a/src/__tests__/forward-flags.test.ts +++ b/src/__tests__/forward-flags.test.ts @@ -126,6 +126,37 @@ describe('cross-tool forwarding', () => { expect(resolved.passthroughArgs).toEqual([]); }); + it('maps antigravity cli forwarding and resume commands to agy', () => { + const session: UnifiedSession = { + id: 'abc123456789', + source: 'antigravity', + cwd: '/tmp/project', + lines: 10, + bytes: 120, + createdAt: new Date('2026-02-20T00:00:00.000Z'), + updatedAt: new Date('2026-02-20T00:00:00.000Z'), + originalPath: '/tmp/session.db', + }; + + const resolved = resolveCrossToolForwarding('antigravity', { + rawArgs: ['--allow-all', '--sandbox', 'false', '--model', 'gemini-3-flash'], + }); + + expect(adapters.antigravity.binaryName).toBe('agy'); + expect(adapters.antigravity.binaryFallbacks).toEqual(['antigravity']); + expect(adapters.antigravity.nativeResumeArgs(session)).toEqual(['--conversation', 'abc123456789']); + expect(getResumeCommand(session)).toBe( + 'agy --conversation abc123456789 (or: antigravity --conversation abc123456789)', + ); + expect(resolved.mappedArgs).toEqual([ + '--dangerously-skip-permissions', + '--sandbox=false', + '--model', + 'gemini-3-flash', + ]); + expect(resolved.passthroughArgs).toEqual([]); + }); + it('consumes unsupported approval and permission forwarding for opencode', () => { const resolved = resolveCrossToolForwarding('opencode', { rawArgs: ['--approval-mode', 'plan', '--permission-mode', 'plan'], @@ -149,6 +180,7 @@ describe('cross-tool forwarding', () => { expect(getDefaultHandoffInitArgs('kiro')).toEqual([]); expect(getDefaultHandoffInitArgs('crush')).toEqual([]); expect(getDefaultHandoffInitArgs('qwen-code')).toEqual([]); + expect(getDefaultHandoffInitArgs('antigravity')).toEqual([]); expect(getDefaultHandoffInitArgs('codex')).toEqual([ '-c', diff --git a/src/parsers/antigravity.ts b/src/parsers/antigravity.ts index 3d32d68..d3c4893 100644 --- a/src/parsers/antigravity.ts +++ b/src/parsers/antigravity.ts @@ -5,6 +5,7 @@ import * as https from 'node:https'; import { createRequire } from 'node:module'; import * as path from 'node:path'; import * as readline from 'node:readline'; +import { pathToFileURL } from 'node:url'; import type { VerbosityConfig } from '../config/index.js'; import { getPreset } from '../config/index.js'; import { logger } from '../logger.js'; @@ -24,6 +25,7 @@ const SOURCE_NAME: SessionSource = 'antigravity'; const SUMMARY_STATE_KEYS = ['antigravityUnifiedStateSync.trajectorySummaries', 'unifiedStateSync.trajectorySummaries']; const BRAIN_ARTIFACT_BASE_FILES = ['task.md', 'implementation_plan.md', 'walkthrough.md']; const UUIDISH_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/iu; +const UUIDISH_ANYWHERE_RE = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/iu; const RPC_TIMEOUT_MS = 1500; const LAUNCH_POLL_INTERVAL_MS_DEFAULT = 500; const LAUNCH_TIMEOUT_MS_DEFAULT = 25_000; @@ -45,6 +47,7 @@ function getLaunchTimeoutMs(): number { interface SqlitePreparedStatement { get(...params: unknown[]): unknown | undefined; + all(...params: unknown[]): unknown[]; } interface SqliteDatabase { @@ -68,12 +71,22 @@ interface LiveSummary extends StateSummary { interface AntigravityRecord { id: string; conversationPath?: string; + cliConversationPath?: string; brainDir?: string; legacyPath?: string; state?: StateSummary; live?: LiveSummary; } +interface CliDbStep { + idx: number; + stepType: number; + payload?: Uint8Array; + metadata?: Uint8Array; + taskDetails?: Uint8Array; + renderInfo?: Uint8Array; +} + interface AntigravityEntry { type: string; timestamp: string; @@ -131,6 +144,15 @@ function getAntigravityRoot(): string { return path.join(configuredHome, '.gemini', 'antigravity'); } +function getAntigravityCliRoot(): string { + const explicit = process.env.ANTIGRAVITY_CLI_HOME?.trim(); + if (explicit) return expandHome(explicit); + + const configuredHome = process.env.GEMINI_CLI_HOME || homeDir(); + if (path.basename(configuredHome) === 'antigravity-cli') return configuredHome; + return path.join(configuredHome, '.gemini', 'antigravity-cli'); +} + function getConversationsDir(): string { return path.join(getAntigravityRoot(), 'conversations'); } @@ -139,6 +161,14 @@ function getBrainDir(): string { return path.join(getAntigravityRoot(), 'brain'); } +function getCliBrainDir(): string { + return path.join(getAntigravityCliRoot(), 'brain'); +} + +function getCliConversationsDir(): string { + return path.join(getAntigravityCliRoot(), 'conversations'); +} + function getCodeTrackerDir(): string { return path.join(getAntigravityRoot(), 'code_tracker'); } @@ -283,6 +313,16 @@ async function discoverConversationRecords(records: Map): Promise { + const conversationsDir = getCliConversationsDir(); + for (const entry of await readDirSafe(conversationsDir)) { + if (!entry.isFile() || !entry.name.endsWith('.db')) continue; + const id = path.basename(entry.name, '.db'); + if (!UUIDISH_RE.test(id)) continue; + addRecord(records, id, { cliConversationPath: path.join(conversationsDir, entry.name) }); + } +} + async function findBrainArtifactPath(brainDir: string, baseName: string): Promise { const entries = await readDirSafe(brainDir); const exact = entries.find((entry) => entry.isFile() && entry.name === baseName); @@ -309,12 +349,13 @@ async function hasBrainArtifacts(dirPath: string): Promise { } async function discoverBrainRecords(records: Map): Promise { - const brainDir = getBrainDir(); - for (const entry of await readDirSafe(brainDir)) { - if (!entry.isDirectory()) continue; - const dirPath = path.join(brainDir, entry.name); - if (!UUIDISH_RE.test(entry.name) && !(await hasBrainArtifacts(dirPath))) continue; - addRecord(records, entry.name, { brainDir: dirPath }); + for (const brainDir of [getBrainDir(), getCliBrainDir()]) { + for (const entry of await readDirSafe(brainDir)) { + if (!entry.isDirectory()) continue; + const dirPath = path.join(brainDir, entry.name); + if (!UUIDISH_RE.test(entry.name) && !(await hasBrainArtifacts(dirPath))) continue; + addRecord(records, entry.name, { brainDir: dirPath }); + } } } @@ -394,9 +435,10 @@ async function discoverLegacyRecords(records: Map): P // sessions whose basenames collide across subdirectories don't merge in // addRecord. Falls back to basename for any file outside code_tracker/. const relative = path.relative(codeTrackerDir, filePath); - const idBase = relative && !relative.startsWith('..') - ? relative.slice(0, -ext.length).replace(/[\\/]/gu, ':') - : path.basename(filePath, ext); + const idBase = + relative && !relative.startsWith('..') + ? relative.slice(0, -ext.length).replace(/[\\/]/gu, ':') + : path.basename(filePath, ext); addRecord(records, `legacy:${idBase}`, { legacyPath: filePath }); } } @@ -415,6 +457,27 @@ function openDb(dbPath: string): { db: SqliteDatabase; close: () => void } | nul } } +function immutableSqliteUri(dbPath: string): string { + const url = pathToFileURL(dbPath); + url.searchParams.set('mode', 'ro'); + url.searchParams.set('immutable', '1'); + return url.toString(); +} + +function openCliConversationDb(dbPath: string): { db: SqliteDatabase; close: () => void } | null { + try { + const require = createRequire(import.meta.url); + const sqliteModule = require('node:sqlite') as { + DatabaseSync: new (database: string, options?: { open?: boolean; readOnly?: boolean }) => SqliteDatabase; + }; + const db = new sqliteModule.DatabaseSync(immutableSqliteUri(dbPath), { open: true, readOnly: true }); + return { db, close: () => db.close() }; + } catch (err) { + logger.debug('antigravity: failed to open cli conversation database', dbPath, err); + return null; + } +} + function readStateValue(dbPath: string, key: string): string | undefined { const handle = openDb(dbPath); if (!handle) return undefined; @@ -758,6 +821,242 @@ function loadStateSummaries(): Map { return combined; } +async function loadCliLastConversationCwds(): Promise> { + const filePath = path.join(getAntigravityCliRoot(), 'cache', 'last_conversations.json'); + try { + const parsed: unknown = JSON.parse(await fsp.readFile(filePath, 'utf8')); + if (!isRecord(parsed)) return new Map(); + const byId = new Map(); + for (const [cwd, id] of Object.entries(parsed)) { + if (isNonEmptyString(cwd) && isNonEmptyString(id)) byId.set(id, cwd); + } + return byId; + } catch (err) { + logger.debug('antigravity: failed to read cli last conversations cache', filePath, err); + return new Map(); + } +} + +function dbBlob(value: unknown): Uint8Array | undefined { + return value instanceof Uint8Array ? value : undefined; +} + +function extractPrintableStrings(bytes: Uint8Array): string[] { + const raw = Buffer.from(bytes).toString('latin1'); + return Array.from(raw.matchAll(/[ -~]{4,}/gu), (match) => match[0].trim()).filter(Boolean); +} + +function textLooksUseful(value: string): boolean { + if (value.length < 8) return false; + if (UUIDISH_RE.test(value.replace(/^\$/u, ''))) return false; + if (UUIDISH_ANYWHERE_RE.test(value)) return false; + if (value.includes('sessionID')) return false; + if (/\b(command|execute_url|read_url|mcp)\(\*\)/u.test(value)) return false; + if (/^(sessionID|trajectory_id|model_enum|toolAction|toolSummary|CommandLine|Cwd|DirectoryPath)$/u.test(value)) { + return false; + } + if (/^[A-Za-z_]+\(.\)$/u.test(value)) return false; + if (/^[A-Za-z0-9_-]{8,}$/u.test(value) && !value.includes(' ')) return false; + return /[A-Za-z][A-Za-z]/u.test(value); +} + +function stringsFromStep(step: CliDbStep): string[] { + const chunks = [step.payload, step.metadata, step.taskDetails, step.renderInfo].filter( + (value): value is Uint8Array => value !== undefined, + ); + const strings: string[] = []; + for (const chunk of chunks) { + strings.push(...Array.from(iterUtf8StringsInProto(chunk, 4))); + strings.push(...extractPrintableStrings(chunk)); + } + return Array.from(new Set(strings.map((value) => value.replace(/\s+/gu, ' ').trim()).filter(textLooksUseful))); +} + +function parseJsonObjects(strings: string[]): Record[] { + const objects: Record[] = []; + for (const value of strings) { + const starts = [...value.matchAll(/\{/gu)].map((match) => match.index).filter((index) => index !== undefined); + for (const start of starts) { + const candidate = value.slice(start); + try { + const parsed: unknown = JSON.parse(candidate); + if (isRecord(parsed)) objects.push(parsed); + } catch { + // Ignore non-JSON protobuf strings. + } + } + } + return objects; +} + +function cleanCliMessageText(value: string): string { + const botMarker = value.indexOf('2(bot-'); + const trimmed = (botMarker >= 0 ? value.slice(0, botMarker) : value).trim(); + const leadingSentence = trimmed.match(/[A-Z][\s\S]+/u)?.[0] ?? trimmed; + return leadingSentence.replace(/[`'"]?$/u, '').trim(); +} + +function chooseCliText(strings: string[], preferred?: (value: string) => boolean): string | undefined { + const candidates = strings + .map(cleanCliMessageText) + .filter((value) => textLooksUseful(value) && !value.startsWith('{') && !value.startsWith('file://')); + const preferredMatch = preferred ? candidates.find(preferred) : undefined; + return preferredMatch ?? candidates.sort((left, right) => right.length - left.length)[0]; +} + +function extractCliCwdFromStrings(strings: string[]): string | undefined { + for (const text of strings) { + const uriMatch = text.match(/file:\/\/[^\s"')]+/u)?.[0]; + if (uriMatch) { + const cwd = decodeFileUri(uriMatch); + if (cwd) return path.dirname(cwd); + } + if (text.startsWith('/') && text.includes(path.sep)) return text.replace(/0$/u, ''); + } + return undefined; +} + +function readCliSteps(dbPath: string): CliDbStep[] { + const handle = openCliConversationDb(dbPath); + if (!handle) return []; + + try { + const rows = handle.db + .prepare('SELECT idx, step_type, metadata, task_details, render_info, step_payload FROM steps ORDER BY idx ASC') + .all(); + return rows.flatMap((row): CliDbStep[] => { + if (!isRecord(row) || typeof row.idx !== 'number' || typeof row.step_type !== 'number') return []; + return [ + { + idx: row.idx, + stepType: row.step_type, + payload: dbBlob(row.step_payload), + metadata: dbBlob(row.metadata), + taskDetails: dbBlob(row.task_details), + renderInfo: dbBlob(row.render_info), + }, + ]; + }); + } catch (err) { + logger.debug('antigravity: failed to read cli conversation steps', dbPath, err); + return []; + } finally { + handle.close(); + } +} + +function readCliStepCount(dbPath: string): number | undefined { + const handle = openCliConversationDb(dbPath); + if (!handle) return undefined; + + try { + const row = handle.db.prepare('SELECT COUNT(*) AS count FROM steps').get(); + return isRecord(row) && typeof row.count === 'number' ? row.count : undefined; + } catch (err) { + logger.debug('antigravity: failed to count cli conversation steps', dbPath, err); + return undefined; + } finally { + handle.close(); + } +} + +function titleFromCliSteps(steps: CliDbStep[]): string | undefined { + for (const stepType of [23, 14]) { + for (const step of steps) { + if (step.stepType !== stepType) continue; + const strings = stringsFromStep(step); + const title = chooseCliText(strings, (value) => value.length <= 120 && !value.includes('\n')); + if (title) return title; + } + } + for (const step of steps) { + const text = chooseCliText(stringsFromStep(step)); + if (text) return text; + } + return undefined; +} + +function summarizeCliToolPayload( + toolName: string, + payload: Record, + collector: SummaryCollector, +): void { + if (toolName === 'run_command') { + const command = firstString(payload, ['CommandLine', 'commandLine', 'command', 'cmd']); + if (!command) return; + const output = firstString(payload, ['Output', 'output', 'stdout', 'stderr', 'result']); + const cwd = firstString(payload, ['Cwd', 'cwd']); + collector.add(toolName, shellSummary(command, output), { + data: { + category: 'shell', + command, + ...(cwd ? { cwd } : {}), + ...(output ? { stdoutTail: output.slice(-500) } : {}), + }, + }); + return; + } + + const filePath = extractFilePath(payload); + if (filePath) { + const isWrite = /write|edit|patch|replace|create/iu.test(toolName); + collector.add(toolName, fileSummary(isWrite ? 'edit' : 'read', filePath), { + data: { category: isWrite ? 'edit' : 'read', filePath }, + filePath, + isWrite, + }); + return; + } + + const directory = firstString(payload, ['DirectoryPath', 'directoryPath', 'path']); + if (directory) { + collector.add(toolName, mcpSummary(toolName, directory), { + data: { category: 'glob', pattern: directory }, + }); + return; + } + + summarizeGenericTool(toolName, payload, undefined, collector); +} + +function extractFromCliDbSteps(steps: CliDbStep[], config: VerbosityConfig, fallbackDate: Date): RpcStepExtraction { + const messages: ConversationMessage[] = []; + const collector = new SummaryCollector(config); + + for (const step of steps) { + const strings = stringsFromStep(step); + const objects = parseJsonObjects(strings); + const timestamp = fallbackDate; + + if (step.stepType === 14 || step.stepType === 23) { + const text = chooseCliText(strings); + if (text) messages.push({ role: 'user', content: text, timestamp }); + } else if (step.stepType === 15) { + const text = chooseCliText(strings, (value) => /^(I|I'll|I will|Done|The|We)\b/u.test(value)); + if (text) messages.push({ role: 'assistant', content: text, timestamp }); + } + + for (const payload of objects) { + const toolName = + firstString(payload, ['toolName', 'name']) ?? + (firstString(payload, ['CommandLine', 'commandLine', 'command', 'cmd']) ? 'run_command' : undefined) ?? + (extractFilePath(payload) ? 'view_file' : undefined) ?? + strings.find((value) => /^[a-z_]+$/u.test(value)); + if (toolName) summarizeCliToolPayload(toolName, payload, collector); + } + } + + return { + messages, + filesModified: collector.getFilesModified(), + toolSummaries: collector.getSummaries(), + pendingTasks: [], + sessionNotes: { + compactSummary: 'Extracted from Antigravity CLI conversation SQLite using best-effort protobuf string decoding.', + }, + }; +} + async function discoverStateRecords(records: Map): Promise { for (const [id, state] of loadStateSummaries()) { addRecord(records, id, { state }); @@ -1105,24 +1404,34 @@ async function buildSessionFromRecord(record: AntigravityRecord): Promise(); + const cliStrings = cliSteps.flatMap(stringsFromStep); + const cliCwd = cliCwds.get(record.id) ?? extractCliCwdFromStrings(cliStrings); + const title = recordPreferredTitle(record) ?? (await titleFromBrain(record.brainDir)) ?? titleFromCliSteps(cliSteps); + const cwd = record.live?.cwd ?? record.state?.cwd ?? (await inferCwdFromBrain(record.brainDir)) ?? cliCwd ?? ''; const createdAt = record.live?.createdAt ?? record.state?.createdAt ?? stats.createdAt ?? stats.updatedAt ?? new Date(0); const updatedAt = record.live?.updatedAt ?? record.state?.updatedAt ?? stats.updatedAt ?? createdAt; const summary = cleanSummary(title ?? `Antigravity conversation ${record.id.slice(0, 8)}`, 80); - const originalPath = record.conversationPath ?? record.brainDir ?? getAntigravityRoot(); + const originalPath = record.cliConversationPath ?? record.conversationPath ?? record.brainDir ?? getAntigravityRoot(); return { id: record.id, source: SOURCE_NAME, cwd, repo: extractRepoFromCwd(cwd), - lines: record.live?.stepCount ?? record.state?.stepCount ?? artifactPaths.length, + lines: + record.live?.stepCount ?? + record.state?.stepCount ?? + (record.cliConversationPath ? readCliStepCount(record.cliConversationPath) : undefined) ?? + artifactPaths.length, bytes: stats.bytes, createdAt, updatedAt, @@ -1162,6 +1471,7 @@ async function buildLegacySession(filePath: string, prefixedId?: string): Promis async function discoverRecords(): Promise> { const records = new Map(); await discoverConversationRecords(records); + await discoverCliConversationRecords(records); await discoverBrainRecords(records); await discoverStateRecords(records); await discoverLiveRecords(records); @@ -1205,6 +1515,8 @@ async function resolveBrainDirForSession(session: UnifiedSession): Promise 0 || + fullAutoOccurrences.length > 0 || + approvalMode === 'yolo' || + askForApproval === 'never' + ) { + context.consume( + ...autoOccurrences, + ...fullAutoOccurrences, + ...askOccurrences, + ...approvalModeOccurrences, + ...permissionModeOccurrences, + ); + args.push('--dangerously-skip-permissions'); + + if (approvalModeOccurrences.length > 0 && approvalMode && approvalMode !== 'yolo') { + warnings.push('Antigravity: mapped auto-approve behavior overrides unsupported approval-mode values.'); + } + } else if (askOccurrences.length > 0 || approvalModeOccurrences.length > 0 || permissionModeOccurrences.length > 0) { + context.consume(...askOccurrences, ...approvalModeOccurrences, ...permissionModeOccurrences); + warnings.push('Antigravity: approval and permission mode forwarding flags are not supported and were ignored.'); + } + + const sandbox = context.latest('sandbox'); + if (sandbox) { + context.consumeKeys('sandbox'); + const normalized = String(sandbox.value).toLowerCase(); + if (sandbox.value === true || ['true', '1', 'yes', 'on', 'enabled'].includes(normalized)) { + args.push('--sandbox=true'); + } else if (['false', '0', 'no', 'off', 'disabled', 'none'].includes(normalized)) { + args.push('--sandbox=false'); + } else { + args.push('--sandbox', String(sandbox.value)); + } + } + + const model = context.latestString('model'); + if (model) { + context.consumeKeys('model'); + args.push('--model', model); + } + + return { mappedArgs: args, warnings }; +} + function mapClaudeFlags(context: ForwardFlagMapContext): ForwardMapResult { const args: string[] = []; const warnings: string[] = []; @@ -886,19 +943,21 @@ register({ // ── Antigravity ────────────────────────────────────────────────────── register({ name: 'antigravity', - label: 'Antigravity', + label: 'Antigravity CLI', color: chalk.hex('#A8DADC'), - storagePath: '~/.gemini/antigravity/', + storagePath: '~/.gemini/antigravity-cli/ (CLI), ~/.gemini/antigravity/ (IDE)', envVar: 'ANTIGRAVITY_HOME', // Antigravity's parser falls back to GEMINI_CLI_HOME when ANTIGRAVITY_HOME // is unset, so changes to that var must also invalidate the index cache. - extraEnvVars: ['GEMINI_CLI_HOME', 'ANTIGRAVITY_STATE_DB'], - binaryName: 'antigravity', + extraEnvVars: ['GEMINI_CLI_HOME', 'ANTIGRAVITY_CLI_HOME', 'ANTIGRAVITY_STATE_DB'], + binaryName: 'agy', + binaryFallbacks: ['antigravity'], parseSessions: parseAntigravitySessions, extractContext: extractAntigravityContext, - nativeResumeArgs: () => [], + nativeResumeArgs: (s) => ['--conversation', s.id], crossToolArgs: (prompt) => [prompt], - resumeCommandDisplay: () => `antigravity`, + resumeCommandDisplay: (s) => `agy --conversation ${s.id} (or: antigravity --conversation ${s.id})`, + mapHandoffFlags: mapAntigravityFlags, }); // ── Kimi CLI ──────────────────────────────────────────────────────────