mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-28 05:55:17 -07:00
feat(core): implement core teleporter module
This commit is contained in:
@@ -52,6 +52,7 @@ export default tseslint.config(
|
|||||||
'packages/test-utils/**',
|
'packages/test-utils/**',
|
||||||
'.gemini/skills/**',
|
'.gemini/skills/**',
|
||||||
'**/*.d.ts',
|
'**/*.d.ts',
|
||||||
|
'packages/core/src/teleportation/trajectory_teleporter.min.js',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
eslint.configs.recommended,
|
eslint.configs.recommended,
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"bundle:browser-mcp": "node scripts/bundle-browser-mcp.mjs",
|
"bundle:browser-mcp": "node scripts/bundle-browser-mcp.mjs",
|
||||||
"build": "node ../../scripts/build_package.js",
|
"build": "node ../../scripts/build_package.js",
|
||||||
|
"build:teleporter": "./scripts/build-teleporter.sh",
|
||||||
"lint": "eslint . --ext .ts,.tsx",
|
"lint": "eslint . --ext .ts,.tsx",
|
||||||
"format": "prettier --write .",
|
"format": "prettier --write .",
|
||||||
"test": "vitest run",
|
"test": "vitest run",
|
||||||
|
|||||||
Executable
+34
@@ -0,0 +1,34 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Define paths
|
||||||
|
CLI_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)"
|
||||||
|
EXA_ROOT="$(cd "$CLI_ROOT/../jetski/Exafunction" && pwd)"
|
||||||
|
TELEPORTER_TS="$CLI_ROOT/packages/core/src/teleportation/trajectory_teleporter.ts"
|
||||||
|
TELEPORTER_MIN_JS="$CLI_ROOT/packages/core/src/teleportation/trajectory_teleporter.min.js"
|
||||||
|
|
||||||
|
if [ ! -d "$EXA_ROOT" ]; then
|
||||||
|
echo "Error: Exafunction directory not found at $EXA_ROOT"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Building Protobuf JS definitions in Exafunction..."
|
||||||
|
cd "$EXA_ROOT"
|
||||||
|
pnpm --dir exa/proto_ts build
|
||||||
|
|
||||||
|
echo "Bundling and minifying trajectory_teleporter.ts..."
|
||||||
|
# Because esbuild resolves relative imports from the source file's directory,
|
||||||
|
# and trajectory_teleporter.ts playfully imports './exa/...', we copy it to EXA_ROOT
|
||||||
|
# temporarily for the build step to succeed.
|
||||||
|
cp "$TELEPORTER_TS" "$EXA_ROOT/trajectory_teleporter_tmp.ts"
|
||||||
|
|
||||||
|
cd "$EXA_ROOT"
|
||||||
|
pnpm dlx esbuild "./trajectory_teleporter_tmp.ts" \
|
||||||
|
--bundle \
|
||||||
|
--format=esm \
|
||||||
|
--platform=node \
|
||||||
|
--outfile="$TELEPORTER_MIN_JS"
|
||||||
|
|
||||||
|
rm "$EXA_ROOT/trajectory_teleporter_tmp.ts"
|
||||||
|
|
||||||
|
echo "Done! Wrote bundle to $TELEPORTER_MIN_JS"
|
||||||
@@ -221,6 +221,8 @@ export * from './telemetry/constants.js';
|
|||||||
export { sessionId, createSessionId } from './utils/session.js';
|
export { sessionId, createSessionId } from './utils/session.js';
|
||||||
export * from './utils/compatibility.js';
|
export * from './utils/compatibility.js';
|
||||||
export * from './utils/browser.js';
|
export * from './utils/browser.js';
|
||||||
|
export * from './teleportation/index.js';
|
||||||
|
|
||||||
export { Storage } from './config/storage.js';
|
export { Storage } from './config/storage.js';
|
||||||
|
|
||||||
// Export hooks system
|
// Export hooks system
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2026 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { listAgySessions, loadAgySession } from './discovery.js';
|
||||||
|
import { trajectoryToJson } from './teleporter.js';
|
||||||
|
import { convertAgyToCliRecord } from './converter.js';
|
||||||
|
import type {
|
||||||
|
TrajectoryProvider,
|
||||||
|
ConversationRecord,
|
||||||
|
} from '../config/config.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trajectory provider for Antigravity (Jetski) sessions.
|
||||||
|
*/
|
||||||
|
const agyProvider: TrajectoryProvider = {
|
||||||
|
prefix: 'agy:',
|
||||||
|
displayName: 'Antigravity',
|
||||||
|
|
||||||
|
async listSessions(workspaceUri?: string) {
|
||||||
|
const sessions = await listAgySessions(workspaceUri);
|
||||||
|
return sessions.map((s) => ({
|
||||||
|
id: s.id,
|
||||||
|
mtime: s.mtime,
|
||||||
|
displayName: s.displayName,
|
||||||
|
messageCount: s.messageCount,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
|
||||||
|
async loadSession(id: string): Promise<ConversationRecord | null> {
|
||||||
|
const data = await loadAgySession(id);
|
||||||
|
if (!data) return null;
|
||||||
|
const json = trajectoryToJson(data);
|
||||||
|
return convertAgyToCliRecord(json);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line import/no-default-export
|
||||||
|
export default agyProvider;
|
||||||
@@ -0,0 +1,278 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2025 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-type-assertion */
|
||||||
|
|
||||||
|
import type {
|
||||||
|
ConversationRecord,
|
||||||
|
ToolCallRecord,
|
||||||
|
MessageRecord,
|
||||||
|
} from '../services/chatRecordingService.js';
|
||||||
|
import { CoreToolCallStatus } from '../scheduler/types.js';
|
||||||
|
import {
|
||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-return */
|
||||||
|
|
||||||
|
EDIT_TOOL_NAME,
|
||||||
|
GLOB_TOOL_NAME,
|
||||||
|
GREP_TOOL_NAME,
|
||||||
|
LS_TOOL_NAME,
|
||||||
|
READ_FILE_TOOL_NAME,
|
||||||
|
SHELL_TOOL_NAME,
|
||||||
|
WEB_FETCH_TOOL_NAME,
|
||||||
|
WEB_SEARCH_TOOL_NAME,
|
||||||
|
WRITE_FILE_TOOL_NAME,
|
||||||
|
ASK_USER_TOOL_NAME,
|
||||||
|
} from '../tools/definitions/coreTools.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts an Antigravity Trajectory JSON to a Gemini CLI ConversationRecord.
|
||||||
|
*/
|
||||||
|
export function convertAgyToCliRecord(agyJson: unknown): ConversationRecord {
|
||||||
|
if (typeof agyJson !== 'object' || agyJson === null) {
|
||||||
|
throw new Error('Invalid AGY JSON');
|
||||||
|
}
|
||||||
|
const json = agyJson as Record<string, unknown>;
|
||||||
|
const messages: MessageRecord[] = [];
|
||||||
|
const sessionId = (json['trajectoryId'] as string) || 'agy-session';
|
||||||
|
const startTime = new Date().toISOString(); // Default to now if not found
|
||||||
|
|
||||||
|
let currentGeminiMessage: (MessageRecord & { type: 'gemini' }) | null = null;
|
||||||
|
|
||||||
|
const steps = (json['steps'] as any[]) || [];
|
||||||
|
|
||||||
|
for (const step of steps) {
|
||||||
|
const s = step as Record<string, unknown>;
|
||||||
|
const metadata = s['metadata'] as Record<string, unknown> | undefined;
|
||||||
|
const timestamp =
|
||||||
|
(metadata?.['timestamp'] as string) || new Date().toISOString();
|
||||||
|
const stepId =
|
||||||
|
(metadata?.['stepId'] as string) ||
|
||||||
|
Math.random().toString(36).substring(7);
|
||||||
|
|
||||||
|
switch (s['type']) {
|
||||||
|
case 14: // CORTEX_STEP_TYPE_USER_INPUT
|
||||||
|
case 'CORTEX_STEP_TYPE_USER_INPUT': {
|
||||||
|
// Close current Gemini message if open
|
||||||
|
currentGeminiMessage = null;
|
||||||
|
const userInput = s['userInput'] as Record<string, unknown> | undefined;
|
||||||
|
messages.push({
|
||||||
|
id: stepId,
|
||||||
|
timestamp,
|
||||||
|
type: 'user',
|
||||||
|
content: [{ text: (userInput?.['userResponse'] as string) || '' }],
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 15: // CORTEX_STEP_TYPE_PLANNER_RESPONSE
|
||||||
|
case 'CORTEX_STEP_TYPE_PLANNER_RESPONSE': {
|
||||||
|
const plannerResponse = s['plannerResponse'] as
|
||||||
|
| Record<string, unknown>
|
||||||
|
| undefined;
|
||||||
|
const response = plannerResponse?.['response'] || '';
|
||||||
|
const thinking = plannerResponse?.['thinking'] || '';
|
||||||
|
currentGeminiMessage = {
|
||||||
|
id: stepId,
|
||||||
|
timestamp,
|
||||||
|
type: 'gemini',
|
||||||
|
content: [{ text: response as string }],
|
||||||
|
thoughts: thinking
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
subject: 'Thinking',
|
||||||
|
description: thinking as string,
|
||||||
|
timestamp,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: [],
|
||||||
|
toolCalls: [],
|
||||||
|
};
|
||||||
|
messages.push(currentGeminiMessage);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 7: // CORTEX_STEP_TYPE_GREP_SEARCH
|
||||||
|
case 'CORTEX_STEP_TYPE_GREP_SEARCH':
|
||||||
|
case 8: // CORTEX_STEP_TYPE_VIEW_FILE
|
||||||
|
case 'CORTEX_STEP_TYPE_VIEW_FILE':
|
||||||
|
case 9: // CORTEX_STEP_TYPE_LIST_DIRECTORY
|
||||||
|
case 'CORTEX_STEP_TYPE_LIST_DIRECTORY':
|
||||||
|
case 21: // CORTEX_STEP_TYPE_RUN_COMMAND
|
||||||
|
case 'CORTEX_STEP_TYPE_RUN_COMMAND':
|
||||||
|
case 23: // CORTEX_STEP_TYPE_WRITE_TO_FILE
|
||||||
|
case 'CORTEX_STEP_TYPE_WRITE_TO_FILE':
|
||||||
|
case 25: // CORTEX_STEP_TYPE_FIND
|
||||||
|
case 'CORTEX_STEP_TYPE_FIND':
|
||||||
|
case 31: // CORTEX_STEP_TYPE_READ_URL_CONTENT
|
||||||
|
case 'CORTEX_STEP_TYPE_READ_URL_CONTENT':
|
||||||
|
case 33: // CORTEX_STEP_TYPE_SEARCH_WEB
|
||||||
|
case 'CORTEX_STEP_TYPE_SEARCH_WEB':
|
||||||
|
case 38: // CORTEX_STEP_TYPE_MCP_TOOL
|
||||||
|
case 'CORTEX_STEP_TYPE_MCP_TOOL':
|
||||||
|
case 85: // CORTEX_STEP_TYPE_BROWSER_SUBAGENT
|
||||||
|
case 'CORTEX_STEP_TYPE_BROWSER_SUBAGENT':
|
||||||
|
case 86: // CORTEX_STEP_TYPE_FILE_CHANGE
|
||||||
|
case 'CORTEX_STEP_TYPE_FILE_CHANGE':
|
||||||
|
case 140: // CORTEX_STEP_TYPE_GENERIC
|
||||||
|
case 'CORTEX_STEP_TYPE_GENERIC': {
|
||||||
|
if (!currentGeminiMessage) {
|
||||||
|
// If no planner response preceded this, create a dummy one
|
||||||
|
const adjunctMessage: MessageRecord = {
|
||||||
|
id: `adjunct-${stepId}`,
|
||||||
|
timestamp,
|
||||||
|
type: 'gemini',
|
||||||
|
content: [],
|
||||||
|
toolCalls: [],
|
||||||
|
thoughts: [],
|
||||||
|
};
|
||||||
|
messages.push(adjunctMessage);
|
||||||
|
currentGeminiMessage = adjunctMessage as MessageRecord & {
|
||||||
|
type: 'gemini';
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentGeminiMessage) {
|
||||||
|
currentGeminiMessage.toolCalls?.push(mapAgyStepToToolCall(s));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
// Skip unknown steps
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
sessionId,
|
||||||
|
projectHash: 'agy-imported',
|
||||||
|
startTime,
|
||||||
|
lastUpdated: new Date().toISOString(),
|
||||||
|
messages,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapAgyStepToToolCall(step: Record<string, any>): ToolCallRecord {
|
||||||
|
const timestamp =
|
||||||
|
(step['metadata']?.['timestamp'] as string) || new Date().toISOString();
|
||||||
|
const id =
|
||||||
|
(step['metadata']?.['stepId'] as string) ||
|
||||||
|
Math.random().toString(36).substring(7);
|
||||||
|
let name = 'unknown_tool';
|
||||||
|
let args: any = {};
|
||||||
|
let result: any = null;
|
||||||
|
|
||||||
|
if (step['viewFile']) {
|
||||||
|
name = READ_FILE_TOOL_NAME;
|
||||||
|
args = { AbsolutePath: step['viewFile']['absolutePathUri'] };
|
||||||
|
result = [{ text: step['viewFile']['content'] || '' }];
|
||||||
|
} else if (step['listDirectory']) {
|
||||||
|
name = LS_TOOL_NAME;
|
||||||
|
args = { DirectoryPath: step['listDirectory']['directoryPathUri'] };
|
||||||
|
} else if (step['grepSearch']) {
|
||||||
|
name = GREP_TOOL_NAME;
|
||||||
|
args = {
|
||||||
|
Query: step['grepSearch']['query'],
|
||||||
|
SearchPath: step['grepSearch']['searchPathUri'],
|
||||||
|
};
|
||||||
|
result = [{ text: step['grepSearch']['rawOutput'] || '' }];
|
||||||
|
} else if (step['runCommand']) {
|
||||||
|
name = SHELL_TOOL_NAME;
|
||||||
|
args = { CommandLine: step['runCommand']['commandLine'] };
|
||||||
|
result = [{ text: step['runCommand']['combinedOutput']?.['full'] || '' }];
|
||||||
|
} else if (step['fileChange']) {
|
||||||
|
name = EDIT_TOOL_NAME; // Or multi_replace_file_content
|
||||||
|
args = { TargetFile: step['fileChange']['absolutePathUri'] };
|
||||||
|
} else if (step['writeToFile']) {
|
||||||
|
name = WRITE_FILE_TOOL_NAME;
|
||||||
|
args = { TargetFile: step['writeToFile']['targetFileUri'] };
|
||||||
|
} else if (step['find']) {
|
||||||
|
name = GLOB_TOOL_NAME;
|
||||||
|
args = {
|
||||||
|
Pattern: step['find']['pattern'],
|
||||||
|
SearchDirectory: step['find']['searchDirectory'],
|
||||||
|
};
|
||||||
|
result = [{ text: step['find']['truncatedOutput'] || '' }];
|
||||||
|
} else if (step['readUrlContent']) {
|
||||||
|
name = WEB_FETCH_TOOL_NAME;
|
||||||
|
args = { Url: step['readUrlContent']['url'] };
|
||||||
|
// We intentionally don't try fully mapping the complex KnowledgeBaseItem struct into a string here
|
||||||
|
result = [{ text: 'successfully read url content' }];
|
||||||
|
} else if (step['searchWeb']) {
|
||||||
|
name = WEB_SEARCH_TOOL_NAME; // Usually mapped from 'searchWeb'
|
||||||
|
args = { query: step['searchWeb']['query'] };
|
||||||
|
if (step['searchWeb']['domain']) {
|
||||||
|
args['domain'] = step['searchWeb']['domain'];
|
||||||
|
}
|
||||||
|
result = [{ text: 'successfully searched web' }];
|
||||||
|
} else if (step['mcpTool']) {
|
||||||
|
const mcpStep = step['mcpTool'];
|
||||||
|
name = mcpStep['toolCall']?.['name'] || 'unknown_mcp_tool';
|
||||||
|
try {
|
||||||
|
if (mcpStep['toolCall']?.['arguments']) {
|
||||||
|
args = JSON.parse(mcpStep['toolCall']['arguments']);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
args = {};
|
||||||
|
}
|
||||||
|
result = [{ text: mcpStep['resultString'] || '' }];
|
||||||
|
} else if (step['browserSubagent']) {
|
||||||
|
name = 'browser_subagent';
|
||||||
|
args = { Task: step['browserSubagent']['task'] };
|
||||||
|
} else if (step['generic']) {
|
||||||
|
const generic = step['generic'] as Record<string, unknown>;
|
||||||
|
const rawName = generic['toolName'] as string;
|
||||||
|
|
||||||
|
// Map generic tools to official CLI constants where applicable
|
||||||
|
if (rawName === 'ask_user') {
|
||||||
|
name = ASK_USER_TOOL_NAME;
|
||||||
|
} else {
|
||||||
|
name = rawName;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
args = JSON.parse(generic['argsJson'] as string);
|
||||||
|
} catch {
|
||||||
|
args = {};
|
||||||
|
}
|
||||||
|
result = [{ text: (generic['responseJson'] as string) || '' }];
|
||||||
|
}
|
||||||
|
|
||||||
|
const safeArgs = args as Record<string, unknown>;
|
||||||
|
const status =
|
||||||
|
step['status'] === 3 || step['status'] === 'CORTEX_STEP_STATUS_DONE'
|
||||||
|
? CoreToolCallStatus.Success
|
||||||
|
: CoreToolCallStatus.Error;
|
||||||
|
|
||||||
|
// Synthesize a UI string from the args so it isn't blank in the terminal
|
||||||
|
const argValues = Object.values(safeArgs)
|
||||||
|
.filter((v) => typeof v === 'string' || typeof v === 'number')
|
||||||
|
.join(', ');
|
||||||
|
const description = argValues || '';
|
||||||
|
|
||||||
|
// Synthesize a UI string for the result output
|
||||||
|
let resultDisplay: string | undefined = undefined;
|
||||||
|
if (Array.isArray(result) && result.length > 0) {
|
||||||
|
const textParts = result
|
||||||
|
.map((part) => part?.text)
|
||||||
|
.filter((text) => typeof text === 'string' && text.length > 0);
|
||||||
|
|
||||||
|
if (textParts.length > 0) {
|
||||||
|
resultDisplay = textParts.join('\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
args: safeArgs,
|
||||||
|
description,
|
||||||
|
result,
|
||||||
|
resultDisplay,
|
||||||
|
status,
|
||||||
|
timestamp,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,199 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2025 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-type-assertion */
|
||||||
|
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';
|
||||||
|
import type { MessageRecord } from '../services/chatRecordingService.js';
|
||||||
|
|
||||||
|
export interface AgySessionInfo {
|
||||||
|
id: string;
|
||||||
|
path: string;
|
||||||
|
mtime: string;
|
||||||
|
displayName?: string;
|
||||||
|
messageCount?: number;
|
||||||
|
workspaceUri?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AGY_CONVERSATIONS_DIR = path.join(
|
||||||
|
os.homedir(),
|
||||||
|
'.gemini',
|
||||||
|
'jetski',
|
||||||
|
'conversations',
|
||||||
|
);
|
||||||
|
|
||||||
|
const AGY_KEY_PATH = path.join(os.homedir(), '.gemini', 'jetski', 'key.txt');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads the Antigravity encryption key.
|
||||||
|
* Priority: JETSKI_TELEPORT_KEY env var > ~/.gemini/jetski/key.txt
|
||||||
|
*/
|
||||||
|
export async function loadAgyKey(): Promise<Buffer | undefined> {
|
||||||
|
const envKey = process.env['JETSKI_TELEPORT_KEY'];
|
||||||
|
if (envKey) {
|
||||||
|
return Buffer.from(envKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const keyContent = await fs.readFile(AGY_KEY_PATH, 'utf-8');
|
||||||
|
return Buffer.from(keyContent.trim());
|
||||||
|
} catch (_e) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lists all Antigravity sessions found on disk.
|
||||||
|
* @param filterWorkspaceUri Optional filter to only return sessions matching this workspace URI (e.g. "file:///...").
|
||||||
|
*/
|
||||||
|
export async function listAgySessions(
|
||||||
|
filterWorkspaceUri?: string,
|
||||||
|
): Promise<AgySessionInfo[]> {
|
||||||
|
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: ReturnType<typeof extractAgyDetails> = {};
|
||||||
|
try {
|
||||||
|
const data = await fs.readFile(filePath);
|
||||||
|
const json = trajectoryToJson(data);
|
||||||
|
details = extractAgyDetails(json);
|
||||||
|
} catch (_error) {
|
||||||
|
// Ignore errors during parsing
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
filterWorkspaceUri &&
|
||||||
|
details.workspaceUri &&
|
||||||
|
details.workspaceUri !== filterWorkspaceUri
|
||||||
|
) {
|
||||||
|
continue; // Skip sessions from other workspaces if we have a filter
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
workspaceUri?: string;
|
||||||
|
} {
|
||||||
|
try {
|
||||||
|
const record = convertAgyToCliRecord(json);
|
||||||
|
const messages = record.messages || [];
|
||||||
|
|
||||||
|
// Find first user message for display name
|
||||||
|
const firstUserMsg = messages.find((m: MessageRecord) => m.type === 'user');
|
||||||
|
const displayName = firstUserMsg
|
||||||
|
? partListUnionToString(firstUserMsg.content).slice(0, 100)
|
||||||
|
: 'Antigravity Session';
|
||||||
|
|
||||||
|
// Attempt to extract authoritative workspace object from top-level metadata first
|
||||||
|
let workspaceUri: string | undefined;
|
||||||
|
const agyJson = json as Record<string, unknown>;
|
||||||
|
|
||||||
|
const metadata = agyJson['metadata'] as Record<string, unknown> | undefined;
|
||||||
|
if (metadata) {
|
||||||
|
const workspaces = metadata['workspaces'] as
|
||||||
|
| Array<Record<string, unknown>>
|
||||||
|
| undefined;
|
||||||
|
const firstWorkspace = workspaces?.[0];
|
||||||
|
if (firstWorkspace && firstWorkspace['workspaceFolderAbsoluteUri']) {
|
||||||
|
workspaceUri = firstWorkspace['workspaceFolderAbsoluteUri'] as string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: Attempt to extract workspace object from raw JSON steps (e.g. older offline trajectories)
|
||||||
|
if (!workspaceUri) {
|
||||||
|
const steps = (agyJson['steps'] as Array<Record<string, unknown>>) || [];
|
||||||
|
for (const step of steps) {
|
||||||
|
const userInput = step['userInput'] as
|
||||||
|
| Record<string, unknown>
|
||||||
|
| undefined;
|
||||||
|
if (userInput) {
|
||||||
|
const activeState = userInput['activeUserState'] as
|
||||||
|
| Record<string, unknown>
|
||||||
|
| undefined;
|
||||||
|
const activeDoc = activeState?.['activeDocument'] as
|
||||||
|
| Record<string, unknown>
|
||||||
|
| undefined;
|
||||||
|
if (activeDoc && activeDoc['workspaceUri']) {
|
||||||
|
workspaceUri = activeDoc['workspaceUri'] as string;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
displayName,
|
||||||
|
messageCount: messages.length,
|
||||||
|
workspaceUri,
|
||||||
|
};
|
||||||
|
} catch (_error) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads the raw binary data of an Antigravity session.
|
||||||
|
*/
|
||||||
|
export async function loadAgySession(id: string): Promise<Buffer | null> {
|
||||||
|
const filePath = path.join(AGY_CONVERSATIONS_DIR, `${id}.pb`);
|
||||||
|
try {
|
||||||
|
return await fs.readFile(filePath);
|
||||||
|
} catch (_error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the most recent session if it was updated within the last 10 minutes.
|
||||||
|
*/
|
||||||
|
export async function getRecentAgySession(
|
||||||
|
workspaceUri?: string,
|
||||||
|
): Promise<AgySessionInfo | null> {
|
||||||
|
const sessions = await listAgySessions(workspaceUri);
|
||||||
|
if (sessions.length === 0) return null;
|
||||||
|
|
||||||
|
// Sort by mtime descending
|
||||||
|
const sorted = sessions.sort(
|
||||||
|
(a, b) => new Date(b.mtime).getTime() - new Date(a.mtime).getTime(),
|
||||||
|
);
|
||||||
|
|
||||||
|
const mostRecent = sorted[0];
|
||||||
|
const mtime = new Date(mostRecent.mtime).getTime();
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
// 10 minutes threshold
|
||||||
|
if (now - mtime < 10 * 60 * 1000) {
|
||||||
|
return mostRecent;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2025 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface AgyTrajectory {
|
||||||
|
trajectoryId: string;
|
||||||
|
cascadeId: string;
|
||||||
|
trajectoryType: number;
|
||||||
|
steps: unknown[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export * from './teleporter.js';
|
||||||
|
export { convertAgyToCliRecord } from './converter.js';
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2025 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as teleporter from './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);
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2026 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function trajectoryToJson(data: Buffer): unknown;
|
||||||
|
export function jsonToTrajectory(json: unknown): Buffer;
|
||||||
|
export function decrypt(data: Buffer): unknown;
|
||||||
|
export function encrypt(data: Buffer): Buffer;
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,79 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2026 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-nocheck
|
||||||
|
import * as crypto from 'node:crypto';
|
||||||
|
import { Trajectory } from './exa/proto_ts/dist/exa/gemini_coder/proto/trajectory_pb.js';
|
||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||||
|
|
||||||
|
const DEFAULT_KEY = Buffer.from('safeCodeiumworldKeYsecretBalloon');
|
||||||
|
const NONCE_SIZE = 12; // GCM default nonce size
|
||||||
|
const TAG_SIZE = 16; // GCM default tag size
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decrypts data using AES-256-GCM.
|
||||||
|
* The data is expected to be in the format: [nonce (12b)][ciphertext][tag (16b)]
|
||||||
|
*/
|
||||||
|
export function decrypt(data: Buffer, key: Buffer = DEFAULT_KEY): Buffer {
|
||||||
|
if (data.length < NONCE_SIZE + TAG_SIZE) {
|
||||||
|
throw new Error('Data too short');
|
||||||
|
}
|
||||||
|
|
||||||
|
const nonce = data.subarray(0, NONCE_SIZE);
|
||||||
|
const tag = data.subarray(data.length - TAG_SIZE);
|
||||||
|
const ciphertext = data.subarray(NONCE_SIZE, data.length - TAG_SIZE);
|
||||||
|
|
||||||
|
const decipher = crypto.createDecipheriv('aes-256-gcm', key, nonce);
|
||||||
|
decipher.setAuthTag(tag);
|
||||||
|
|
||||||
|
return Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encrypts data using AES-256-GCM.
|
||||||
|
* Returns data in the format: [nonce (12b)][ciphertext][tag (16b)]
|
||||||
|
*/
|
||||||
|
export function encrypt(data: Buffer, key: Buffer = DEFAULT_KEY): Buffer {
|
||||||
|
const nonce = crypto.randomBytes(NONCE_SIZE);
|
||||||
|
const cipher = crypto.createCipheriv('aes-256-gcm', key, nonce);
|
||||||
|
|
||||||
|
const ciphertext = Buffer.concat([cipher.update(data), cipher.final()]);
|
||||||
|
const tag = cipher.getAuthTag();
|
||||||
|
|
||||||
|
return Buffer.concat([nonce, ciphertext, tag]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts Antigravity binary trajectory to JSON.
|
||||||
|
*/
|
||||||
|
export function trajectoryToJson(
|
||||||
|
data: Buffer,
|
||||||
|
key: Buffer = DEFAULT_KEY,
|
||||||
|
): unknown {
|
||||||
|
let pbData: Buffer;
|
||||||
|
try {
|
||||||
|
// Try to decrypt first
|
||||||
|
pbData = decrypt(data, key);
|
||||||
|
} catch (_e) {
|
||||||
|
// Fallback to plain protobuf if decryption fails
|
||||||
|
pbData = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
const trajectory = Trajectory.fromBinary(pbData);
|
||||||
|
return trajectory.toJson();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts JSON to Antigravity binary trajectory (encrypted).
|
||||||
|
*/
|
||||||
|
export function jsonToTrajectory(
|
||||||
|
json: unknown,
|
||||||
|
key: Buffer = DEFAULT_KEY,
|
||||||
|
): Buffer {
|
||||||
|
const trajectory = Trajectory.fromJson(json, { ignoreUnknownFields: true });
|
||||||
|
const pbData = Buffer.from(trajectory.toBinary());
|
||||||
|
return encrypt(pbData, key);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user