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.
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 +6 -2
  40. package/teleportation-cli.cjs +488 -453
  41. package/.claude/hooks/heartbeat.mjs +0 -396
  42. package/lib/daemon/pid-manager.js +0 -183
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Daemon Utilities
3
+ *
4
+ * Helper functions extracted from teleportation-daemon.js
5
+ */
6
+
7
+ import { join } from 'path';
8
+ import { appendFileSync } from 'fs';
9
+
10
+ /**
11
+ * Cross-platform debug logging utility
12
+ */
13
+ export function debugLog(filename, message, options = {}) {
14
+ const { enabled, logDir } = options;
15
+ if (!enabled) return;
16
+
17
+ try {
18
+ const logPath = join(logDir || '/tmp', filename);
19
+ appendFileSync(logPath, `[${new Date().toISOString()}] ${message}\n`);
20
+ } catch (error) {
21
+ console.error(`[daemon] Failed to write debug log: ${error.message}`);
22
+ }
23
+ }
24
+
25
+ /**
26
+ * Truncate output for logging and storage
27
+ */
28
+ export function truncateOutput(output, label) {
29
+ const MAX_OUTPUT_SIZE = 100_000; // 100KB
30
+ if (!output || output.length <= MAX_OUTPUT_SIZE) return output;
31
+
32
+ console.log(`[daemon] Truncating ${label} (${output.length} -> ${MAX_OUTPUT_SIZE} chars)`);
33
+ return output.slice(0, MAX_OUTPUT_SIZE) + `\n\n... (output truncated, total size: ${output.length} characters) ...`;
34
+ }
35
+
36
+ /**
37
+ * Sanitize for log (remove sensitive info)
38
+ */
39
+ export function sanitizeForLog(data) {
40
+ if (!data) return data;
41
+ // Simple sanitization - in a real app, use a more robust library
42
+ return String(data).replace(/Bearer\s+[a-zA-Z0-9._-]+/g, 'Bearer [REDACTED]');
43
+ }
44
+
45
+ /**
46
+ * Validation helpers
47
+ */
48
+ export function validateSessionId(session_id) {
49
+ if (!session_id || typeof session_id !== 'string') {
50
+ throw new Error('session_id must be a non-empty string');
51
+ }
52
+ if (session_id.length > 256) {
53
+ throw new Error('session_id too long (max 256 characters)');
54
+ }
55
+ // Allow @ and . for user@host format
56
+ if (!/^[a-zA-Z0-9_@.-]+$/.test(session_id)) {
57
+ throw new Error('session_id contains invalid characters (only alphanumeric, dash, underscore, @, . allowed)');
58
+ }
59
+ return session_id;
60
+ }
61
+
62
+ export function validateApprovalId(approval_id) {
63
+ if (!approval_id || typeof approval_id !== 'string') {
64
+ throw new Error('approval_id must be a non-empty string');
65
+ }
66
+ if (approval_id.length > 256) {
67
+ throw new Error('approval_id too long (max 256 characters)');
68
+ }
69
+ if (!/^[a-zA-Z0-9_-]+$/.test(approval_id)) {
70
+ throw new Error('approval_id contains invalid characters');
71
+ }
72
+ return approval_id;
73
+ }
74
+
75
+ export function validateToolName(tool_name) {
76
+ if (!tool_name || typeof tool_name !== 'string') {
77
+ throw new Error('tool_name must be a non-empty string');
78
+ }
79
+ return tool_name;
80
+ }
81
+
82
+ /**
83
+ * Build tool execution prompt
84
+ */
85
+ export function buildToolPrompt(tool_name, tool_input) {
86
+ const input = tool_input || {};
87
+ return JSON.stringify({
88
+ tool: tool_name,
89
+ parameters: input
90
+ });
91
+ }
@@ -79,6 +79,25 @@ export function checkClaudeCode() {
79
79
  };
80
80
  }
81
81
 
82
+ /**
83
+ * Check if Gemini CLI is installed
84
+ */
85
+ export function checkGeminiCli() {
86
+ try {
87
+ const geminiPath = execSync('which gemini', { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
88
+ if (geminiPath) {
89
+ return { valid: true, path: geminiPath };
90
+ }
91
+ } catch (e) {
92
+ // Gemini not found
93
+ }
94
+
95
+ return {
96
+ valid: false,
97
+ error: 'Gemini CLI not found in PATH.'
98
+ };
99
+ }
100
+
82
101
  /**
83
102
  * Ensure required directories exist
84
103
  */
@@ -91,7 +110,8 @@ export async function ensureDirectories() {
91
110
  settingsDir,
92
111
  hooksDir,
93
112
  join(HOME_DIR, '.teleportation'),
94
- join(HOME_DIR, '.teleportation', 'daemon')
113
+ join(HOME_DIR, '.teleportation', 'daemon'),
114
+ join(HOME_DIR, '.gemini', 'hooks')
95
115
  ];
96
116
 
97
117
  for (const dir of dirs) {
@@ -121,8 +141,43 @@ export async function verifyHooks(sourceHooksDir) {
121
141
  'notification.mjs',
122
142
  'user_prompt_submit.mjs', // Handles /model command detection
123
143
  'config-loader.mjs',
124
- 'session-register.mjs',
125
- 'heartbeat.mjs' // Spawned by session-register.mjs, needs to be in hooks directory
144
+ 'session-register.mjs'
145
+ ];
146
+
147
+ const found = [];
148
+ const missing = [];
149
+
150
+ for (const hook of hooks) {
151
+ const hookPath = join(sourceHooksDir, hook);
152
+ try {
153
+ await stat(hookPath);
154
+ // Set executable permissions (755)
155
+ try {
156
+ await chmod(hookPath, 0o755);
157
+ } catch (_) {
158
+ // Ignore chmod errors
159
+ }
160
+ found.push(hook);
161
+ } catch (e) {
162
+ if (e.code === 'ENOENT') {
163
+ missing.push(hook);
164
+ }
165
+ }
166
+ }
167
+
168
+ return { found, missing };
169
+ }
170
+
171
+ /**
172
+ * Verify Gemini hooks exist in source directory
173
+ */
174
+ export async function verifyGeminiHooks(sourceHooksDir) {
175
+ const hooks = [
176
+ 'before_tool.mjs',
177
+ 'after_tool.mjs',
178
+ 'session_start.mjs',
179
+ 'session_end.mjs',
180
+ 'after_agent.mjs'
126
181
  ];
127
182
 
128
183
  const found = [];
@@ -186,6 +241,72 @@ export async function installHooks(sourceHooksDir) {
186
241
  };
187
242
  }
188
243
 
244
+ /**
245
+ * Install Gemini hooks to ~/.gemini/hooks and update settings
246
+ */
247
+ export async function installGeminiHooks(sourceGeminiHooksDir) {
248
+ const result = await verifyGeminiHooks(sourceGeminiHooksDir);
249
+ const destHooksDir = join(HOME_DIR, '.gemini', 'hooks');
250
+ const copyFailed = [];
251
+
252
+ try {
253
+ await mkdir(destHooksDir, { recursive: true });
254
+ } catch (e) {
255
+ if (e.code !== 'EEXIST') {
256
+ throw e;
257
+ }
258
+ }
259
+
260
+ // Copy hooks
261
+ for (const hook of result.found) {
262
+ const sourcePath = join(sourceGeminiHooksDir, hook);
263
+ const destPath = join(destHooksDir, hook);
264
+ try {
265
+ await copyFile(sourcePath, destPath);
266
+ await chmod(destPath, 0o755);
267
+ } catch (e) {
268
+ copyFailed.push({ file: hook, error: e.message });
269
+ }
270
+ }
271
+
272
+ // Update ~/.gemini/settings.json
273
+ const settingsPath = join(HOME_DIR, '.gemini', 'settings.json');
274
+ let settings = {};
275
+
276
+ try {
277
+ if (await stat(settingsPath).catch(() => false)) {
278
+ const content = await readFile(settingsPath, 'utf8');
279
+ settings = JSON.parse(content);
280
+ }
281
+ } catch (e) {
282
+ // Ignore error, start with empty settings
283
+ }
284
+
285
+ // Merge hooks config
286
+ settings.tools = { ...settings.tools, enableHooks: true };
287
+ settings.hooks = settings.hooks || {};
288
+
289
+ const hooksConfig = {
290
+ BeforeTool: [{ command: `node ${join(destHooksDir, 'before_tool.mjs')}`, timeout: 65000 }],
291
+ AfterTool: [{ command: `node ${join(destHooksDir, 'after_tool.mjs')}`, timeout: 10000 }],
292
+ SessionStart: [{ command: `node ${join(destHooksDir, 'session_start.mjs')}`, timeout: 15000 }],
293
+ AfterAgent: [{ command: `node ${join(destHooksDir, 'after_agent.mjs')}`, timeout: 15000 }],
294
+ SessionEnd: [{ command: `node ${join(destHooksDir, 'session_end.mjs')}`, timeout: 10000 }]
295
+ };
296
+
297
+ // Standardize: Ensure BeforeAgent is also set if needed (parity with Claude's UserPromptSubmit)
298
+ // For now, we follow the PRD-0024 spec
299
+ Object.assign(settings.hooks, hooksConfig);
300
+
301
+ await writeFile(settingsPath, JSON.stringify(settings, null, 2));
302
+
303
+ return {
304
+ installed: result.found,
305
+ failed: result.missing.map(hook => ({ file: hook, error: 'File not found' })).concat(copyFailed),
306
+ settingsPath
307
+ };
308
+ }
309
+
189
310
  /**
190
311
  * Copy daemon files to ~/.teleportation/daemon/
191
312
  */
@@ -195,7 +316,6 @@ export async function installDaemon() {
195
316
 
196
317
  const daemonFiles = [
197
318
  'teleportation-daemon.js',
198
- 'pid-manager.js',
199
319
  'lifecycle.js'
200
320
  ];
201
321
 
@@ -311,7 +431,7 @@ export async function installLibFiles() {
311
431
  { subdir: 'auth', files: ['credentials.js', 'api-key.js'] },
312
432
  { subdir: 'session', files: ['metadata.js'] },
313
433
  { subdir: 'config', files: ['manager.js'] },
314
- { subdir: 'daemon', files: ['lifecycle.js', 'pid-manager.js'] } // Required for heartbeat daemon auto-start
434
+ { subdir: 'daemon', files: ['lifecycle.js', 'transcript-ingestion.js'] } // Required for daemon auto-start and real-time transcript ingestion
315
435
  ];
316
436
 
317
437
  const installed = [];
@@ -503,26 +623,70 @@ export async function verifyInstallation() {
503
623
  * - Hooks stay in PROJECT/.claude/hooks/ (source files, just verify they exist)
504
624
  * - Settings created in PROJECT/.claude/settings.json with absolute paths
505
625
  * - Daemon copied to ~/.teleportation/daemon/ (shared across projects)
626
+ *
627
+ * Global installation (Gemini):
628
+ * - Gemini hooks copied to ~/.gemini/hooks/
629
+ * - Global ~/.gemini/settings.json updated
630
+ *
631
+ * @param {string} sourceHooksDir - Source directory for Claude hooks
632
+ * @param {Object} [options] - Installation options
633
+ * @param {boolean} [options.includeClaude] - Force include Claude hooks (overrides detection)
634
+ * @param {boolean} [options.includeGemini] - Force include Gemini hooks (overrides detection)
506
635
  */
507
- export async function install(sourceHooksDir) {
636
+ export async function install(sourceHooksDir, options = {}) {
508
637
  // Pre-flight checks
509
638
  const nodeCheck = checkNodeVersion();
510
639
  if (!nodeCheck.valid) {
511
640
  throw new Error(nodeCheck.error);
512
641
  }
513
642
 
643
+ // Check available CLIs
514
644
  const claudeCheck = checkClaudeCode();
515
- if (!claudeCheck.valid) {
516
- throw new Error(claudeCheck.error);
645
+ const geminiCheck = checkGeminiCli();
646
+
647
+ // Resolve what to install based on options and detection
648
+ const shouldInstallClaude = options.includeClaude !== undefined
649
+ ? options.includeClaude
650
+ : claudeCheck.valid;
651
+
652
+ const shouldInstallGemini = options.includeGemini !== undefined
653
+ ? options.includeGemini
654
+ : geminiCheck.valid;
655
+
656
+ if (!shouldInstallClaude && !shouldInstallGemini) {
657
+ if (options.includeClaude === false && options.includeGemini === false) {
658
+ throw new Error('No targets selected for installation.');
659
+ }
660
+ throw new Error('Neither Claude Code nor Gemini CLI found. Please install one of them first, or specify a target.');
517
661
  }
518
662
 
519
663
  // Create directories
520
664
  await ensureDirectories();
521
665
 
522
- // Install hooks (verify and copy to destination)
523
- const hookResult = await installHooks(sourceHooksDir);
524
- if (hookResult.failed.length > 0) {
525
- throw new Error(`Failed to install hooks: ${hookResult.failed.map(f => f.file).join(', ')}`);
666
+ let hooksInstalled = 0;
667
+ let geminiHooksInstalled = 0;
668
+
669
+ // 1. Install Claude hooks
670
+ if (shouldInstallClaude) {
671
+ const hookResult = await installHooks(sourceHooksDir);
672
+ if (hookResult.failed.length > 0) {
673
+ throw new Error(`Failed to install Claude hooks: ${hookResult.failed.map(f => f.file).join(', ')}`);
674
+ }
675
+ hooksInstalled = hookResult.installed.length;
676
+ // Create project-level settings with absolute paths for Claude
677
+ await createSettings();
678
+ }
679
+
680
+ // 2. Install Gemini hooks
681
+ if (shouldInstallGemini) {
682
+ // Determine Gemini hooks source directory (sibling to .claude/hooks)
683
+ const sourceGeminiHooksDir = join(dirname(sourceHooksDir), '..', '.gemini', 'hooks');
684
+ const geminiResult = await installGeminiHooks(sourceGeminiHooksDir);
685
+ if (geminiResult.failed.length > 0) {
686
+ console.warn(`Warning: Some Gemini hooks failed to install: ${geminiResult.failed.map(f => f.file).join(', ')}`);
687
+ } else {
688
+ geminiHooksInstalled = geminiResult.installed.length;
689
+ }
526
690
  }
527
691
 
528
692
  // Install daemon (still goes to ~/.teleportation/daemon/)
@@ -544,21 +708,21 @@ export async function install(sourceHooksDir) {
544
708
  console.warn(`Warning: Some lib files failed to install: ${libResult.failed.map(f => f.file).join(', ')}`);
545
709
  }
546
710
 
547
- // Create project-level settings with absolute paths
548
- await createSettings();
549
-
550
711
  // Write version file
551
712
  await writeVersionFile();
552
713
 
553
- // Verify
554
- const verification = await verifyInstallation();
555
- if (!verification.valid) {
556
- throw new Error(`Installation verification failed: ${verification.error}`);
714
+ // Verify (only checking Claude verification if Claude is installed)
715
+ if (claudeCheck.valid) {
716
+ const verification = await verifyInstallation();
717
+ if (!verification.valid) {
718
+ throw new Error(`Installation verification failed: ${verification.error}`);
719
+ }
557
720
  }
558
721
 
559
722
  return {
560
723
  success: true,
561
- hooksInstalled: hookResult.installed.length,
724
+ hooksInstalled,
725
+ geminiHooksInstalled,
562
726
  daemonInstalled: daemonResult.installed.length,
563
727
  libFilesInstalled: libResult.installed.length,
564
728
  settingsFile: getProjectSettings(),
@@ -0,0 +1,136 @@
1
+ /**
2
+ * UHR (Universal Hook Registry) installer wrapper for Teleportation.
3
+ *
4
+ * Provides functions to install and uninstall teleportation hooks via the UHR CLI.
5
+ * Falls back gracefully when UHR is not available -- callers can check with
6
+ * `isUhrAvailable()` and use direct installation (installer.js) as a fallback.
7
+ *
8
+ * The manifest file (teleportation.uhr.json) uses `__HOOKS_DIR__` as a placeholder
9
+ * in hook commands. This module replaces that placeholder with the actual absolute
10
+ * path to the hooks directory before passing the manifest to UHR.
11
+ *
12
+ * @module lib/install/uhr-installer
13
+ */
14
+
15
+ import { execSync } from 'child_process';
16
+ import { join } from 'path';
17
+ import { tmpdir } from 'os';
18
+
19
+ /**
20
+ * Check if the UHR CLI is available on this system.
21
+ *
22
+ * Checks two locations:
23
+ * 1. `uhr` on the system PATH (via `which uhr`)
24
+ * 2. `node_modules/.bin/uhr` (local project install)
25
+ *
26
+ * @returns {Promise<boolean>} true if UHR CLI is reachable
27
+ */
28
+ export async function isUhrAvailable() {
29
+ // Check PATH first
30
+ try {
31
+ execSync('which uhr', { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
32
+ return true;
33
+ } catch (_) {
34
+ // Not in PATH
35
+ }
36
+
37
+ // Check local node_modules
38
+ try {
39
+ const localBin = join('node_modules', '.bin', 'uhr');
40
+ const file = Bun.file(localBin);
41
+ return await file.exists();
42
+ } catch (_) {
43
+ return false;
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Install hooks via the UHR CLI.
49
+ *
50
+ * Reads the manifest at `manifestPath`, replaces every occurrence of the
51
+ * `__HOOKS_DIR__` placeholder with the absolute `hooksDir` path, writes a
52
+ * temporary manifest, and runs `uhr install <tempManifest>`.
53
+ *
54
+ * @param {string} manifestPath - Absolute path to the UHR manifest file (e.g. teleportation.uhr.json)
55
+ * @param {string} hooksDir - Absolute path to the hooks directory
56
+ * @param {object} [options] - Reserved for future use (e.g. dryRun)
57
+ * @returns {Promise<{success: true, warnings: string[]} | {success: false, reason: string}>}
58
+ */
59
+ export async function installViaUhr(manifestPath, hooksDir, options = {}) {
60
+ // 1. Read the manifest
61
+ let manifestText;
62
+ try {
63
+ const file = Bun.file(manifestPath);
64
+ manifestText = await file.text();
65
+ } catch (err) {
66
+ const msg = err instanceof Error ? err.message : String(err);
67
+ return { success: false, reason: `Failed to read manifest file: ${msg}` };
68
+ }
69
+
70
+ // 2. Parse to validate JSON
71
+ let manifest;
72
+ try {
73
+ manifest = JSON.parse(manifestText);
74
+ } catch (err) {
75
+ const msg = err instanceof Error ? err.message : String(err);
76
+ return { success: false, reason: `Failed to parse manifest JSON: ${msg}` };
77
+ }
78
+
79
+ // 3. Replace __HOOKS_DIR__ placeholder with the actual absolute path
80
+ const templated = JSON.stringify(manifest, null, 2).replaceAll('__HOOKS_DIR__', hooksDir);
81
+
82
+ // 4. Write to a temp file
83
+ const tempManifestPath = join(tmpdir(), `teleportation-uhr-${Date.now()}.json`);
84
+ try {
85
+ await Bun.write(tempManifestPath, templated);
86
+ } catch (err) {
87
+ const msg = err instanceof Error ? err.message : String(err);
88
+ return { success: false, reason: `Failed to write temp manifest: ${msg}` };
89
+ }
90
+
91
+ // 5. Run uhr install
92
+ const warnings = [];
93
+ try {
94
+ const output = execSync(`uhr install ${tempManifestPath}`, {
95
+ encoding: 'utf8',
96
+ stdio: ['ignore', 'pipe', 'pipe'],
97
+ timeout: 30000,
98
+ });
99
+
100
+ // Collect any warning lines from stdout
101
+ if (output) {
102
+ const lines = output.split('\n').filter(Boolean);
103
+ for (const line of lines) {
104
+ if (/warn/i.test(line)) {
105
+ warnings.push(line.trim());
106
+ }
107
+ }
108
+ }
109
+
110
+ return { success: true, warnings };
111
+ } catch (err) {
112
+ const msg = err instanceof Error ? err.message : String(err);
113
+ return { success: false, reason: msg || 'uhr install failed' };
114
+ }
115
+ }
116
+
117
+ /**
118
+ * Uninstall hooks via the UHR CLI.
119
+ *
120
+ * Runs `uhr uninstall <serviceName>` to remove previously installed hooks.
121
+ *
122
+ * @param {string} serviceName - The service name to uninstall (e.g. "teleportation")
123
+ * @returns {Promise<{success: boolean}>}
124
+ */
125
+ export async function uninstallViaUhr(serviceName) {
126
+ try {
127
+ execSync(`uhr uninstall ${serviceName}`, {
128
+ encoding: 'utf8',
129
+ stdio: ['ignore', 'pipe', 'pipe'],
130
+ timeout: 30000,
131
+ });
132
+ return { success: true };
133
+ } catch (_) {
134
+ return { success: false };
135
+ }
136
+ }
@@ -404,4 +404,50 @@ export class BaseProvider {
404
404
  errors,
405
405
  };
406
406
  }
407
+
408
+ /**
409
+ * Check provider health and connectivity.
410
+ *
411
+ * Tests connection to the provider's API and returns health status.
412
+ * Subclasses should override this to add provider-specific checks.
413
+ *
414
+ * @returns {Promise<Object>} Health check result
415
+ * @returns {boolean} return.healthy - Whether the provider is healthy
416
+ * @returns {string} return.provider - Provider name
417
+ * @returns {Object} [return.details] - Additional health details
418
+ * @returns {string} [return.error] - Error message if unhealthy
419
+ */
420
+ async checkHealth() {
421
+ // Default implementation - subclasses should override
422
+ return {
423
+ healthy: true,
424
+ provider: this.constructor.name,
425
+ details: {
426
+ message: 'Health check not implemented for this provider',
427
+ },
428
+ };
429
+ }
430
+
431
+ /**
432
+ * Get provider capabilities.
433
+ *
434
+ * Returns information about what this provider supports.
435
+ * Subclasses should override to provide accurate capabilities.
436
+ *
437
+ * @returns {Object} Provider capabilities
438
+ * @returns {boolean} return.supportsCheckpoints - Can create/restore checkpoints
439
+ * @returns {boolean} return.supportsSnapshots - Can create/restore snapshots
440
+ * @returns {boolean} return.supportsHibernation - Can hibernate/wake machines
441
+ * @returns {boolean} return.supportsAutoStop - Supports auto-stop when idle
442
+ * @returns {number} [return.checkpointTime] - Estimated checkpoint time in ms
443
+ * @returns {number} [return.coldStartTime] - Estimated cold start time in ms
444
+ */
445
+ getCapabilities() {
446
+ return {
447
+ supportsCheckpoints: false,
448
+ supportsSnapshots: false,
449
+ supportsHibernation: false,
450
+ supportsAutoStop: false,
451
+ };
452
+ }
407
453
  }
@@ -273,6 +273,8 @@ for i in 1 2 3 4 5; do
273
273
  done
274
274
 
275
275
  # Start teleportation daemon
276
+ # Mark this as a cloud session so the frontend can show the cloud badge
277
+ export TELEPORTATION_IS_CLOUD=true
276
278
  bun teleportation-cli.cjs daemon start --session-id ${safeSessionId} --task ${safeTask}
277
279
  `.trim();
278
280
  }
@@ -503,4 +505,60 @@ bun teleportation-cli.cjs daemon start --session-id ${safeSessionId} --task ${sa
503
505
 
504
506
  return response.json();
505
507
  }
508
+
509
+ /**
510
+ * Get provider capabilities
511
+ *
512
+ * @returns {Object} Provider capabilities
513
+ */
514
+ getCapabilities() {
515
+ return {
516
+ supportsCheckpoints: false,
517
+ supportsSnapshots: true,
518
+ supportsHibernation: false,
519
+ supportsAutoStop: true,
520
+ autoStopTimeout: 1800000, // 30 minutes default
521
+ provider: 'daytona',
522
+ };
523
+ }
524
+
525
+ /**
526
+ * Check provider health
527
+ *
528
+ * @returns {Promise<Object>} Health check result
529
+ */
530
+ async checkHealth() {
531
+ try {
532
+ // Try to list workspaces - if this works, the API is healthy
533
+ const url = `${this.apiUrl}/workspace`;
534
+ const response = await this._fetchWithTimeout(url, {
535
+ method: 'GET',
536
+ headers: this._headers(),
537
+ }, 10000); // 10 second timeout for health check
538
+
539
+ if (!response.ok) {
540
+ return {
541
+ healthy: false,
542
+ provider: 'daytona',
543
+ error: `API returned status ${response.status}`,
544
+ };
545
+ }
546
+
547
+ return {
548
+ healthy: true,
549
+ provider: 'daytona',
550
+ details: {
551
+ apiUrl: this.apiUrl,
552
+ profileId: this.profileId,
553
+ timestamp: new Date().toISOString(),
554
+ },
555
+ };
556
+ } catch (error) {
557
+ return {
558
+ healthy: false,
559
+ provider: 'daytona',
560
+ error: error.message,
561
+ };
562
+ }
563
+ }
506
564
  }