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,620 @@
1
+ /**
2
+ * Teleport Manager
3
+ *
4
+ * Orchestrates the full teleportation flow:
5
+ * 1. Capture local session state
6
+ * 2. Upload state to relay API
7
+ * 3. Provision cloud environment
8
+ * 4. Export and upload transcript
9
+ * 5. Resume session on cloud
10
+ *
11
+ * @module lib/teleport/manager
12
+ */
13
+
14
+ import { nanoid } from 'nanoid';
15
+ import { captureSessionState } from './session-capture.js';
16
+ import { getExporterForCoder } from './exporters/index.js';
17
+ import { GitCommitter } from './git-committer.js';
18
+ import { ResultsDelivery } from '../remote/results-delivery.js';
19
+
20
+ /**
21
+ * Teleport modes
22
+ * @enum {string}
23
+ */
24
+ export const TeleportMode = {
25
+ /** Pause local session, continue on cloud */
26
+ PAUSE: 'pause',
27
+ /** Fork: local continues, cloud runs in parallel */
28
+ FORK: 'fork',
29
+ };
30
+
31
+ /**
32
+ * Teleport status
33
+ * @enum {string}
34
+ */
35
+ export const TeleportStatus = {
36
+ PENDING: 'pending',
37
+ CAPTURING: 'capturing',
38
+ UPLOADING: 'uploading',
39
+ PROVISIONING: 'provisioning',
40
+ EXPORTING: 'exporting',
41
+ RESUMING: 'resuming',
42
+ RUNNING: 'running',
43
+ COMPLETED: 'completed',
44
+ FAILED: 'failed',
45
+ };
46
+
47
+ /**
48
+ * TeleportManager orchestrates session teleportation to cloud environments.
49
+ */
50
+ export class TeleportManager {
51
+ /**
52
+ * Initialize TeleportManager
53
+ *
54
+ * @param {Object} config - Manager configuration
55
+ * @param {Object} config.providerFactory - Provider factory instance
56
+ * @param {string} config.relayApiUrl - Relay API URL
57
+ * @param {string} config.relayApiKey - Relay API key
58
+ * @param {Object} [config.vaultClient] - Vault client for secrets
59
+ * @param {Object} [config.livePortClient] - LivePort client for tunnels
60
+ */
61
+ constructor(config) {
62
+ if (!config) {
63
+ throw new Error('TeleportManager config is required');
64
+ }
65
+ if (!config.providerFactory) {
66
+ throw new Error('providerFactory is required');
67
+ }
68
+ if (!config.relayApiUrl) {
69
+ throw new Error('relayApiUrl is required');
70
+ }
71
+ if (!config.relayApiKey) {
72
+ throw new Error('relayApiKey is required');
73
+ }
74
+
75
+ this.providerFactory = config.providerFactory;
76
+ this.relayApiUrl = config.relayApiUrl;
77
+ this.relayApiKey = config.relayApiKey;
78
+ this.vaultClient = config.vaultClient;
79
+ this.livePortClient = config.livePortClient;
80
+
81
+ // Track active teleports
82
+ this.activeTeleports = new Map();
83
+ }
84
+
85
+ /**
86
+ * Initiate a teleportation session.
87
+ *
88
+ * @param {string} sessionId - Local session ID
89
+ * @param {Object} options - Teleport options
90
+ * @param {string} [options.provider] - Force specific provider
91
+ * @param {string} [options.targetCoder='claude-code'] - Target CLI (claude-code, gemini-cli)
92
+ * @param {TeleportMode} [options.mode='pause'] - Teleport mode
93
+ * @param {string} [options.task] - Task description for the cloud session
94
+ * @param {string} [options.cwd] - Working directory
95
+ * @param {string} [options.claudeSessionId] - Claude session ID for transcript
96
+ * @param {string} [options.githubToken] - GitHub token for repo access
97
+ * @param {Function} [options.onProgress] - Progress callback
98
+ * @returns {Promise<Object>} Teleport result
99
+ */
100
+ async initiateTeleport(sessionId, options = {}) {
101
+ const {
102
+ provider,
103
+ targetCoder = 'claude-code',
104
+ mode = TeleportMode.PAUSE,
105
+ task,
106
+ cwd,
107
+ claudeSessionId,
108
+ githubToken,
109
+ onProgress,
110
+ commitInterval,
111
+ } = options;
112
+
113
+ // Generate teleport ID
114
+ const teleportId = `tel_${nanoid(12)}`;
115
+
116
+ // Initialize teleport state
117
+ const teleportState = {
118
+ teleportId,
119
+ sessionId,
120
+ status: TeleportStatus.PENDING,
121
+ targetCoder,
122
+ mode,
123
+ task,
124
+ createdAt: new Date().toISOString(),
125
+ provider: null,
126
+ machineId: null,
127
+ error: null,
128
+ gitCommitter: null,
129
+ };
130
+
131
+ this.activeTeleports.set(teleportId, teleportState);
132
+
133
+ try {
134
+ // Step 1: Capture session state
135
+ this._updateStatus(teleportId, TeleportStatus.CAPTURING, onProgress);
136
+ const sessionState = await this._captureSession(sessionId, {
137
+ cwd,
138
+ claudeSessionId,
139
+ });
140
+
141
+ // Store session state for later use (e.g., deliverResults)
142
+ teleportState.sessionState = sessionState;
143
+
144
+ // Step 2: Upload session state to relay
145
+ this._updateStatus(teleportId, TeleportStatus.UPLOADING, onProgress);
146
+ const teleportRecord = await this._uploadSessionState(teleportId, sessionId, sessionState, {
147
+ targetCoder,
148
+ mode,
149
+ task,
150
+ });
151
+
152
+ // Step 3: Provision cloud environment
153
+ this._updateStatus(teleportId, TeleportStatus.PROVISIONING, onProgress);
154
+ const cloudMachine = await this._provisionCloud(teleportId, sessionState, {
155
+ provider,
156
+ targetCoder,
157
+ task,
158
+ githubToken,
159
+ });
160
+
161
+ teleportState.provider = cloudMachine.provider;
162
+ teleportState.machineId = cloudMachine.machineId;
163
+
164
+ // Step 4: Export and upload transcript
165
+ this._updateStatus(teleportId, TeleportStatus.EXPORTING, onProgress);
166
+ const transcriptPath = await this._prepareTranscript(sessionId, targetCoder, cloudMachine);
167
+
168
+ // Step 5: Resume session on cloud
169
+ this._updateStatus(teleportId, TeleportStatus.RESUMING, onProgress);
170
+ await this._resumeSession(teleportId, cloudMachine, transcriptPath, {
171
+ targetCoder,
172
+ task,
173
+ claudeSessionId,
174
+ });
175
+
176
+ // Success!
177
+ this._updateStatus(teleportId, TeleportStatus.RUNNING, onProgress);
178
+
179
+ // Start auto-commit if we have repo info
180
+ if (sessionState.git && sessionState.cwd) {
181
+ const gitCommitter = new GitCommitter({
182
+ repoPath: sessionState.cwd,
183
+ branch: `teleport/${teleportId}`,
184
+ teleportId,
185
+ interval: options.commitInterval,
186
+ });
187
+
188
+ teleportState.gitCommitter = gitCommitter;
189
+ gitCommitter.startAutoCommit();
190
+ }
191
+
192
+ return {
193
+ teleportId,
194
+ sessionId,
195
+ provider: cloudMachine.provider,
196
+ machineId: cloudMachine.machineId,
197
+ spriteUrl: cloudMachine.spriteUrl,
198
+ tunnelUrl: cloudMachine.tunnelUrl,
199
+ targetCoder,
200
+ mode,
201
+ status: TeleportStatus.RUNNING,
202
+ timelineUrl: `${this.relayApiUrl}/sessions/${sessionId}/timeline`,
203
+ };
204
+ } catch (error) {
205
+ teleportState.status = TeleportStatus.FAILED;
206
+ teleportState.error = error.message;
207
+
208
+ // Attempt cleanup on failure
209
+ await this._cleanupOnError(teleportId, teleportState);
210
+
211
+ throw error;
212
+ }
213
+ }
214
+
215
+ /**
216
+ * Capture local session state.
217
+ *
218
+ * @private
219
+ * @param {string} sessionId - Session ID
220
+ * @param {Object} options - Capture options
221
+ * @returns {Promise<Object>} Session state
222
+ */
223
+ async _captureSession(sessionId, options = {}) {
224
+ return captureSessionState(sessionId, {
225
+ cwd: options.cwd,
226
+ claudeSessionId: options.claudeSessionId,
227
+ includeUntracked: false, // Don't include untracked files by default
228
+ });
229
+ }
230
+
231
+ /**
232
+ * Upload session state to relay API.
233
+ *
234
+ * @private
235
+ * @param {string} teleportId - Teleport ID
236
+ * @param {string} sessionId - Session ID
237
+ * @param {Object} sessionState - Captured session state
238
+ * @param {Object} options - Upload options
239
+ * @returns {Promise<Object>} Teleport record from relay
240
+ */
241
+ async _uploadSessionState(teleportId, sessionId, sessionState, options = {}) {
242
+ const response = await fetch(`${this.relayApiUrl}/api/sessions/${sessionId}/teleport`, {
243
+ method: 'POST',
244
+ headers: {
245
+ 'Content-Type': 'application/json',
246
+ 'Authorization': `Bearer ${this.relayApiKey}`,
247
+ },
248
+ body: JSON.stringify({
249
+ teleport_id: teleportId,
250
+ git_state: sessionState.git,
251
+ patch: sessionState.patch,
252
+ target_coder: options.targetCoder,
253
+ mode: options.mode,
254
+ task: options.task,
255
+ meta: sessionState.meta,
256
+ }),
257
+ });
258
+
259
+ if (!response.ok) {
260
+ const error = await response.text();
261
+ throw new Error(`Failed to upload session state: ${response.status} ${error}`);
262
+ }
263
+
264
+ return response.json();
265
+ }
266
+
267
+ /**
268
+ * Provision cloud environment.
269
+ *
270
+ * @private
271
+ * @param {string} teleportId - Teleport ID
272
+ * @param {Object} sessionState - Session state
273
+ * @param {Object} options - Provisioning options
274
+ * @returns {Promise<Object>} Cloud machine details
275
+ */
276
+ async _provisionCloud(teleportId, sessionState, options = {}) {
277
+ // Select provider based on task or explicit choice
278
+ const providerType = this.providerFactory.selectProvider(
279
+ options.task || '',
280
+ { provider: options.provider }
281
+ );
282
+
283
+ // Create provider instance
284
+ const provider = this.providerFactory.createProvider(providerType);
285
+
286
+ // Configure session for cloud
287
+ const sessionConfig = {
288
+ sessionId: teleportId,
289
+ repoUrl: sessionState.git.repoUrl,
290
+ branch: sessionState.git.branch,
291
+ relayApiUrl: this.relayApiUrl,
292
+ relayApiKey: this.relayApiKey,
293
+ githubToken: options.githubToken || process.env.GITHUB_TOKEN,
294
+ mechApiKey: process.env.MECH_API_KEY,
295
+ task: options.task,
296
+ patch: sessionState.patch,
297
+ };
298
+
299
+ // Create the machine
300
+ const machine = await provider.createMachine(sessionConfig);
301
+
302
+ return {
303
+ provider: providerType,
304
+ machineId: machine.machineId,
305
+ spriteUrl: machine.spriteUrl,
306
+ tunnelUrl: machine.tunnelUrl,
307
+ namespace: machine.namespace,
308
+ sshKeyId: machine.sshKeyId,
309
+ bridgeKeyId: machine.bridgeKeyId,
310
+ };
311
+ }
312
+
313
+ /**
314
+ * Prepare and upload transcript for target CLI.
315
+ *
316
+ * @private
317
+ * @param {string} sessionId - Session ID
318
+ * @param {string} targetCoder - Target CLI name
319
+ * @param {Object} cloudMachine - Cloud machine details
320
+ * @returns {Promise<string>} Transcript path on cloud
321
+ */
322
+ async _prepareTranscript(sessionId, targetCoder, cloudMachine) {
323
+ // Fetch timeline events from relay
324
+ const response = await fetch(`${this.relayApiUrl}/api/sessions/${sessionId}/timeline`, {
325
+ headers: {
326
+ 'Authorization': `Bearer ${this.relayApiKey}`,
327
+ },
328
+ });
329
+
330
+ if (!response.ok) {
331
+ // Timeline might not exist yet - that's OK
332
+ if (response.status === 404) {
333
+ return null;
334
+ }
335
+ throw new Error(`Failed to fetch timeline: ${response.status}`);
336
+ }
337
+
338
+ const timeline = await response.json();
339
+ const events = timeline.events || [];
340
+
341
+ if (events.length === 0) {
342
+ return null; // No transcript to export
343
+ }
344
+
345
+ // Get appropriate exporter
346
+ const exporter = await getExporterForCoder(targetCoder);
347
+
348
+ // Export to CLI format
349
+ const transcript = await exporter.export(events);
350
+
351
+ // Upload transcript to relay for cloud to download
352
+ const uploadResponse = await fetch(
353
+ `${this.relayApiUrl}/api/teleports/${cloudMachine.machineId}/transcript`,
354
+ {
355
+ method: 'PUT',
356
+ headers: {
357
+ 'Content-Type': 'application/json',
358
+ 'Authorization': `Bearer ${this.relayApiKey}`,
359
+ },
360
+ body: JSON.stringify({
361
+ content: transcript,
362
+ format: targetCoder,
363
+ }),
364
+ }
365
+ );
366
+
367
+ if (!uploadResponse.ok) {
368
+ throw new Error(`Failed to upload transcript: ${uploadResponse.status}`);
369
+ }
370
+
371
+ const result = await uploadResponse.json();
372
+ return result.path;
373
+ }
374
+
375
+ /**
376
+ * Resume session on cloud environment.
377
+ *
378
+ * @private
379
+ * @param {string} teleportId - Teleport ID
380
+ * @param {Object} cloudMachine - Cloud machine details
381
+ * @param {string|null} transcriptPath - Path to transcript on cloud
382
+ * @param {Object} options - Resume options
383
+ * @returns {Promise<void>}
384
+ */
385
+ async _resumeSession(teleportId, cloudMachine, transcriptPath, options = {}) {
386
+ // The bootstrap script on the cloud should handle:
387
+ // 1. Writing transcript to CLI-native location
388
+ // 2. Starting the CLI with --resume flag
389
+ // 3. Starting the teleportation daemon
390
+
391
+ // Notify relay that teleport is ready
392
+ await fetch(`${this.relayApiUrl}/api/teleports/${teleportId}/ready`, {
393
+ method: 'POST',
394
+ headers: {
395
+ 'Content-Type': 'application/json',
396
+ 'Authorization': `Bearer ${this.relayApiKey}`,
397
+ },
398
+ body: JSON.stringify({
399
+ machine_id: cloudMachine.machineId,
400
+ provider: cloudMachine.provider,
401
+ transcript_path: transcriptPath,
402
+ target_coder: options.targetCoder,
403
+ }),
404
+ });
405
+ }
406
+
407
+ /**
408
+ * Update teleport status and notify callback.
409
+ *
410
+ * @private
411
+ * @param {string} teleportId - Teleport ID
412
+ * @param {TeleportStatus} status - New status
413
+ * @param {Function} [onProgress] - Progress callback
414
+ */
415
+ _updateStatus(teleportId, status, onProgress) {
416
+ const state = this.activeTeleports.get(teleportId);
417
+ if (state) {
418
+ state.status = status;
419
+ state.updatedAt = new Date().toISOString();
420
+ }
421
+
422
+ if (onProgress) {
423
+ onProgress({ teleportId, status });
424
+ }
425
+ }
426
+
427
+ /**
428
+ * Cleanup resources on error.
429
+ *
430
+ * @private
431
+ * @param {string} teleportId - Teleport ID
432
+ * @param {Object} teleportState - Teleport state
433
+ */
434
+ async _cleanupOnError(teleportId, teleportState) {
435
+ try {
436
+ // Stop auto-commits
437
+ if (teleportState.gitCommitter) {
438
+ teleportState.gitCommitter.stopAutoCommit();
439
+ }
440
+
441
+ if (teleportState.machineId && teleportState.provider) {
442
+ const provider = this.providerFactory.createProvider(teleportState.provider);
443
+ await provider.destroyMachine(teleportState.machineId);
444
+ }
445
+ } catch (cleanupError) {
446
+ console.error(`[teleport-manager] Cleanup failed for ${teleportId}:`, cleanupError.message);
447
+ }
448
+ }
449
+
450
+ /**
451
+ * Get teleport status.
452
+ *
453
+ * @param {string} teleportId - Teleport ID
454
+ * @returns {Object|null} Teleport state or null if not found
455
+ */
456
+ getTeleportStatus(teleportId) {
457
+ return this.activeTeleports.get(teleportId) || null;
458
+ }
459
+
460
+ /**
461
+ * Stop a running teleport and destroy cloud resources.
462
+ *
463
+ * @param {string} teleportId - Teleport ID
464
+ * @param {Object} [options] - Stop options
465
+ * @param {boolean} [options.success=true] - Whether task completed successfully
466
+ * @param {string} [options.description] - Final commit description
467
+ * @param {string} [options.reason] - Reason for stopping (if not success)
468
+ * @param {boolean} [options.createPR=false] - Whether to create PR with results
469
+ * @param {string} [options.baseBranch] - Base branch for PR
470
+ * @returns {Promise<Object>} Stop result with optional PR details
471
+ */
472
+ async stopTeleport(teleportId, options = {}) {
473
+ const state = this.activeTeleports.get(teleportId);
474
+ if (!state) {
475
+ return { success: false, error: 'Teleport not found' };
476
+ }
477
+
478
+ const { success = true, description, reason, createPR = false, baseBranch } = options;
479
+
480
+ try {
481
+ // Stop auto-commits and create final commit
482
+ if (state.gitCommitter) {
483
+ state.gitCommitter.stopAutoCommit();
484
+
485
+ if (success) {
486
+ await state.gitCommitter.createFinalCommit(
487
+ description || state.task || 'Task completed'
488
+ );
489
+ } else {
490
+ await state.gitCommitter.createPartialCommit(
491
+ description || 'Partial work completed',
492
+ reason || 'Session stopped'
493
+ );
494
+ }
495
+
496
+ // Push to remote before destroying machine
497
+ if (createPR) {
498
+ try {
499
+ await state.gitCommitter.push();
500
+ } catch (pushError) {
501
+ console.error(`[teleport-manager] Push failed: ${pushError.message}`);
502
+ }
503
+ }
504
+ }
505
+
506
+ if (state.machineId && state.provider) {
507
+ const provider = this.providerFactory.createProvider(state.provider);
508
+ await provider.destroyMachine(state.machineId);
509
+ }
510
+
511
+ state.status = TeleportStatus.COMPLETED;
512
+ state.completedAt = new Date().toISOString();
513
+
514
+ const result = { success: true };
515
+
516
+ // Create PR if requested
517
+ if (createPR) {
518
+ try {
519
+ const deliveryResult = await this.deliverResults(teleportId, { baseBranch });
520
+ result.delivery = deliveryResult;
521
+ } catch (deliveryError) {
522
+ result.deliveryError = deliveryError.message;
523
+ }
524
+ }
525
+
526
+ return result;
527
+ } catch (error) {
528
+ state.error = error.message;
529
+ throw error;
530
+ }
531
+ }
532
+
533
+ /**
534
+ * List all active teleports.
535
+ *
536
+ * @returns {Object[]} Array of teleport states
537
+ */
538
+ listActiveTeleports() {
539
+ return Array.from(this.activeTeleports.values());
540
+ }
541
+
542
+ /**
543
+ * Update activity description for WIP commits.
544
+ *
545
+ * @param {string} teleportId - Teleport ID
546
+ * @param {string} activity - Current activity description
547
+ */
548
+ updateActivity(teleportId, activity) {
549
+ const state = this.activeTeleports.get(teleportId);
550
+ if (state && state.gitCommitter) {
551
+ state.gitCommitter.updateActivity(activity);
552
+ }
553
+ }
554
+
555
+ /**
556
+ * Get commit statistics for a teleport session.
557
+ *
558
+ * @param {string} teleportId - Teleport ID
559
+ * @returns {Object|null} Commit stats or null if not found
560
+ */
561
+ getCommitStats(teleportId) {
562
+ const state = this.activeTeleports.get(teleportId);
563
+ if (state && state.gitCommitter) {
564
+ return state.gitCommitter.getStats();
565
+ }
566
+ return null;
567
+ }
568
+
569
+ /**
570
+ * Deliver results from a completed teleport session.
571
+ *
572
+ * Creates a PR (preferred) or falls back to a local branch.
573
+ *
574
+ * @param {string} teleportId - Teleport ID
575
+ * @param {Object} [options] - Delivery options
576
+ * @param {string} [options.baseBranch] - Base branch for PR (defaults to 'main')
577
+ * @returns {Promise<Object>} Delivery result
578
+ */
579
+ async deliverResults(teleportId, options = {}) {
580
+ const state = this.activeTeleports.get(teleportId);
581
+ if (!state) {
582
+ throw new Error(`Teleport ${teleportId} not found`);
583
+ }
584
+
585
+ if (!state.sessionState || !state.sessionState.cwd) {
586
+ throw new Error(`No repository path for teleport ${teleportId}`);
587
+ }
588
+
589
+ const remoteBranch = state.gitCommitter?.branch || `teleport/${teleportId}`;
590
+
591
+ // Create ResultsDelivery instance
592
+ const resultsDelivery = new ResultsDelivery({
593
+ repoPath: state.sessionState.cwd,
594
+ sessionId: state.sessionId,
595
+ remoteBranch,
596
+ verbose: options.verbose,
597
+ });
598
+
599
+ // Deliver results with fallback strategy
600
+ const result = await resultsDelivery.deliverResults({
601
+ task: state.task,
602
+ baseBranch: options.baseBranch,
603
+ timelineUrl: `${this.relayApiUrl}/sessions/${state.sessionId}/timeline`,
604
+ metadata: {
605
+ provider: state.provider,
606
+ machineId: state.machineId,
607
+ duration: state.completedAt && state.createdAt
608
+ ? Math.floor((new Date(state.completedAt) - new Date(state.createdAt)) / 1000)
609
+ : undefined,
610
+ },
611
+ });
612
+
613
+ // Update state with delivery result
614
+ state.deliveryResult = result;
615
+
616
+ return result;
617
+ }
618
+ }
619
+
620
+ export default TeleportManager;