Skip to content

Commit 74d2577

Browse files
committed
feat(cli): add /teleport command for portable session management
1 parent 162ac93 commit 74d2577

10 files changed

Lines changed: 1264 additions & 0 deletions

File tree

‎docs/cli/teleportation.md‎

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
# Teleportation
2+
3+
Teleportation lets you move your active AI engineering sessions between different
4+
machines. Unlike sharing a chat transcript, teleporting captures your entire
5+
workspace state, including your plans, tasks, tracker data, and full activity
6+
logs.
7+
8+
By using teleportation, you can start a complex engineering task on your local
9+
laptop and "needlecast" it to a powerful remote server or a different
10+
development environment without losing your progress or context.
11+
12+
## How it works
13+
14+
Teleportation bundles all session-related data from your local Gemini temporary
15+
directory (`~/.gemini/tmp`) into a portable, compressed archive (`.tar.gz`).
16+
You can then transfer this archive to another machine and import it to resume
17+
working exactly where you left off.
18+
19+
The bundle includes:
20+
- Chat history and conversation state.
21+
- AI-generated plans and task statuses.
22+
- Detailed activity logs and tool outputs.
23+
- Project-specific tracker data.
24+
25+
## Export a session
26+
27+
To package your current session for transfer, use the `/teleport export` command.
28+
29+
1. Run the export command in your active session:
30+
```bash
31+
/teleport export
32+
```
33+
This creates a file named `gemini-session-<short-id>.tar.gz` in your current
34+
directory.
35+
36+
2. Optional: Specify a custom output path:
37+
```bash
38+
/teleport export current my-backup.tar.gz
39+
```
40+
41+
3. Optional: Export a specific session by its ID:
42+
```bash
43+
/teleport export session-abc-123
44+
```
45+
46+
## Import a session
47+
48+
To restore a session on a new machine, use the `/teleport import` command.
49+
50+
1. Move the exported tarball to the new machine.
51+
2. Run the import command:
52+
```bash
53+
/teleport import ./my-backup.tar.gz
54+
```
55+
3. Resume the imported session:
56+
```bash
57+
/resume <session-id>
58+
```
59+
The import command will display the session ID you need to resume.
60+
61+
## Security and privacy
62+
63+
Teleportation includes several features to ensure your session data remains
64+
secure during transit.
65+
66+
### Encryption
67+
68+
You can encrypt your session bundle using AES-256-GCM. This ensures that even
69+
if the archive is intercepted, the contents cannot be read without your secret.
70+
71+
To use encryption:
72+
1. Add the `--secret` flag to your export command:
73+
```bash
74+
/teleport export --secret
75+
```
76+
2. Enter a password when prompted. Gemini CLI uses the Scrypt key derivation
77+
function to protect your password against brute-force attacks.
78+
3. When importing, add the `--secret` flag again:
79+
```bash
80+
/teleport import ./encrypted-session.tar.gz --secret
81+
```
82+
83+
You can also use the `GEMINI_TELEPORT_SECRET` environment variable or a key file
84+
with `--key-file <path>` to provide the secret without an interactive prompt.
85+
86+
### Path traversal protection
87+
88+
During the import process, Gemini CLI automatically scans the archive for
89+
malicious paths. It prevents any files from being extracted outside of the
90+
designated Gemini temporary directory, protecting your system from path
91+
traversal attacks.
92+
93+
## Cloud blob storage
94+
95+
Teleportation supports direct transfers to and from Google Cloud Storage (GCS)
96+
and Amazon S3. This lets you store your sessions in a centralized location that
97+
you control, without committing large log files to your Git repository.
98+
99+
### Prerequisites
100+
101+
To use cloud storage, you must have the corresponding cloud CLI installed and
102+
authenticated on your machine:
103+
- **GCS**: Requires `gcloud` or `gsutil`.
104+
- **S3**: Requires `aws`.
105+
106+
### Cloud usage examples
107+
108+
**Export directly to a bucket:**
109+
```bash
110+
/teleport export --blob gs://my-sessions-bucket/task-alpha.tar.gz
111+
```
112+
113+
**Import directly from a bucket:**
114+
```bash
115+
/teleport import gs://my-sessions-bucket/task-alpha.tar.gz
116+
```
117+
118+
**Secure cloud transfer:**
119+
```bash
120+
/teleport export --secret --blob s3://my-bucket/secure-session.tar.gz
121+
```
122+
123+
## Next steps
124+
125+
- Learn more about [Session management](./session-management.md).
126+
- Explore [Checkpointing](./checkpointing.md) for local file safety.

‎docs/reference/commands.md‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -442,6 +442,18 @@ Slash commands provide meta-level control over the CLI itself.
442442
- **`tools`**:
443443
- **Description:** Show tool-specific usage statistics.
444444

445+
### `/teleport`
446+
447+
- **Description:** Export or import sessions to make them portable across
448+
machines.
449+
- **Sub-commands:**
450+
- **`export [session-id] [output-path] [--secret] [--key-file <path>] [--blob <uri>]`**:
451+
- **Description:** Packages the session state into a compressed archive.
452+
- **Note:** Use `--secret` for AES-256 encryption or `--blob` to upload directly
453+
to GCS/S3.
454+
- **`import <path-or-uri> [--secret] [--key-file <path>]`**:
455+
- **Description:** Restores a session from a local file or cloud URI.
456+
445457
### `/terminal-setup`
446458

447459
- **Description:** Configure terminal keybindings for multiline input (VS Code,

‎docs/sidebar.json‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@
4343
"label": "Manage sessions and history",
4444
"slug": "docs/cli/tutorials/session-management"
4545
},
46+
{
47+
"label": "Teleport sessions between machines",
48+
"slug": "docs/cli/teleportation"
49+
},
4650
{
4751
"label": "Plan tasks with todos",
4852
"slug": "docs/cli/tutorials/task-planning"
@@ -136,6 +140,7 @@
136140
{ "label": "Sandboxing", "slug": "docs/cli/sandbox" },
137141
{ "label": "Settings", "slug": "docs/cli/settings" },
138142
{ "label": "Telemetry", "slug": "docs/cli/telemetry" },
143+
{ "label": "Teleportation", "slug": "docs/cli/teleportation" },
139144
{ "label": "Token caching", "slug": "docs/cli/token-caching" }
140145
]
141146
},

‎integration-tests/teleport.test.ts‎

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
/**
2+
* @license
3+
* Copyright 2025 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
8+
import * as fs from 'node:fs/promises';
9+
import * as path from 'node:path';
10+
import * as os from 'node:os';
11+
import { TeleportService, Config, ChatRecordingService } from '@google/gemini-cli-core';
12+
13+
describe('Teleport E2E Integration', () => {
14+
let tmpDir: string;
15+
let machineA_Home: string;
16+
let machineB_Home: string;
17+
let projectDir: string;
18+
19+
beforeEach(async () => {
20+
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'gemini-teleport-e2e-'));
21+
machineA_Home = path.join(tmpDir, 'machineA');
22+
machineB_Home = path.join(tmpDir, 'machineB');
23+
projectDir = path.join(tmpDir, 'my-project');
24+
25+
await fs.mkdir(machineA_Home, { recursive: true });
26+
await fs.mkdir(machineB_Home, { recursive: true });
27+
await fs.mkdir(projectDir, { recursive: true });
28+
});
29+
30+
afterEach(async () => {
31+
await fs.rm(tmpDir, { recursive: true, force: true });
32+
});
33+
34+
it('should round-trip a session between two simulated machines', async () => {
35+
// --- STEP 1: Setup Machine A and create a session ---
36+
const configA = new Config({
37+
sessionId: 'session-123',
38+
targetDir: projectDir,
39+
cwd: projectDir,
40+
model: 'test-model',
41+
});
42+
const machineA_TempDir = path.join(machineA_Home, 'tmp');
43+
await fs.mkdir(machineA_TempDir, { recursive: true });
44+
vi.spyOn(configA.storage, 'getProjectTempDir').mockReturnValue(machineA_TempDir);
45+
46+
const recordingServiceA = new ChatRecordingService(configA);
47+
recordingServiceA.initialize();
48+
49+
recordingServiceA.recordMessage({
50+
model: 'test-model',
51+
type: 'user',
52+
content: [{ text: 'Hello from Machine A' }]
53+
});
54+
55+
const realSessionId = configA.getSessionId();
56+
const chatFilePathA = recordingServiceA.getConversationFilePath();
57+
expect(chatFilePathA).not.toBeNull();
58+
59+
// --- STEP 2: Export from Machine A ---
60+
const teleportServiceA = new TeleportService(configA);
61+
const tarballPath = path.join(tmpDir, 'teleport.tar.gz');
62+
await teleportServiceA.exportSession(realSessionId, tarballPath);
63+
64+
// --- STEP 3: Setup Machine B and Import ---
65+
const configB = new Config({
66+
sessionId: 'new-session',
67+
targetDir: projectDir,
68+
cwd: projectDir,
69+
model: 'test-model',
70+
});
71+
const machineB_TempDir = path.join(machineB_Home, 'tmp');
72+
await fs.mkdir(machineB_TempDir, { recursive: true });
73+
vi.spyOn(configB.storage, 'getProjectTempDir').mockReturnValue(machineB_TempDir);
74+
75+
const teleportServiceB = new TeleportService(configB);
76+
const importResult = await teleportServiceB.importSession(tarballPath);
77+
78+
expect(importResult.sessionId).toBe(realSessionId);
79+
80+
// --- STEP 4: Verify Machine B can "see" the session ---
81+
const chatFiles = await configB.storage.listProjectChatFiles();
82+
expect(chatFiles.length).toBe(1);
83+
84+
// storage.listProjectChatFiles returns relative paths
85+
const importedFile = path.join(machineB_TempDir, chatFiles[0].filePath);
86+
const conversationData = JSON.parse(await fs.readFile(importedFile, 'utf8'));
87+
88+
const recordingServiceB = new ChatRecordingService(configB);
89+
recordingServiceB.initialize({
90+
filePath: importedFile,
91+
conversation: conversationData
92+
});
93+
94+
const conversation = recordingServiceB.getConversation();
95+
expect(conversation).not.toBeNull();
96+
expect(conversation?.messages[0].content[0].text).toBe('Hello from Machine A');
97+
});
98+
99+
it('should handle encrypted sessions in E2E', async () => {
100+
const secret = 'password123';
101+
const configA = new Config({ sessionId: 'enc-session', targetDir: projectDir, cwd: projectDir, model: 'm' });
102+
const machineA_TempDir = path.join(machineA_Home, 'tmp');
103+
await fs.mkdir(machineA_TempDir, { recursive: true });
104+
vi.spyOn(configA.storage, 'getProjectTempDir').mockReturnValue(machineA_TempDir);
105+
106+
const recordingServiceA = new ChatRecordingService(configA);
107+
recordingServiceA.initialize();
108+
recordingServiceA.recordMessage({ model: 'm', type: 'user', content: [{ text: 'Encrypted message' }] });
109+
110+
const realSessionId = configA.getSessionId();
111+
const teleportServiceA = new TeleportService(configA);
112+
const tarballPath = path.join(tmpDir, 'encrypted.tar.gz');
113+
await teleportServiceA.exportSession(realSessionId, tarballPath, secret);
114+
115+
// Machine B
116+
const configB = new Config({ sessionId: 'b', targetDir: projectDir, cwd: projectDir, model: 'm' });
117+
const machineB_TempDir = path.join(machineB_Home, 'tmp');
118+
await fs.mkdir(machineB_TempDir, { recursive: true });
119+
vi.spyOn(configB.storage, 'getProjectTempDir').mockReturnValue(machineB_TempDir);
120+
121+
const teleportServiceB = new TeleportService(configB);
122+
await teleportServiceB.importSession(tarballPath, secret);
123+
124+
const chatFiles = await configB.storage.listProjectChatFiles();
125+
const importedFile = path.join(machineB_TempDir, chatFiles[0].filePath);
126+
const conversationData = JSON.parse(await fs.readFile(importedFile, 'utf8'));
127+
128+
const recordingServiceB = new ChatRecordingService(configB);
129+
recordingServiceB.initialize({
130+
filePath: importedFile,
131+
conversation: conversationData
132+
});
133+
134+
const conversation = recordingServiceB.getConversation();
135+
expect(conversation?.messages[0].content[0].text).toBe('Encrypted message');
136+
});
137+
});

‎packages/cli/src/services/BuiltinCommandLoader.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ import { quitCommand } from '../ui/commands/quitCommand.js';
5252
import { restoreCommand } from '../ui/commands/restoreCommand.js';
5353
import { resumeCommand } from '../ui/commands/resumeCommand.js';
5454
import { statsCommand } from '../ui/commands/statsCommand.js';
55+
import { teleportCommand } from '../ui/commands/teleportCommand.js';
5556
import { themeCommand } from '../ui/commands/themeCommand.js';
5657
import { toolsCommand } from '../ui/commands/toolsCommand.js';
5758
import { skillsCommand } from '../ui/commands/skillsCommand.js';
@@ -112,6 +113,7 @@ export class BuiltinCommandLoader implements ICommandLoader {
112113
];
113114
};
114115

116+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
115117
const chatResumeSubCommands = addDebugToChatResumeSubCommands(
116118
chatCommand.subCommands,
117119
);
@@ -195,6 +197,7 @@ export class BuiltinCommandLoader implements ICommandLoader {
195197
subCommands: addDebugToChatResumeSubCommands(resumeCommand.subCommands),
196198
},
197199
statsCommand,
200+
teleportCommand,
198201
themeCommand,
199202
toolsCommand,
200203
...(this.config?.isSkillsSupportEnabled()

0 commit comments

Comments
 (0)