diff --git a/docs/providers/antigravity.md b/docs/providers/antigravity.md index 4438c043..c308c5fc 100644 --- a/docs/providers/antigravity.md +++ b/docs/providers/antigravity.md @@ -13,7 +13,7 @@ CodeBurn discovers Antigravity sessions from local directories on disk, then que 1. **Session Discovery:** It scans the following folders for `.pb` or `.db` files: - **Antigravity CLI:** `%USERPROFILE%\.gemini\antigravity-cli\conversations` (and `implicit`) - **Antigravity App/older path:** `%USERPROFILE%\.gemini\antigravity\conversations` - - **Antigravity IDE (VSCode-based):** `%USERPROFILE%\.gemini\antigravity-ide\conversations` (and `implicit`) + - **Antigravity IDE:** `%USERPROFILE%\.gemini\antigravity-ide\conversations` (and `implicit`). The IDE also maintains VSCode-style global state at `%APPDATA%\Antigravity IDE\User\globalStorage\state.vscdb`, but that DB stores trajectory metadata (titles, timestamps, workspace paths) — not token usage. Token usage data still comes from the `.db` conversation files. 2. **Language Server RPC Query:** It locates the active language-server process via `ps` on POSIX or `Get-CimInstance Win32_Process` on Windows. It extracts the port and CSRF token from the process arguments, and queries the local HTTPS RPC endpoint `GetCascadeTrajectoryGeneratorMetadata` to parse the session. 3. **Cache Fallback:** If the language server is not running, it falls back to the local results cache. diff --git a/package-lock.json b/package-lock.json index d7b693f1..e649491f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "codeburn", - "version": "0.9.12", + "version": "0.9.15", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "codeburn", - "version": "0.9.12", + "version": "0.9.15", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "^1.29.0", diff --git a/src/main.ts b/src/main.ts index 10b4f632..79f87007 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1496,9 +1496,10 @@ program try { if (action === 'install') { const result = await installAntigravityStatusLineHook(!!opts.force) - console.log(result === 'already-installed' - ? '\n Antigravity CLI usage capture is already installed.\n' - : '\n Antigravity CLI usage capture installed.\n') + const headline = result === 'already-installed' + ? 'Antigravity CLI usage capture is already installed.' + : 'Antigravity CLI usage capture installed.' + console.log(`\n ${headline}\n Note: this captures CLI (agy) sessions only. IDE sessions are read from .db files automatically.\n`) return } if (action === 'uninstall') { diff --git a/src/providers/antigravity.ts b/src/providers/antigravity.ts index 710b4bf7..a5e2872d 100644 --- a/src/providers/antigravity.ts +++ b/src/providers/antigravity.ts @@ -43,7 +43,7 @@ const CONVERSATION_ROOTS: readonly AntigravityConversationRoot[] = [ extensions: ['.pb'], }, ] as const -const CACHE_VERSION = 2 +const CACHE_VERSION = 4 const RPC_TIMEOUT_MS = 5000 const MAX_RESPONSE_BYTES = 16 * 1024 * 1024 @@ -468,6 +468,10 @@ export function antigravityAppDataDirFromSourcePath(path: string): 'antigravity' const lower = path.replace(/\\/g, '/').toLowerCase() if (lower.includes('/.gemini/antigravity-ide/')) return 'antigravity-ide' if (lower.includes('/.gemini/antigravity-cli/')) return 'antigravity-cli' + // APPDATA-style install dirs (e.g. `%APPDATA%\Antigravity IDE\...`) — checked + // after the .gemini roots so a profile directory containing "Antigravity IDE" + // cannot misclassify CLI conversation paths. + if (lower.includes('/antigravity ide/')) return 'antigravity-ide' return 'antigravity' } @@ -1197,6 +1201,24 @@ function applyAntigravityProject(call: ParsedProviderCall, source: SessionSource call.project = source.project } +// gen_metadata rows and some RPC entries (missing chatStartMetadata.createdAt) +// carry no per-call timestamp. Left empty, those calls are silently dropped by +// the date-range filters in parser.ts (`if (!callTs) continue`). Every emission +// path stamps the conversation file's mtime as a fallback: all calls in the +// session then share the file's last-write time — best available, since the +// source data has no per-call times. Returns true when a call was repaired so +// cache-hit paths can persist the fix. +function stampFallbackTimestamp(calls: ParsedProviderCall[], fallbackTimestamp: string): boolean { + let repaired = false + for (const call of calls) { + if (!call.timestamp) { + call.timestamp = fallbackTimestamp + repaired = true + } + } + return repaired +} + function createParser(source: SessionSource, seenKeys: Set): SessionParser { return { async *parse(): AsyncGenerator { @@ -1215,9 +1237,11 @@ function createParser(source: SessionSource, seenKeys: Set): SessionPars if (!s) return const projectPath = await extractWorkspacePath(source.path) + const fallbackTimestamp = new Date(s.mtimeMs).toISOString() const cached = cache.cascades[cascadeId] if (cached && cached.mtimeMs === s.mtimeMs && cached.sizeBytes === s.size && cached.calls.length > 0) { + if (stampFallbackTimestamp(cached.calls, fallbackTimestamp)) cacheDirty = true for (const call of cached.calls) { applyAntigravityProject(call, source, projectPath) if (seenKeys.has(call.deduplicationKey)) continue @@ -1229,6 +1253,7 @@ function createParser(source: SessionSource, seenKeys: Set): SessionPars const sqliteResults = await parseSqliteGenMetadataCalls(source.path, cascadeId) if (sqliteResults.length > 0) { + stampFallbackTimestamp(sqliteResults, fallbackTimestamp) for (const call of sqliteResults) { applyAntigravityProject(call, source, projectPath) } @@ -1251,6 +1276,7 @@ function createParser(source: SessionSource, seenKeys: Set): SessionPars const server = await detectServer(antigravityAppDataDirFromSourcePath(source.path)) if (!server) { if (cached) { + if (stampFallbackTimestamp(cached.calls, fallbackTimestamp)) cacheDirty = true for (const call of cached.calls) { applyAntigravityProject(call, source, projectPath) if (seenKeys.has(call.deduplicationKey)) continue @@ -1270,6 +1296,7 @@ function createParser(source: SessionSource, seenKeys: Set): SessionPars ) } catch { if (cached) { + if (stampFallbackTimestamp(cached.calls, fallbackTimestamp)) cacheDirty = true for (const call of cached.calls) { applyAntigravityProject(call, source, projectPath) if (seenKeys.has(call.deduplicationKey)) continue @@ -1281,6 +1308,7 @@ function createParser(source: SessionSource, seenKeys: Set): SessionPars } const results = buildCallsFromGeneratorMetadata(cascadeId, metadata, modelMap) + stampFallbackTimestamp(results, fallbackTimestamp) for (const call of results) { applyAntigravityProject(call, source, projectPath) } diff --git a/src/session-cache.ts b/src/session-cache.ts index abfb41c8..79424112 100644 --- a/src/session-cache.ts +++ b/src/session-cache.ts @@ -118,7 +118,7 @@ const PROVIDER_PARSE_VERSIONS: Record = { 'kilo-code': 'worktree-project-grouping-v1', 'roo-code': 'worktree-project-grouping-v1', warp: 'worktree-project-grouping-v1', - antigravity: 'worktree-project-grouping-v3', + antigravity: 'worktree-project-grouping-v5', } // ── Cache Dir ────────────────────────────────────────────────────────── diff --git a/tests/providers/antigravity.test.ts b/tests/providers/antigravity.test.ts index e98fd591..255d4249 100644 --- a/tests/providers/antigravity.test.ts +++ b/tests/providers/antigravity.test.ts @@ -1,4 +1,4 @@ -import { mkdtemp, mkdir, readFile, rm, writeFile } from 'fs/promises' +import { mkdtemp, mkdir, readFile, rm, stat, writeFile } from 'fs/promises' import { tmpdir } from 'os' import { join } from 'path' import { createRequire } from 'node:module' @@ -605,4 +605,74 @@ describe('antigravity provider helpers', () => { await rm(tempHome, { recursive: true, force: true }) } }) + + async function withTempAntigravityHome(prefix: string, fn: (tempHome: string) => Promise): Promise { + const tempHome = await mkdtemp(join(tmpdir(), prefix)) + const previousCacheDir = process.env['CODEBURN_CACHE_DIR'] + process.env['CODEBURN_CACHE_DIR'] = join(tempHome, 'cache') + try { + await fn(tempHome) + } finally { + if (previousCacheDir === undefined) delete process.env['CODEBURN_CACHE_DIR'] + else process.env['CODEBURN_CACHE_DIR'] = previousCacheDir + await rm(tempHome, { recursive: true, force: true }) + } + } + + it('stamps file mtime as fallback timestamp for SQLite-parsed calls', async () => { + if (!isSqliteAvailable()) return + + await withTempAntigravityHome('codeburn-antigravity-timestamp-', async (tempHome) => { + const fixture = JSON.parse(await readFile( + new URL('../fixtures/antigravity-cli-current/gen-metadata.json', import.meta.url), + 'utf-8', + )) as CurrentCliFixture + const conversationsDir = join(tempHome, '.gemini', 'antigravity-ide', 'conversations') + + await mkdir(conversationsDir, { recursive: true }) + + const dbPath = join(conversationsDir, `${fixture.conversationId}.db`) + createCurrentAntigravityCliDb(dbPath, fixture) + + const beforeStat = await stat(dbPath) + + const parser = createAntigravityProvider().createSessionParser({ + path: dbPath, + project: 'antigravity-ide', + provider: 'antigravity', + }, new Set()) + const calls: ParsedProviderCall[] = [] + for await (const call of parser.parse()) calls.push(call) + + expect(calls.length).toBeGreaterThan(0) + for (const call of calls) { + expect(call.timestamp).not.toBe('') + const callTime = new Date(call.timestamp).getTime() + expect(Math.abs(callTime - beforeStat.mtimeMs)).toBeLessThan(5000) + } + }) + }) + + it('classifies APPDATA Antigravity IDE paths as antigravity-ide', () => { + expect(antigravityAppDataDirFromSourcePath( + 'C:\\Users\\User\\AppData\\Roaming\\Antigravity IDE\\User\\globalStorage\\state.vscdb', + )).toBe('antigravity-ide') + + expect(antigravityAppDataDirFromSourcePath( + '/Users/User/.gemini/antigravity-ide/conversations/abc.db', + )).toBe('antigravity-ide') + + expect(antigravityAppDataDirFromSourcePath( + '/Users/User/.gemini/antigravity-cli/conversations/abc.db', + )).toBe('antigravity-cli') + + // .gemini roots take precedence over the broad "Antigravity IDE" match. + expect(antigravityAppDataDirFromSourcePath( + 'C:\\Users\\Antigravity IDE\\.gemini\\antigravity-cli\\conversations\\abc.db', + )).toBe('antigravity-cli') + + expect(antigravityAppDataDirFromSourcePath( + '/Users/User/.gemini/antigravity/conversations/abc.db', + )).toBe('antigravity') + }) })