teleportation-cli 1.1.5 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/hooks/permission_request.mjs +326 -59
- package/.claude/hooks/post_tool_use.mjs +90 -0
- package/.claude/hooks/pre_tool_use.mjs +212 -293
- package/.claude/hooks/session-register.mjs +89 -104
- package/.claude/hooks/session_end.mjs +41 -42
- package/.claude/hooks/session_start.mjs +45 -60
- package/.claude/hooks/stop.mjs +752 -99
- package/.claude/hooks/user_prompt_submit.mjs +26 -3
- package/lib/cli/daemon-commands.js +1 -1
- package/lib/cli/teleport-commands.js +469 -0
- package/lib/daemon/daemon-v2.js +104 -0
- package/lib/daemon/lifecycle.js +56 -171
- package/lib/daemon/services/index.js +3 -0
- package/lib/daemon/services/polling-service.js +173 -0
- package/lib/daemon/services/queue-service.js +318 -0
- package/lib/daemon/services/session-service.js +115 -0
- package/lib/daemon/state.js +35 -0
- package/lib/daemon/task-executor-v2.js +413 -0
- package/lib/daemon/task-executor.js +270 -96
- package/lib/daemon/teleportation-daemon.js +709 -126
- package/lib/daemon/timeline-analyzer.js +215 -0
- package/lib/daemon/transcript-ingestion.js +696 -0
- package/lib/daemon/utils.js +91 -0
- package/lib/install/installer.js +184 -20
- package/lib/install/uhr-installer.js +136 -0
- package/lib/remote/providers/base-provider.js +46 -0
- package/lib/remote/providers/daytona-provider.js +58 -0
- package/lib/remote/providers/provider-factory.js +90 -19
- package/lib/remote/providers/sprites-provider.js +711 -0
- package/lib/teleport/exporters/claude-exporter.js +302 -0
- package/lib/teleport/exporters/gemini-exporter.js +307 -0
- package/lib/teleport/exporters/index.js +93 -0
- package/lib/teleport/exporters/interface.js +153 -0
- package/lib/teleport/fork-tracker.js +415 -0
- package/lib/teleport/git-committer.js +337 -0
- package/lib/teleport/index.js +48 -0
- package/lib/teleport/manager.js +620 -0
- package/lib/teleport/session-capture.js +282 -0
- package/package.json +6 -2
- package/teleportation-cli.cjs +488 -453
- package/.claude/hooks/heartbeat.mjs +0 -396
- package/lib/daemon/pid-manager.js +0 -183
|
@@ -46,6 +46,10 @@ const fetchJson = async (url, opts) => {
|
|
|
46
46
|
|
|
47
47
|
const { session_id, prompt } = input;
|
|
48
48
|
|
|
49
|
+
// Detect message source
|
|
50
|
+
const isAutonomousTask = env.TELEPORTATION_TASK_MODE === 'true' || !!env.TELEPORTATION_PARENT_SESSION_ID;
|
|
51
|
+
const source = isAutonomousTask ? 'autonomous_task' : 'cli_interactive';
|
|
52
|
+
|
|
49
53
|
// Clear away mode only on actual user activity (prompt submit), not on tool attempts.
|
|
50
54
|
// Also support /away and /back here in case they are handled as prompts.
|
|
51
55
|
if (session_id && prompt && typeof prompt === 'string') {
|
|
@@ -53,8 +57,15 @@ const fetchJson = async (url, opts) => {
|
|
|
53
57
|
const lowered = trimmed.toLowerCase();
|
|
54
58
|
|
|
55
59
|
let desiredAway = null;
|
|
56
|
-
|
|
57
|
-
|
|
60
|
+
// Support multiple command formats: /away, teleportation away, teleport away, teleporation away (typo)
|
|
61
|
+
if (lowered === '/away' ||
|
|
62
|
+
lowered === 'teleportation away' ||
|
|
63
|
+
lowered === 'teleport away' ||
|
|
64
|
+
lowered === 'teleporation away') desiredAway = true;
|
|
65
|
+
else if (lowered === '/back' ||
|
|
66
|
+
lowered === 'teleportation back' ||
|
|
67
|
+
lowered === 'teleport back' ||
|
|
68
|
+
lowered === 'teleporation back') desiredAway = false;
|
|
58
69
|
else if (trimmed.length > 0) desiredAway = false;
|
|
59
70
|
|
|
60
71
|
if (desiredAway !== null) {
|
|
@@ -66,13 +77,21 @@ const fetchJson = async (url, opts) => {
|
|
|
66
77
|
const RELAY_API_KEY = env.RELAY_API_KEY || config.relayApiKey || '';
|
|
67
78
|
|
|
68
79
|
if (RELAY_API_URL && RELAY_API_KEY) {
|
|
80
|
+
// When user types locally, signal they're back and set location to 'local'
|
|
81
|
+
// This helps the daemon know to stop auto-continuing after approvals
|
|
82
|
+
const patchBody = { is_away: desiredAway };
|
|
83
|
+
if (desiredAway === false) {
|
|
84
|
+
// User is active locally - mark location as 'local'
|
|
85
|
+
patchBody.last_approval_location = 'local';
|
|
86
|
+
}
|
|
87
|
+
|
|
69
88
|
await fetchJson(`${RELAY_API_URL}/api/sessions/${session_id}/daemon-state`, {
|
|
70
89
|
method: 'PATCH',
|
|
71
90
|
headers: {
|
|
72
91
|
'Content-Type': 'application/json',
|
|
73
92
|
'Authorization': `Bearer ${RELAY_API_KEY}`
|
|
74
93
|
},
|
|
75
|
-
body: JSON.stringify(
|
|
94
|
+
body: JSON.stringify(patchBody)
|
|
76
95
|
});
|
|
77
96
|
|
|
78
97
|
// Log away_mode_changed event to timeline (only for explicit /away or /back commands)
|
|
@@ -89,6 +108,7 @@ const fetchJson = async (url, opts) => {
|
|
|
89
108
|
body: JSON.stringify({
|
|
90
109
|
session_id,
|
|
91
110
|
type: 'away_mode_changed',
|
|
111
|
+
source,
|
|
92
112
|
data: {
|
|
93
113
|
is_away: desiredAway,
|
|
94
114
|
source: 'user_prompt',
|
|
@@ -150,6 +170,7 @@ const fetchJson = async (url, opts) => {
|
|
|
150
170
|
body: JSON.stringify({
|
|
151
171
|
session_id,
|
|
152
172
|
type: 'model_change_requested',
|
|
173
|
+
source,
|
|
153
174
|
data: {
|
|
154
175
|
command: prompt,
|
|
155
176
|
timestamp: Date.now()
|
|
@@ -200,10 +221,12 @@ const fetchJson = async (url, opts) => {
|
|
|
200
221
|
body: JSON.stringify({
|
|
201
222
|
session_id,
|
|
202
223
|
type: 'user_message',
|
|
224
|
+
source,
|
|
203
225
|
data: {
|
|
204
226
|
prompt: truncatedPrompt,
|
|
205
227
|
full_length: trimmed.length,
|
|
206
228
|
truncated: trimmed.length > MAX_PROMPT_LENGTH,
|
|
229
|
+
source: 'cli',
|
|
207
230
|
timestamp: Date.now()
|
|
208
231
|
}
|
|
209
232
|
})
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Handles away mode, back mode, and daemon status commands
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { checkDaemonStatus, startDaemon, stopDaemon } from '../daemon/
|
|
6
|
+
import { checkDaemonStatus, startDaemon, stopDaemon } from '../daemon/lifecycle.js';
|
|
7
7
|
|
|
8
8
|
// Color helpers
|
|
9
9
|
const c = {
|
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* CLI Commands for Session Teleportation
|
|
4
|
+
*
|
|
5
|
+
* Provides command-line interface for:
|
|
6
|
+
* - Teleporting local sessions to cloud environments
|
|
7
|
+
* - Checking teleport status
|
|
8
|
+
* - Stopping teleports
|
|
9
|
+
* - Listing active teleports
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { TeleportManager, TeleportMode, TeleportStatus } from '../teleport/manager.js';
|
|
13
|
+
import { ProviderFactory } from '../remote/providers/provider-factory.js';
|
|
14
|
+
import { getSupportedCoders } from '../teleport/exporters/index.js';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Color helpers for terminal output
|
|
18
|
+
*/
|
|
19
|
+
const c = {
|
|
20
|
+
red: (text) => `\x1b[0;31m${text}\x1b[0m`,
|
|
21
|
+
green: (text) => `\x1b[0;32m${text}\x1b[0m`,
|
|
22
|
+
yellow: (text) => `\x1b[1;33m${text}\x1b[0m`,
|
|
23
|
+
blue: (text) => `\x1b[0;34m${text}\x1b[0m`,
|
|
24
|
+
cyan: (text) => `\x1b[0;36m${text}\x1b[0m`,
|
|
25
|
+
purple: (text) => `\x1b[0;35m${text}\x1b[0m`,
|
|
26
|
+
dim: (text) => `\x1b[2m${text}\x1b[0m`,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Load saved relay credentials
|
|
31
|
+
*/
|
|
32
|
+
async function loadSavedRelayCredentials() {
|
|
33
|
+
try {
|
|
34
|
+
const { CredentialManager } = await import('../auth/credentials.js');
|
|
35
|
+
const manager = new CredentialManager();
|
|
36
|
+
const creds = await manager.load();
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
relayApiUrl: creds?.relayApiUrl || '',
|
|
40
|
+
relayApiKey: creds?.relayApiKey || creds?.apiKey || '',
|
|
41
|
+
githubToken: creds?.githubToken || '',
|
|
42
|
+
};
|
|
43
|
+
} catch (error) {
|
|
44
|
+
if (process.env.TELEPORTATION_DEBUG === 'true') {
|
|
45
|
+
console.error(`[teleport] Failed to load saved credentials: ${error.message}`);
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
relayApiUrl: '',
|
|
49
|
+
relayApiKey: '',
|
|
50
|
+
githubToken: '',
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Create provider factory with environment variables
|
|
57
|
+
*/
|
|
58
|
+
function createProviderFactory() {
|
|
59
|
+
return new ProviderFactory({
|
|
60
|
+
vaultClient: {
|
|
61
|
+
apiUrl: process.env.MECH_VAULT_URL || 'https://vault.mechdna.net/api',
|
|
62
|
+
apiKey: process.env.MECH_API_KEY,
|
|
63
|
+
appId: process.env.MECH_APP_ID,
|
|
64
|
+
},
|
|
65
|
+
livePortClient: {
|
|
66
|
+
apiKey: process.env.LIVEPORT_API_KEY,
|
|
67
|
+
apiUrl: process.env.LIVEPORT_API_URL || 'https://api.liveport.dev/v1',
|
|
68
|
+
},
|
|
69
|
+
flyApiToken: process.env.FLY_API_TOKEN,
|
|
70
|
+
daytonaApiKey: process.env.DAYTONA_API_KEY,
|
|
71
|
+
spritesApiKey: process.env.SPRITES_API_KEY,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Create TeleportManager instance
|
|
77
|
+
*/
|
|
78
|
+
async function createTeleportManager() {
|
|
79
|
+
const savedCreds = await loadSavedRelayCredentials();
|
|
80
|
+
|
|
81
|
+
const relayApiUrl = process.env.RELAY_API_URL || savedCreds.relayApiUrl || 'https://api.teleportation.dev';
|
|
82
|
+
const relayApiKey = process.env.RELAY_API_KEY || savedCreds.relayApiKey;
|
|
83
|
+
|
|
84
|
+
if (!relayApiKey) {
|
|
85
|
+
throw new Error('No relay API key found. Run "teleportation login" first.');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const providerFactory = createProviderFactory();
|
|
89
|
+
|
|
90
|
+
return new TeleportManager({
|
|
91
|
+
providerFactory,
|
|
92
|
+
relayApiUrl,
|
|
93
|
+
relayApiKey,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Format status with color
|
|
99
|
+
*/
|
|
100
|
+
function formatStatus(status) {
|
|
101
|
+
const statusColors = {
|
|
102
|
+
[TeleportStatus.PENDING]: c.dim,
|
|
103
|
+
[TeleportStatus.CAPTURING]: c.yellow,
|
|
104
|
+
[TeleportStatus.UPLOADING]: c.yellow,
|
|
105
|
+
[TeleportStatus.PROVISIONING]: c.yellow,
|
|
106
|
+
[TeleportStatus.EXPORTING]: c.yellow,
|
|
107
|
+
[TeleportStatus.RESUMING]: c.yellow,
|
|
108
|
+
[TeleportStatus.RUNNING]: c.green,
|
|
109
|
+
[TeleportStatus.COMPLETED]: c.blue,
|
|
110
|
+
[TeleportStatus.FAILED]: c.red,
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const colorFn = statusColors[status] || c.dim;
|
|
114
|
+
return colorFn(status);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Format duration in milliseconds to human-readable string
|
|
119
|
+
*/
|
|
120
|
+
function formatDuration(ms) {
|
|
121
|
+
const seconds = Math.floor(ms / 1000);
|
|
122
|
+
const minutes = Math.floor(seconds / 60);
|
|
123
|
+
const hours = Math.floor(minutes / 60);
|
|
124
|
+
|
|
125
|
+
if (hours > 0) return `${hours}h ${minutes % 60}m`;
|
|
126
|
+
if (minutes > 0) return `${minutes}m ${seconds % 60}s`;
|
|
127
|
+
return `${seconds}s`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Start a teleport session
|
|
132
|
+
*
|
|
133
|
+
* @param {Object} options
|
|
134
|
+
* @param {string} options.sessionId - Local session ID (or generates one)
|
|
135
|
+
* @param {string} [options.task] - Task description
|
|
136
|
+
* @param {string} [options.cwd] - Working directory
|
|
137
|
+
* @param {string} [options.provider] - Force specific provider
|
|
138
|
+
* @param {string} [options.targetCoder] - Target CLI (claude-code, gemini-cli)
|
|
139
|
+
* @param {string} [options.mode] - Teleport mode (pause, fork)
|
|
140
|
+
*/
|
|
141
|
+
export async function commandTeleportStart(options = {}) {
|
|
142
|
+
console.log(c.purple('\nTeleporting Session to Cloud...\n'));
|
|
143
|
+
|
|
144
|
+
const {
|
|
145
|
+
sessionId = `local-${Date.now()}`,
|
|
146
|
+
task,
|
|
147
|
+
cwd = process.cwd(),
|
|
148
|
+
provider,
|
|
149
|
+
targetCoder = 'claude-code',
|
|
150
|
+
mode = 'pause',
|
|
151
|
+
} = options;
|
|
152
|
+
|
|
153
|
+
// Validate target coder
|
|
154
|
+
const supportedCoders = getSupportedCoders();
|
|
155
|
+
if (!supportedCoders.includes(targetCoder)) {
|
|
156
|
+
console.log(c.red(`❌ Unsupported coder: ${targetCoder}`));
|
|
157
|
+
console.log(c.cyan(`Supported coders: ${supportedCoders.join(', ')}\n`));
|
|
158
|
+
process.exit(1);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Validate mode
|
|
162
|
+
if (!Object.values(TeleportMode).includes(mode)) {
|
|
163
|
+
console.log(c.red(`❌ Invalid mode: ${mode}`));
|
|
164
|
+
console.log(c.cyan(`Valid modes: ${Object.values(TeleportMode).join(', ')}\n`));
|
|
165
|
+
process.exit(1);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
try {
|
|
169
|
+
const manager = await createTeleportManager();
|
|
170
|
+
|
|
171
|
+
// Progress callback
|
|
172
|
+
const onProgress = (event) => {
|
|
173
|
+
const statusIcon = {
|
|
174
|
+
[TeleportStatus.CAPTURING]: '📷',
|
|
175
|
+
[TeleportStatus.UPLOADING]: '⬆️',
|
|
176
|
+
[TeleportStatus.PROVISIONING]: '🚀',
|
|
177
|
+
[TeleportStatus.EXPORTING]: '📦',
|
|
178
|
+
[TeleportStatus.RESUMING]: '▶️',
|
|
179
|
+
[TeleportStatus.RUNNING]: '✅',
|
|
180
|
+
};
|
|
181
|
+
const icon = statusIcon[event.status] || '⏳';
|
|
182
|
+
console.log(` ${icon} ${event.status}`);
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
console.log(c.yellow('Session Details:'));
|
|
186
|
+
console.log(` Session ID: ${c.cyan(sessionId)}`);
|
|
187
|
+
console.log(` Working Dir: ${c.dim(cwd)}`);
|
|
188
|
+
console.log(` Target CLI: ${c.cyan(targetCoder)}`);
|
|
189
|
+
console.log(` Mode: ${c.cyan(mode)}`);
|
|
190
|
+
if (task) console.log(` Task: ${task}`);
|
|
191
|
+
if (provider) console.log(` Provider: ${c.cyan(provider)}`);
|
|
192
|
+
console.log('');
|
|
193
|
+
|
|
194
|
+
console.log(c.yellow('Progress:'));
|
|
195
|
+
|
|
196
|
+
const result = await manager.initiateTeleport(sessionId, {
|
|
197
|
+
task,
|
|
198
|
+
cwd,
|
|
199
|
+
provider,
|
|
200
|
+
targetCoder,
|
|
201
|
+
mode,
|
|
202
|
+
onProgress,
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
console.log('');
|
|
206
|
+
console.log(c.green('✅ Teleport Started Successfully!\n'));
|
|
207
|
+
|
|
208
|
+
console.log(c.yellow('Teleport Details:'));
|
|
209
|
+
console.log(` Teleport ID: ${c.cyan(result.teleportId)}`);
|
|
210
|
+
console.log(` Provider: ${c.cyan(result.provider)}`);
|
|
211
|
+
console.log(` Machine ID: ${c.dim(result.machineId)}`);
|
|
212
|
+
if (result.spriteUrl) {
|
|
213
|
+
console.log(` Sprite URL: ${c.blue(result.spriteUrl)}`);
|
|
214
|
+
}
|
|
215
|
+
if (result.tunnelUrl) {
|
|
216
|
+
console.log(` Tunnel URL: ${c.blue(result.tunnelUrl)}`);
|
|
217
|
+
}
|
|
218
|
+
console.log(` Timeline: ${c.blue(result.timelineUrl)}`);
|
|
219
|
+
console.log('');
|
|
220
|
+
|
|
221
|
+
if (mode === TeleportMode.PAUSE) {
|
|
222
|
+
console.log(c.yellow('⏸️ Local session paused. Work continues on cloud.\n'));
|
|
223
|
+
} else {
|
|
224
|
+
console.log(c.yellow('🔀 Fork mode: Local session continues in parallel.\n'));
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return result;
|
|
228
|
+
} catch (error) {
|
|
229
|
+
console.log(c.red(`\n❌ Teleport failed: ${error.message}\n`));
|
|
230
|
+
if (process.env.TELEPORTATION_DEBUG === 'true') {
|
|
231
|
+
console.error(error.stack);
|
|
232
|
+
}
|
|
233
|
+
process.exit(1);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Get teleport status
|
|
239
|
+
*
|
|
240
|
+
* @param {string} teleportId - Teleport ID
|
|
241
|
+
*/
|
|
242
|
+
export async function commandTeleportStatus(teleportId) {
|
|
243
|
+
if (!teleportId) {
|
|
244
|
+
console.log(c.red('❌ Teleport ID is required\n'));
|
|
245
|
+
process.exit(1);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
try {
|
|
249
|
+
const manager = await createTeleportManager();
|
|
250
|
+
const state = manager.getTeleportStatus(teleportId);
|
|
251
|
+
|
|
252
|
+
if (!state) {
|
|
253
|
+
console.log(c.red(`❌ Teleport not found: ${teleportId}\n`));
|
|
254
|
+
process.exit(1);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
console.log(c.purple(`\nTeleport Status: ${teleportId}\n`));
|
|
258
|
+
|
|
259
|
+
console.log(c.yellow('Status:'));
|
|
260
|
+
console.log(` Status: ${formatStatus(state.status)}`);
|
|
261
|
+
console.log(` Session ID: ${c.cyan(state.sessionId)}`);
|
|
262
|
+
console.log(` Mode: ${c.cyan(state.mode)}`);
|
|
263
|
+
console.log(` Target CLI: ${c.cyan(state.targetCoder)}`);
|
|
264
|
+
|
|
265
|
+
if (state.provider) {
|
|
266
|
+
console.log(` Provider: ${c.cyan(state.provider)}`);
|
|
267
|
+
}
|
|
268
|
+
if (state.machineId) {
|
|
269
|
+
console.log(` Machine ID: ${c.dim(state.machineId)}`);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
console.log('');
|
|
273
|
+
console.log(c.yellow('Timestamps:'));
|
|
274
|
+
console.log(` Created: ${c.dim(state.createdAt)}`);
|
|
275
|
+
if (state.updatedAt) {
|
|
276
|
+
console.log(` Updated: ${c.dim(state.updatedAt)}`);
|
|
277
|
+
}
|
|
278
|
+
if (state.completedAt) {
|
|
279
|
+
console.log(` Completed: ${c.dim(state.completedAt)}`);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (state.task) {
|
|
283
|
+
console.log('');
|
|
284
|
+
console.log(c.yellow('Task:'));
|
|
285
|
+
console.log(` ${state.task}`);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (state.error) {
|
|
289
|
+
console.log('');
|
|
290
|
+
console.log(c.red('Error:'));
|
|
291
|
+
console.log(` ${state.error}`);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Get commit stats if available
|
|
295
|
+
const commitStats = manager.getCommitStats(teleportId);
|
|
296
|
+
if (commitStats) {
|
|
297
|
+
console.log('');
|
|
298
|
+
console.log(c.yellow('Git Commits:'));
|
|
299
|
+
console.log(` Total Commits: ${c.cyan(commitStats.commitCount)}`);
|
|
300
|
+
console.log(` Auto-Commit Active: ${commitStats.isRunning ? c.green('Yes') : c.dim('No')}`);
|
|
301
|
+
if (commitStats.lastCommitAt) {
|
|
302
|
+
console.log(` Last Commit: ${c.dim(commitStats.lastCommitAt)}`);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
console.log('');
|
|
307
|
+
|
|
308
|
+
return state;
|
|
309
|
+
} catch (error) {
|
|
310
|
+
console.log(c.red(`\n❌ Failed to get status: ${error.message}\n`));
|
|
311
|
+
process.exit(1);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* List active teleports
|
|
317
|
+
*
|
|
318
|
+
* @param {Object} [options]
|
|
319
|
+
* @param {string} [options.status] - Filter by status
|
|
320
|
+
*/
|
|
321
|
+
export async function commandTeleportList(options = {}) {
|
|
322
|
+
console.log(c.purple('\nActive Teleports\n'));
|
|
323
|
+
|
|
324
|
+
try {
|
|
325
|
+
const manager = await createTeleportManager();
|
|
326
|
+
let teleports = manager.listActiveTeleports();
|
|
327
|
+
|
|
328
|
+
// Filter by status if specified
|
|
329
|
+
if (options.status) {
|
|
330
|
+
teleports = teleports.filter(t => t.status === options.status);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (teleports.length === 0) {
|
|
334
|
+
console.log(c.dim('No active teleports found.\n'));
|
|
335
|
+
console.log(c.cyan('Start a teleport with: teleportation teleport start --task "your task"\n'));
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Table header
|
|
340
|
+
console.log(c.dim('─'.repeat(80)));
|
|
341
|
+
console.log(
|
|
342
|
+
c.yellow('ID'.padEnd(20)) +
|
|
343
|
+
c.yellow('Status'.padEnd(15)) +
|
|
344
|
+
c.yellow('Provider'.padEnd(12)) +
|
|
345
|
+
c.yellow('CLI'.padEnd(15)) +
|
|
346
|
+
c.yellow('Age')
|
|
347
|
+
);
|
|
348
|
+
console.log(c.dim('─'.repeat(80)));
|
|
349
|
+
|
|
350
|
+
for (const t of teleports) {
|
|
351
|
+
const age = formatDuration(Date.now() - new Date(t.createdAt).getTime());
|
|
352
|
+
console.log(
|
|
353
|
+
t.teleportId.substring(0, 18).padEnd(20) +
|
|
354
|
+
formatStatus(t.status).padEnd(15 + 10) + // Extra for ANSI codes
|
|
355
|
+
(t.provider || '-').padEnd(12) +
|
|
356
|
+
t.targetCoder.padEnd(15) +
|
|
357
|
+
c.dim(age)
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
console.log(c.dim('─'.repeat(80)));
|
|
362
|
+
console.log(c.dim(`\nTotal: ${teleports.length} teleport(s)\n`));
|
|
363
|
+
|
|
364
|
+
return teleports;
|
|
365
|
+
} catch (error) {
|
|
366
|
+
console.log(c.red(`\n❌ Failed to list teleports: ${error.message}\n`));
|
|
367
|
+
process.exit(1);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Stop a teleport session
|
|
373
|
+
*
|
|
374
|
+
* @param {string} teleportId - Teleport ID
|
|
375
|
+
* @param {Object} [options]
|
|
376
|
+
* @param {boolean} [options.createPR] - Create PR with results
|
|
377
|
+
* @param {string} [options.baseBranch] - Base branch for PR
|
|
378
|
+
*/
|
|
379
|
+
export async function commandTeleportStop(teleportId, options = {}) {
|
|
380
|
+
if (!teleportId) {
|
|
381
|
+
console.log(c.red('❌ Teleport ID is required\n'));
|
|
382
|
+
process.exit(1);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
console.log(c.yellow(`\nStopping teleport: ${teleportId}\n`));
|
|
386
|
+
|
|
387
|
+
try {
|
|
388
|
+
const manager = await createTeleportManager();
|
|
389
|
+
|
|
390
|
+
const result = await manager.stopTeleport(teleportId, {
|
|
391
|
+
success: true,
|
|
392
|
+
createPR: options.createPR,
|
|
393
|
+
baseBranch: options.baseBranch,
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
if (!result.success) {
|
|
397
|
+
console.log(c.red(`❌ Failed to stop teleport: ${result.error}\n`));
|
|
398
|
+
process.exit(1);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
console.log(c.green('✅ Teleport stopped successfully!\n'));
|
|
402
|
+
|
|
403
|
+
if (result.delivery) {
|
|
404
|
+
console.log(c.yellow('Results Delivered:'));
|
|
405
|
+
console.log(` Strategy: ${c.cyan(result.delivery.strategy)}`);
|
|
406
|
+
if (result.delivery.prUrl) {
|
|
407
|
+
console.log(` PR: ${c.blue(result.delivery.prUrl)}`);
|
|
408
|
+
}
|
|
409
|
+
if (result.delivery.branch) {
|
|
410
|
+
console.log(` Branch: ${c.cyan(result.delivery.branch)}`);
|
|
411
|
+
}
|
|
412
|
+
console.log('');
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
if (result.deliveryError) {
|
|
416
|
+
console.log(c.yellow('⚠️ Delivery issue:'));
|
|
417
|
+
console.log(` ${c.dim(result.deliveryError)}\n`);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
return result;
|
|
421
|
+
} catch (error) {
|
|
422
|
+
console.log(c.red(`\n❌ Failed to stop teleport: ${error.message}\n`));
|
|
423
|
+
process.exit(1);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Show help for teleport commands
|
|
429
|
+
*/
|
|
430
|
+
export function commandTeleportHelp() {
|
|
431
|
+
console.log(c.purple('\nSession Teleportation Commands\n'));
|
|
432
|
+
console.log(c.cyan('Transfer local coding sessions to cloud environments\n'));
|
|
433
|
+
|
|
434
|
+
console.log(c.yellow('Commands:'));
|
|
435
|
+
console.log(' ' + c.green('teleport start') + ' Start teleporting current session');
|
|
436
|
+
console.log(' ' + c.green('teleport status <id>') + ' Get teleport status');
|
|
437
|
+
console.log(' ' + c.green('teleport list') + ' List active teleports');
|
|
438
|
+
console.log(' ' + c.green('teleport stop <id>') + ' Stop a teleport session\n');
|
|
439
|
+
|
|
440
|
+
console.log(c.yellow('Start Options:'));
|
|
441
|
+
console.log(' ' + c.green('--task "description"') + ' Task for the cloud session');
|
|
442
|
+
console.log(' ' + c.green('--cwd /path') + ' Working directory (default: current)');
|
|
443
|
+
console.log(' ' + c.green('--provider sprites|daytona') + ' Force specific cloud provider');
|
|
444
|
+
console.log(' ' + c.green('--target-coder cli') + ' Target CLI (claude-code, gemini-cli)');
|
|
445
|
+
console.log(' ' + c.green('--mode pause|fork') + ' Teleport mode\n');
|
|
446
|
+
|
|
447
|
+
console.log(c.yellow('Stop Options:'));
|
|
448
|
+
console.log(' ' + c.green('--create-pr') + ' Create PR with results');
|
|
449
|
+
console.log(' ' + c.green('--base-branch main') + ' Base branch for PR\n');
|
|
450
|
+
|
|
451
|
+
console.log(c.yellow('Modes:'));
|
|
452
|
+
console.log(' ' + c.cyan('pause') + ' Pause local session, continue on cloud (default)');
|
|
453
|
+
console.log(' ' + c.cyan('fork') + ' Fork: local continues, cloud runs in parallel\n');
|
|
454
|
+
|
|
455
|
+
console.log(c.purple('Examples:'));
|
|
456
|
+
console.log(' teleportation teleport start --task "Implement feature X"');
|
|
457
|
+
console.log(' teleportation teleport start --target-coder gemini-cli --mode fork');
|
|
458
|
+
console.log(' teleportation teleport status tel_abc123xyz');
|
|
459
|
+
console.log(' teleportation teleport stop tel_abc123xyz --create-pr');
|
|
460
|
+
console.log('');
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
export default {
|
|
464
|
+
commandTeleportStart,
|
|
465
|
+
commandTeleportStatus,
|
|
466
|
+
commandTeleportList,
|
|
467
|
+
commandTeleportStop,
|
|
468
|
+
commandTeleportHelp,
|
|
469
|
+
};
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Teleportation Daemon v2
|
|
5
|
+
*
|
|
6
|
+
* Powered by @derivativelabs/agent-process.
|
|
7
|
+
* Decomposed into modular services.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { createAgent } from '@derivativelabs/agent-process';
|
|
11
|
+
import { HeartbeatService } from '@derivativelabs/agent-process/services';
|
|
12
|
+
import { createDaemonState } from './state.js';
|
|
13
|
+
import { SessionService, QueueService, PollingService } from './services/index.js';
|
|
14
|
+
import { CredentialManager } from '../auth/credentials.js';
|
|
15
|
+
|
|
16
|
+
async function main() {
|
|
17
|
+
console.log('[daemon-v2] Starting up...');
|
|
18
|
+
|
|
19
|
+
// 1. Configuration
|
|
20
|
+
const config = {
|
|
21
|
+
port: parseInt(process.env.TELEPORTATION_DAEMON_PORT || '3050', 10),
|
|
22
|
+
relayApiUrl: process.env.RELAY_API_URL || 'https://api.teleportation.dev',
|
|
23
|
+
relayApiKey: process.env.RELAY_API_KEY || '',
|
|
24
|
+
pollIntervalMs: parseInt(process.env.DAEMON_POLL_INTERVAL_MS || '5000', 10),
|
|
25
|
+
cleanupIntervalMs: 60 * 60 * 1000, // 1 hour
|
|
26
|
+
idleCheckIntervalMs: 300000, // 5 min
|
|
27
|
+
idleTimeoutMs: 1800000, // 30 min
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// 2. Load credentials if not in environment
|
|
31
|
+
if (!config.relayApiKey) {
|
|
32
|
+
try {
|
|
33
|
+
const credManager = new CredentialManager();
|
|
34
|
+
const creds = await credManager.load();
|
|
35
|
+
if (creds && creds.apiKey) {
|
|
36
|
+
config.relayApiKey = creds.apiKey;
|
|
37
|
+
config.relayApiUrl = creds.relayUrl || config.relayApiUrl;
|
|
38
|
+
}
|
|
39
|
+
} catch (e) {
|
|
40
|
+
console.warn('[daemon-v2] Failed to load credentials:', e.message);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// 3. Shared State
|
|
45
|
+
const state = createDaemonState();
|
|
46
|
+
|
|
47
|
+
// 4. Services
|
|
48
|
+
const sessionService = new SessionService(state, config);
|
|
49
|
+
const queueService = new QueueService(state, config);
|
|
50
|
+
const pollingService = new PollingService(state, config);
|
|
51
|
+
|
|
52
|
+
// 5. Agent
|
|
53
|
+
const agent = createAgent({
|
|
54
|
+
name: 'teleportation-daemon',
|
|
55
|
+
port: config.port,
|
|
56
|
+
services: [
|
|
57
|
+
// Custom Services
|
|
58
|
+
sessionService,
|
|
59
|
+
queueService,
|
|
60
|
+
pollingService,
|
|
61
|
+
|
|
62
|
+
// Interval-based services using built-in HeartbeatService
|
|
63
|
+
new HeartbeatService({
|
|
64
|
+
name: 'relay-polling',
|
|
65
|
+
interval: config.pollIntervalMs,
|
|
66
|
+
handler: () => pollingService.poll()
|
|
67
|
+
}),
|
|
68
|
+
new HeartbeatService({
|
|
69
|
+
name: 'queue-processor',
|
|
70
|
+
interval: 1000, // Process queue every second
|
|
71
|
+
handler: () => queueService.processNext()
|
|
72
|
+
}),
|
|
73
|
+
new HeartbeatService({
|
|
74
|
+
name: 'session-cleanup',
|
|
75
|
+
interval: 60000, // Every minute
|
|
76
|
+
handler: () => {
|
|
77
|
+
const now = Date.now();
|
|
78
|
+
const timeout = 240000; // 4 minutes
|
|
79
|
+
for (const [id, lastActivity] of state.sessionActivity) {
|
|
80
|
+
if (now - lastActivity > timeout) {
|
|
81
|
+
state.sessions.delete(id);
|
|
82
|
+
state.sessionActivity.delete(id);
|
|
83
|
+
console.log(`[daemon-v2] Cleaned up stale session ${id}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
})
|
|
88
|
+
]
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// 6. Start
|
|
92
|
+
try {
|
|
93
|
+
await agent.start();
|
|
94
|
+
console.log('[daemon-v2] Ready');
|
|
95
|
+
} catch (error) {
|
|
96
|
+
console.error('[daemon-v2] Failed to start:', error);
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
main().catch(err => {
|
|
102
|
+
console.error('[daemon-v2] Critical error:', err);
|
|
103
|
+
process.exit(1);
|
|
104
|
+
});
|