teleportation-cli 1.1.5 → 1.2.0

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.
Files changed (42) hide show
  1. package/.claude/hooks/permission_request.mjs +326 -59
  2. package/.claude/hooks/post_tool_use.mjs +90 -0
  3. package/.claude/hooks/pre_tool_use.mjs +212 -293
  4. package/.claude/hooks/session-register.mjs +89 -104
  5. package/.claude/hooks/session_end.mjs +41 -42
  6. package/.claude/hooks/session_start.mjs +45 -60
  7. package/.claude/hooks/stop.mjs +752 -99
  8. package/.claude/hooks/user_prompt_submit.mjs +26 -3
  9. package/lib/cli/daemon-commands.js +1 -1
  10. package/lib/cli/teleport-commands.js +469 -0
  11. package/lib/daemon/daemon-v2.js +104 -0
  12. package/lib/daemon/lifecycle.js +56 -171
  13. package/lib/daemon/services/index.js +3 -0
  14. package/lib/daemon/services/polling-service.js +173 -0
  15. package/lib/daemon/services/queue-service.js +318 -0
  16. package/lib/daemon/services/session-service.js +115 -0
  17. package/lib/daemon/state.js +35 -0
  18. package/lib/daemon/task-executor-v2.js +413 -0
  19. package/lib/daemon/task-executor.js +270 -96
  20. package/lib/daemon/teleportation-daemon.js +709 -126
  21. package/lib/daemon/timeline-analyzer.js +215 -0
  22. package/lib/daemon/transcript-ingestion.js +696 -0
  23. package/lib/daemon/utils.js +91 -0
  24. package/lib/install/installer.js +184 -20
  25. package/lib/install/uhr-installer.js +136 -0
  26. package/lib/remote/providers/base-provider.js +46 -0
  27. package/lib/remote/providers/daytona-provider.js +58 -0
  28. package/lib/remote/providers/provider-factory.js +90 -19
  29. package/lib/remote/providers/sprites-provider.js +711 -0
  30. package/lib/teleport/exporters/claude-exporter.js +302 -0
  31. package/lib/teleport/exporters/gemini-exporter.js +307 -0
  32. package/lib/teleport/exporters/index.js +93 -0
  33. package/lib/teleport/exporters/interface.js +153 -0
  34. package/lib/teleport/fork-tracker.js +415 -0
  35. package/lib/teleport/git-committer.js +337 -0
  36. package/lib/teleport/index.js +48 -0
  37. package/lib/teleport/manager.js +620 -0
  38. package/lib/teleport/session-capture.js +282 -0
  39. package/package.json +9 -5
  40. package/teleportation-cli.cjs +488 -453
  41. package/.claude/hooks/heartbeat.mjs +0 -396
  42. 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
- if (lowered === '/away' || lowered === 'teleportation away') desiredAway = true;
57
- else if (lowered === '/back' || lowered === 'teleportation back') desiredAway = false;
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({ is_away: desiredAway })
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/pid-manager.js';
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
+ });