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
@@ -0,0 +1,711 @@
1
+ /**
2
+ * Sprites.dev Provider for Remote Development Environments
3
+ *
4
+ * Uses Sprites.dev (Fly.io's Firecracker micro-VMs) for ephemeral development workspaces.
5
+ * Sprites offer:
6
+ * - Sub-10 second boot times
7
+ * - 300ms checkpoint/restore
8
+ * - Auto-hibernate after 30 seconds of inactivity
9
+ * - Pay-per-compute pricing
10
+ *
11
+ * Ideal for:
12
+ * - Long-running tasks (overnight builds, large refactors)
13
+ * - Tasks requiring checkpoints/rollback
14
+ * - State preservation across sessions
15
+ *
16
+ * @see https://sprites.dev/docs/api
17
+ */
18
+
19
+ import { BaseProvider } from './base-provider.js';
20
+
21
+ export class SpritesProvider extends BaseProvider {
22
+ /**
23
+ * Initialize SpritesProvider
24
+ * @param {Object} config - Provider configuration
25
+ * @param {VaultClient} config.vaultClient - Mech Vault client
26
+ * @param {LivePortClient} config.livePortClient - LivePort client
27
+ * @param {string} [config.spritesToken] - Sprites API token (or SPRITES_TOKEN env var)
28
+ * @param {string} [config.apiUrl] - Sprites API URL (default: https://api.sprites.dev/v1)
29
+ * @param {number} [config.timeout] - Request timeout in ms (default: 60000)
30
+ */
31
+ constructor(config) {
32
+ super(config);
33
+ this.apiKey = config.spritesToken || process.env.SPRITES_TOKEN;
34
+ this.apiUrl = config.apiUrl || process.env.SPRITES_API_URL || 'https://api.sprites.dev/v1';
35
+ this.timeout = config.timeout || 60000; // 60 second default (sprites boot is fast but setup may take time)
36
+
37
+ if (!this.apiKey) {
38
+ throw new Error('SPRITES_TOKEN is required for SpritesProvider');
39
+ }
40
+ }
41
+
42
+ /**
43
+ * Create headers for Sprites API
44
+ * @private
45
+ * @returns {Object} HTTP headers
46
+ */
47
+ _headers() {
48
+ return {
49
+ 'Content-Type': 'application/json',
50
+ 'Authorization': `Bearer ${this.apiKey}`,
51
+ };
52
+ }
53
+
54
+ /**
55
+ * Format API error response for better debugging
56
+ * @private
57
+ * @param {Response} response - Fetch response
58
+ * @param {string} url - Request URL
59
+ * @returns {Promise<string>} Formatted error message
60
+ */
61
+ async _formatApiError(response, url) {
62
+ let body = '';
63
+ try {
64
+ body = await response.text();
65
+ } catch {
66
+ body = '';
67
+ }
68
+
69
+ const truncated = body.length > 500 ? `${body.slice(0, 500)}…` : body;
70
+ return `Sprites API error: ${response.status} ${response.statusText} (${url})\n${truncated}`;
71
+ }
72
+
73
+ /**
74
+ * Fetch with timeout support
75
+ * @private
76
+ * @param {string} url - URL to fetch
77
+ * @param {Object} options - Fetch options
78
+ * @param {number} [customTimeout] - Custom timeout in ms (overrides this.timeout)
79
+ * @returns {Promise<Response>} Fetch response
80
+ * @throws {Error} If request times out or fails
81
+ */
82
+ async _fetchWithTimeout(url, options = {}, customTimeout = null) {
83
+ const timeoutMs = customTimeout || this.timeout;
84
+ const controller = new AbortController();
85
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
86
+
87
+ try {
88
+ const response = await fetch(url, {
89
+ ...options,
90
+ signal: controller.signal,
91
+ });
92
+ clearTimeout(timeoutId);
93
+ return response;
94
+ } catch (error) {
95
+ clearTimeout(timeoutId);
96
+ if (error.name === 'AbortError') {
97
+ throw new Error(`Request timed out after ${timeoutMs}ms: ${url}`);
98
+ }
99
+ throw error;
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Escape shell argument by wrapping in single quotes and escaping any single quotes.
105
+ * This is critical for preventing shell injection attacks.
106
+ *
107
+ * @private
108
+ * @param {string} arg - Argument to escape
109
+ * @returns {string} Shell-safe escaped argument
110
+ */
111
+ _escapeShellArg(arg) {
112
+ if (arg === null || arg === undefined) return "''";
113
+ // Convert to string and replace any single quotes with '\'' (end quote, escaped quote, start quote)
114
+ return `'${String(arg).replace(/'/g, "'\\''")}'`;
115
+ }
116
+
117
+ /**
118
+ * Sanitize sprite name to match API requirements (alphanumeric, dashes, underscores only)
119
+ * @private
120
+ * @param {string} sessionId - Session identifier
121
+ * @returns {string} Sanitized sprite name
122
+ */
123
+ _sanitizeSpriteName(sessionId) {
124
+ // Remove non-alphanumeric except dashes and underscores, limit length
125
+ const sanitized = String(sessionId)
126
+ .replace(/[^a-zA-Z0-9_-]/g, '')
127
+ .substring(0, 50);
128
+ return `teleport-${sanitized}`;
129
+ }
130
+
131
+ /**
132
+ * Create a remote Sprites workspace
133
+ *
134
+ * Provisions a new Sprites micro-VM with:
135
+ * - Git repository cloned automatically
136
+ * - SSH access configured
137
+ * - LivePort tunnel setup
138
+ * - All secrets stored in Vault
139
+ * - Auto-hibernate when inactive
140
+ *
141
+ * @param {Object} sessionConfig - Session configuration (see BaseProvider)
142
+ * @returns {Promise<Object>} Sprite details with IDs for cleanup
143
+ * @throws {Error} If sprite creation fails
144
+ */
145
+ async createMachine(sessionConfig) {
146
+ this._validateSessionConfig(sessionConfig);
147
+
148
+ const sessionId = sessionConfig.sessionId;
149
+ const spriteName = this._sanitizeSpriteName(sessionId);
150
+ const namespace = `teleportationsprite${String(sessionId).replace(/[^a-zA-Z0-9]/g, '')}`;
151
+
152
+ // 1. Setup SSH access
153
+ const ssh = await this._setupSSHAccess(sessionId, namespace);
154
+
155
+ // 2. Setup LivePort tunnel
156
+ const tunnel = await this._setupTunnel(sessionId, spriteName);
157
+
158
+ // 3. Store necessary secrets in Vault
159
+ const secrets = {
160
+ RELAY_API_URL: sessionConfig.relayApiUrl,
161
+ RELAY_API_KEY: sessionConfig.relayApiKey,
162
+ GITHUB_TOKEN: sessionConfig.githubToken,
163
+ MECH_API_KEY: sessionConfig.mechApiKey,
164
+ LIVEPORT_BRIDGE_KEY: tunnel.bridgeKey,
165
+ ...sessionConfig.additionalSecrets,
166
+ };
167
+
168
+ const secretIds = await this._storeSecrets(namespace, secrets);
169
+
170
+ // 4. Create sprite configuration
171
+ const spriteConfig = {
172
+ name: spriteName,
173
+ url_settings: {
174
+ auth: 'sprite', // Use Sprites-native auth
175
+ },
176
+ // Image with development tools pre-installed
177
+ image: sessionConfig.image || 'oven/bun:latest',
178
+ // Resource allocation
179
+ guest: {
180
+ cpus: sessionConfig.cpus || 1,
181
+ memory_mb: sessionConfig.memory || 2048,
182
+ },
183
+ // Metadata for tracking
184
+ metadata: {
185
+ session_id: sessionId,
186
+ provider: 'SpritesProvider',
187
+ created_at: new Date().toISOString(),
188
+ },
189
+ };
190
+
191
+ // 5. Create the sprite
192
+ const createUrl = `${this.apiUrl}/sprites`;
193
+ const response = await this._fetchWithTimeout(createUrl, {
194
+ method: 'POST',
195
+ headers: this._headers(),
196
+ body: JSON.stringify(spriteConfig),
197
+ });
198
+
199
+ if (!response.ok) {
200
+ // Cleanup on failure
201
+ await this._cleanup(namespace, tunnel.keyId, ssh.keyId);
202
+ throw new Error(await this._formatApiError(response, createUrl));
203
+ }
204
+
205
+ const sprite = await response.json();
206
+
207
+ // 6. Execute bootstrap script on the sprite
208
+ try {
209
+ const bootstrapScript = this._generateBootstrapScript(sessionConfig, tunnel, ssh);
210
+ await this._executeBootstrap(spriteName, bootstrapScript);
211
+ } catch (error) {
212
+ // Cleanup on bootstrap failure
213
+ console.error(`[sprites-provider] Bootstrap failed: ${error.message}`);
214
+ await this.destroyMachine(spriteName).catch(() => {});
215
+ await this._cleanup(namespace, tunnel.keyId, ssh.keyId);
216
+ throw new Error(`Sprite bootstrap failed: ${error.message}`);
217
+ }
218
+
219
+ return {
220
+ machineId: spriteName,
221
+ spriteName,
222
+ spriteUrl: sprite.url || `https://${spriteName}.sprites.dev`,
223
+ tunnelUrl: tunnel.tunnelUrl,
224
+ namespace,
225
+ secretIds,
226
+ sshKeyId: ssh.keyId,
227
+ bridgeKeyId: tunnel.keyId,
228
+ status: sprite.state || 'running',
229
+ };
230
+ }
231
+
232
+ /**
233
+ * Generate bootstrap script for Sprites workspace
234
+ *
235
+ * This script runs after the sprite is created and sets up:
236
+ * - SSH authorized keys
237
+ * - Git repository clone
238
+ * - Apply uncommitted changes (if any)
239
+ * - Environment variables
240
+ * - Transcript file placement
241
+ * - LivePort tunnel
242
+ * - CLI resume
243
+ *
244
+ * SECURITY: All user-provided values MUST be escaped to prevent shell injection.
245
+ *
246
+ * @private
247
+ * @param {Object} sessionConfig - Session configuration
248
+ * @param {Object} tunnel - Tunnel configuration with tunnelCommand
249
+ * @param {Object} ssh - SSH key with publicKey
250
+ * @returns {string} Shell script for sprite initialization
251
+ */
252
+ _generateBootstrapScript(sessionConfig, tunnel, ssh) {
253
+ // CRITICAL: Escape all user-provided values to prevent command injection
254
+ const safePublicKey = this._escapeShellArg(ssh.publicKey);
255
+ const safeRepoUrl = this._escapeShellArg(sessionConfig.repoUrl);
256
+ const safeBranch = this._escapeShellArg(sessionConfig.branch || 'main');
257
+ const safeSessionId = this._escapeShellArg(sessionConfig.sessionId);
258
+ const safeTask = this._escapeShellArg(sessionConfig.task || '');
259
+ const safeRelayApiUrl = this._escapeShellArg(sessionConfig.relayApiUrl);
260
+ const safeRelayApiKey = this._escapeShellArg(sessionConfig.relayApiKey);
261
+ const safeGithubToken = this._escapeShellArg(sessionConfig.githubToken || '');
262
+ const safeTunnelCommand = tunnel.tunnelCommand; // This is generated internally, not user input
263
+
264
+ // Build script with escaped values
265
+ return `#!/bin/bash
266
+ set -e
267
+
268
+ echo "[bootstrap] Starting sprite initialization..."
269
+
270
+ # Setup SSH key for access
271
+ mkdir -p ~/.ssh
272
+ echo ${safePublicKey} >> ~/.ssh/authorized_keys
273
+ chmod 700 ~/.ssh
274
+ chmod 600 ~/.ssh/authorized_keys
275
+
276
+ # Configure git to use token for authentication
277
+ # SECURITY: Use printf with %s to safely interpolate the token without shell interpretation
278
+ # This prevents command injection even if the token contains special characters
279
+ git config --global credential.helper store
280
+ if [ -n ${safeGithubToken} ] && [ ${safeGithubToken} != "''" ]; then
281
+ printf 'https://oauth2:%s@github.com\n' ${safeGithubToken} > ~/.git-credentials
282
+ chmod 600 ~/.git-credentials
283
+ fi
284
+
285
+ # Clone repository
286
+ echo "[bootstrap] Cloning repository..."
287
+ cd /workspace
288
+ git clone ${safeRepoUrl} repo || { echo "Git clone failed"; exit 1; }
289
+ cd repo
290
+
291
+ # Checkout specified branch
292
+ git checkout ${safeBranch} || git checkout -b ${safeBranch}
293
+
294
+ # Apply uncommitted changes if provided
295
+ if [ -n "${sessionConfig.patch ? '1' : ''}" ]; then
296
+ echo "[bootstrap] Applying uncommitted changes..."
297
+ echo ${this._escapeShellArg(sessionConfig.patch || '')} | base64 -d | git apply --allow-empty || echo "Patch apply warning (continuing)"
298
+ fi
299
+
300
+ # Set environment variables
301
+ export RELAY_API_URL=${safeRelayApiUrl}
302
+ export RELAY_API_KEY=${safeRelayApiKey}
303
+ export SESSION_ID=${safeSessionId}
304
+
305
+ # Install dependencies
306
+ echo "[bootstrap] Installing dependencies..."
307
+ bun install || npm install
308
+
309
+ # Start LivePort tunnel in background
310
+ echo "[bootstrap] Starting LivePort tunnel..."
311
+ ${safeTunnelCommand} &
312
+ TUNNEL_PID=$!
313
+
314
+ # Wait for tunnel to be ready
315
+ for i in 1 2 3 4 5; do
316
+ if ! kill -0 "$TUNNEL_PID" 2>/dev/null; then
317
+ echo "[bootstrap] LivePort tunnel failed to start"
318
+ exit 1
319
+ fi
320
+ sleep 1
321
+ done
322
+
323
+ echo "[bootstrap] Sprite ready!"
324
+
325
+ # If a task was provided, start the teleportation daemon
326
+ if [ -n ${safeTask} ] && [ ${safeTask} != "''" ]; then
327
+ echo "[bootstrap] Starting task: ${safeTask}"
328
+ bun teleportation-cli.cjs daemon start --session-id ${safeSessionId} --task ${safeTask} &
329
+ fi
330
+ `;
331
+ }
332
+
333
+ /**
334
+ * Execute bootstrap script on sprite
335
+ *
336
+ * Bootstrap operations (git clone, package install, etc.) can take significant time,
337
+ * so we use a longer timeout (5 minutes) than normal API requests.
338
+ *
339
+ * @private
340
+ * @param {string} spriteName - Sprite name
341
+ * @param {string} script - Bootstrap script to execute
342
+ * @returns {Promise<Object>} Execution result
343
+ * @throws {Error} If bootstrap times out or fails
344
+ */
345
+ async _executeBootstrap(spriteName, script) {
346
+ const BOOTSTRAP_TIMEOUT_MS = 300000; // 5 minutes for git clone + npm/bun install
347
+
348
+ const result = await this.executeCommand(spriteName, script, { timeout: BOOTSTRAP_TIMEOUT_MS });
349
+ if (result.exitCode !== 0) {
350
+ throw new Error(`Bootstrap script failed with exit code ${result.exitCode}: ${result.stderr}`);
351
+ }
352
+ return result;
353
+ }
354
+
355
+ /**
356
+ * Execute command on sprite
357
+ *
358
+ * @param {string} spriteName - Sprite name
359
+ * @param {string} command - Command to execute
360
+ * @param {Object} [options] - Execution options
361
+ * @param {number} [options.timeout] - Custom timeout in ms (default: this.timeout)
362
+ * @returns {Promise<Object>} Execution result
363
+ * @returns {string} return.stdout - Standard output
364
+ * @returns {string} return.stderr - Standard error
365
+ * @returns {number} return.exitCode - Exit code
366
+ * @throws {Error} If execution fails or times out
367
+ */
368
+ async executeCommand(spriteName, command, options = {}) {
369
+ this._validateMachineId(spriteName);
370
+
371
+ const url = `${this.apiUrl}/sprites/${spriteName}/exec`;
372
+ const response = await this._fetchWithTimeout(url, {
373
+ method: 'POST',
374
+ headers: this._headers(),
375
+ body: JSON.stringify({ command }),
376
+ }, options.timeout);
377
+
378
+ if (!response.ok) {
379
+ throw new Error(await this._formatApiError(response, url));
380
+ }
381
+
382
+ const result = await response.json();
383
+ return {
384
+ stdout: result.stdout || '',
385
+ stderr: result.stderr || '',
386
+ exitCode: result.exit_code ?? result.exitCode ?? 0,
387
+ };
388
+ }
389
+
390
+ /**
391
+ * Create a checkpoint of the sprite's current state
392
+ *
393
+ * Sprites checkpoints are fast (~300ms) and allow instant restore.
394
+ *
395
+ * @param {string} spriteName - Sprite name
396
+ * @param {string} [comment] - Optional comment for the checkpoint
397
+ * @returns {Promise<Object>} Checkpoint details
398
+ * @returns {string} return.checkpointId - Unique checkpoint ID
399
+ * @returns {string} return.createdAt - Checkpoint creation timestamp
400
+ * @throws {Error} If checkpoint creation fails
401
+ */
402
+ async createCheckpoint(spriteName, comment = '') {
403
+ this._validateMachineId(spriteName);
404
+
405
+ const url = `${this.apiUrl}/sprites/${spriteName}/checkpoints`;
406
+ const response = await this._fetchWithTimeout(url, {
407
+ method: 'POST',
408
+ headers: this._headers(),
409
+ body: JSON.stringify({
410
+ comment: comment || `Checkpoint at ${new Date().toISOString()}`,
411
+ }),
412
+ });
413
+
414
+ if (!response.ok) {
415
+ throw new Error(await this._formatApiError(response, url));
416
+ }
417
+
418
+ const checkpoint = await response.json();
419
+ return {
420
+ checkpointId: checkpoint.id,
421
+ createdAt: checkpoint.created_at || checkpoint.createdAt,
422
+ comment: checkpoint.comment,
423
+ };
424
+ }
425
+
426
+ /**
427
+ * List all checkpoints for a sprite
428
+ *
429
+ * @param {string} spriteName - Sprite name
430
+ * @returns {Promise<Array>} Array of checkpoint objects
431
+ * @throws {Error} If listing fails
432
+ */
433
+ async listCheckpoints(spriteName) {
434
+ this._validateMachineId(spriteName);
435
+
436
+ const url = `${this.apiUrl}/sprites/${spriteName}/checkpoints`;
437
+ const response = await this._fetchWithTimeout(url, {
438
+ method: 'GET',
439
+ headers: this._headers(),
440
+ });
441
+
442
+ if (!response.ok) {
443
+ throw new Error(await this._formatApiError(response, url));
444
+ }
445
+
446
+ const data = await response.json();
447
+ return data.checkpoints || data || [];
448
+ }
449
+
450
+ /**
451
+ * Restore sprite to a previous checkpoint
452
+ *
453
+ * @param {string} spriteName - Sprite name
454
+ * @param {string} checkpointId - Checkpoint ID to restore
455
+ * @returns {Promise<Object>} Restore result
456
+ * @throws {Error} If restore fails
457
+ */
458
+ async restoreCheckpoint(spriteName, checkpointId) {
459
+ this._validateMachineId(spriteName);
460
+
461
+ if (!checkpointId) {
462
+ throw new Error('Checkpoint ID is required for restore');
463
+ }
464
+
465
+ const url = `${this.apiUrl}/sprites/${spriteName}/restore`;
466
+ const response = await this._fetchWithTimeout(url, {
467
+ method: 'POST',
468
+ headers: this._headers(),
469
+ body: JSON.stringify({ checkpoint_id: checkpointId }),
470
+ });
471
+
472
+ if (!response.ok) {
473
+ throw new Error(await this._formatApiError(response, url));
474
+ }
475
+
476
+ return response.json();
477
+ }
478
+
479
+ /**
480
+ * Destroy the sprite
481
+ *
482
+ * Permanently deletes the sprite and all its checkpoints.
483
+ *
484
+ * @param {string} spriteName - Sprite name
485
+ * @returns {Promise<boolean>} True if destroyed successfully
486
+ * @throws {Error} If destruction fails (except 404 which is success)
487
+ */
488
+ async destroyMachine(spriteName) {
489
+ this._validateMachineId(spriteName);
490
+
491
+ const url = `${this.apiUrl}/sprites/${spriteName}`;
492
+ const response = await this._fetchWithTimeout(url, {
493
+ method: 'DELETE',
494
+ headers: this._headers(),
495
+ });
496
+
497
+ // 404 means already deleted - treat as success
498
+ if (response.status === 404) {
499
+ return true;
500
+ }
501
+
502
+ if (!response.ok) {
503
+ throw new Error(await this._formatApiError(response, url));
504
+ }
505
+
506
+ return true;
507
+ }
508
+
509
+ /**
510
+ * Get sprite status
511
+ *
512
+ * Returns current state of the sprite.
513
+ *
514
+ * @param {string} spriteName - Sprite name
515
+ * @returns {Promise<Object>} Sprite status
516
+ * @returns {string} return.status - Sprite state ('cold', 'warm', 'running')
517
+ * @returns {string} return.url - Sprite URL
518
+ * @returns {string} return.createdAt - Creation timestamp
519
+ * @returns {string} return.lastActive - Last activity timestamp
520
+ * @throws {Error} If status fetch fails
521
+ */
522
+ async getMachineStatus(spriteName) {
523
+ this._validateMachineId(spriteName);
524
+
525
+ const url = `${this.apiUrl}/sprites/${spriteName}`;
526
+ const response = await this._fetchWithTimeout(url, {
527
+ method: 'GET',
528
+ headers: this._headers(),
529
+ });
530
+
531
+ if (!response.ok) {
532
+ throw new Error(await this._formatApiError(response, url));
533
+ }
534
+
535
+ const sprite = await response.json();
536
+
537
+ // Map Sprites states to standardized status
538
+ const statusMap = {
539
+ 'cold': 'stopped',
540
+ 'warm': 'hibernating',
541
+ 'running': 'running',
542
+ 'starting': 'starting',
543
+ 'stopping': 'stopping',
544
+ };
545
+
546
+ return {
547
+ status: statusMap[sprite.state] || sprite.state || 'unknown',
548
+ url: sprite.url,
549
+ createdAt: sprite.created_at || sprite.createdAt,
550
+ lastActive: sprite.last_active || sprite.lastActive,
551
+ state: sprite.state, // Original Sprites state
552
+ };
553
+ }
554
+
555
+ /**
556
+ * Get sprite logs
557
+ *
558
+ * @param {string} spriteName - Sprite name
559
+ * @param {Object} [options] - Log retrieval options
560
+ * @param {number} [options.tail] - Number of lines to retrieve
561
+ * @param {boolean} [options.follow] - Stream logs (not implemented)
562
+ * @returns {Promise<Object>} Log data
563
+ * @throws {Error} If log retrieval fails
564
+ */
565
+ async getLogs(spriteName, options = {}) {
566
+ this._validateMachineId(spriteName);
567
+
568
+ const params = new URLSearchParams();
569
+ if (options.tail) params.set('tail', options.tail);
570
+
571
+ const url = `${this.apiUrl}/sprites/${spriteName}/logs${params.toString() ? '?' + params : ''}`;
572
+ const response = await this._fetchWithTimeout(url, {
573
+ method: 'GET',
574
+ headers: this._headers(),
575
+ });
576
+
577
+ if (!response.ok) {
578
+ throw new Error(await this._formatApiError(response, url));
579
+ }
580
+
581
+ const data = await response.json();
582
+ return {
583
+ logs: data.logs || data || [],
584
+ };
585
+ }
586
+
587
+ /**
588
+ * Wake up a hibernating sprite
589
+ *
590
+ * Sprites auto-hibernate after 30 seconds of inactivity.
591
+ * This method wakes them up.
592
+ *
593
+ * @param {string} spriteName - Sprite name
594
+ * @returns {Promise<Object>} Wake result
595
+ * @throws {Error} If wake fails
596
+ */
597
+ async wakeSprite(spriteName) {
598
+ this._validateMachineId(spriteName);
599
+
600
+ const url = `${this.apiUrl}/sprites/${spriteName}/wake`;
601
+ const response = await this._fetchWithTimeout(url, {
602
+ method: 'POST',
603
+ headers: this._headers(),
604
+ });
605
+
606
+ if (!response.ok) {
607
+ throw new Error(await this._formatApiError(response, url));
608
+ }
609
+
610
+ return response.json();
611
+ }
612
+
613
+ /**
614
+ * Get provider capabilities
615
+ *
616
+ * @returns {Object} Provider capabilities
617
+ */
618
+ getCapabilities() {
619
+ return {
620
+ supportsCheckpoints: true,
621
+ supportsSnapshots: true,
622
+ supportsHibernation: true,
623
+ supportsAutoStop: true,
624
+ autoHibernate: true,
625
+ bootTime: 10000, // ~10 seconds
626
+ checkpointTime: 300, // ~300ms
627
+ supportsResume: true,
628
+ provider: 'sprites',
629
+ };
630
+ }
631
+
632
+ /**
633
+ * Check provider health
634
+ *
635
+ * @returns {Promise<Object>} Health check result
636
+ */
637
+ async checkHealth() {
638
+ try {
639
+ // Try to list sprites - if this works, the API is healthy
640
+ const url = `${this.apiUrl}/sprites`;
641
+ const response = await this._fetchWithTimeout(url, {
642
+ method: 'GET',
643
+ headers: this._headers(),
644
+ }, 10000); // 10 second timeout for health check
645
+
646
+ if (!response.ok) {
647
+ return {
648
+ healthy: false,
649
+ provider: 'sprites',
650
+ error: `API returned status ${response.status}`,
651
+ };
652
+ }
653
+
654
+ return {
655
+ healthy: true,
656
+ provider: 'sprites',
657
+ details: {
658
+ apiUrl: this.apiUrl,
659
+ timestamp: new Date().toISOString(),
660
+ },
661
+ };
662
+ } catch (error) {
663
+ return {
664
+ healthy: false,
665
+ provider: 'sprites',
666
+ error: error.message,
667
+ };
668
+ }
669
+ }
670
+
671
+ /**
672
+ * Update sprite labels/tags
673
+ *
674
+ * @param {string} spriteName - Sprite name
675
+ * @param {Object} labels - Key-value pairs of labels
676
+ * @returns {Promise<Object>} Updated sprite info
677
+ */
678
+ async updateLabels(spriteName, labels) {
679
+ this._validateMachineId(spriteName);
680
+
681
+ if (!labels || typeof labels !== 'object') {
682
+ throw new Error('Labels must be an object');
683
+ }
684
+
685
+ const url = `${this.apiUrl}/sprites/${spriteName}`;
686
+ const response = await this._fetchWithTimeout(url, {
687
+ method: 'PATCH',
688
+ headers: this._headers(),
689
+ body: JSON.stringify({ labels }),
690
+ });
691
+
692
+ if (!response.ok) {
693
+ throw new Error(await this._formatApiError(response, url));
694
+ }
695
+
696
+ return response.json();
697
+ }
698
+
699
+ /**
700
+ * Get sprite labels
701
+ *
702
+ * @param {string} spriteName - Sprite name
703
+ * @returns {Promise<Object>} Labels object
704
+ */
705
+ async getLabels(spriteName) {
706
+ const status = await this.getMachineStatus(spriteName);
707
+ return status.labels || {};
708
+ }
709
+ }
710
+
711
+ export default SpritesProvider;