diff --git a/eslint.config.js b/eslint.config.js index 99b1b28f4b..dc05e6ab42 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -52,6 +52,7 @@ export default tseslint.config( 'packages/test-utils/**', '.gemini/skills/**', '**/*.d.ts', + 'packages/core/src/teleportation/trajectory_teleporter.min.js', ], }, eslint.configs.recommended, diff --git a/packages/core/src/teleportation/discovery.ts b/packages/core/src/teleportation/discovery.ts new file mode 100644 index 0000000000..7cd9edf2b6 --- /dev/null +++ b/packages/core/src/teleportation/discovery.ts @@ -0,0 +1,101 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import fs from 'node:fs/promises'; +import path from 'node:path'; +import os from 'node:os'; +import { trajectoryToJson } from './teleporter.js'; +import { convertAgyToCliRecord } from './converter.js'; +import { partListUnionToString } from '../core/geminiRequest.js'; + +export interface AgySessionInfo { + id: string; + path: string; + mtime: string; + displayName?: string; + messageCount?: number; +} + +const AGY_CONVERSATIONS_DIR = path.join( + os.homedir(), + '.gemini', + 'jetski', + 'conversations', +); + +/** + * Lists all Antigravity sessions found on disk. + */ +export async function listAgySessions(): Promise { + try { + const files = await fs.readdir(AGY_CONVERSATIONS_DIR); + const sessions: AgySessionInfo[] = []; + + for (const file of files) { + if (file.endsWith('.pb')) { + const filePath = path.join(AGY_CONVERSATIONS_DIR, file); + const stats = await fs.stat(filePath); + const id = path.basename(file, '.pb'); + + let details = {}; + try { + const data = await fs.readFile(filePath); + const json = trajectoryToJson(data); + details = extractAgyDetails(json); + } catch (_error) { + // Ignore errors during parsing + } + + sessions.push({ + id, + path: filePath, + mtime: stats.mtime.toISOString(), + ...details, + }); + } + } + + return sessions; + } catch (_error) { + // If directory doesn't exist, just return empty list + return []; + } +} + +function extractAgyDetails(json: unknown): { + displayName?: string; + messageCount?: number; +} { + try { + const record = convertAgyToCliRecord(json); + const messages = record.messages || []; + + // Find first user message for display name + const firstUserMsg = messages.find((m) => m.type === 'user'); + const displayName = firstUserMsg + ? partListUnionToString(firstUserMsg.content).slice(0, 100) + : 'Antigravity Session'; + + return { + displayName, + messageCount: messages.length, + }; + } catch (_error) { + return {}; + } +} + +/** + * Loads the raw binary data of an Antigravity session. + */ +export async function loadAgySession(id: string): Promise { + const filePath = path.join(AGY_CONVERSATIONS_DIR, `${id}.pb`); + try { + return await fs.readFile(filePath); + } catch (_error) { + return null; + } +} diff --git a/packages/core/src/teleportation/teleporter.ts b/packages/core/src/teleportation/teleporter.ts new file mode 100644 index 0000000000..7365200940 --- /dev/null +++ b/packages/core/src/teleportation/teleporter.ts @@ -0,0 +1,26 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { createRequire } from 'node:module'; +const require = createRequire(import.meta.url); + +// Import the bundled teleporter which contains the protobuf definitions and decryption logic +// eslint-disable-next-line no-restricted-syntax +const teleporter = require('./trajectory_teleporter.min.js'); + +/** + * Decrypts and parses an Antigravity trajectory file (.pb) into JSON. + */ +export function trajectoryToJson(data: Buffer): unknown { + return teleporter.trajectoryToJson(data); +} + +/** + * Converts a JSON trajectory back to encrypted binary format. + */ +export function jsonToTrajectory(json: unknown): Buffer { + return teleporter.jsonToTrajectory(json); +}