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,153 @@
1
+ /**
2
+ * TranscriptExporter Interface
3
+ *
4
+ * Base class for CLI-specific transcript exporters.
5
+ * Converts Timeline API events to native CLI transcript formats.
6
+ *
7
+ * @module lib/teleport/exporters/interface
8
+ */
9
+
10
+ import { writeFile, mkdir } from 'fs/promises';
11
+ import { dirname } from 'path';
12
+
13
+ /**
14
+ * Timeline event types that can be exported
15
+ * @typedef {'user_message'|'assistant_response'|'tool_use'|'tool_result'|'thinking'|'compact_summary'} TimelineEventType
16
+ */
17
+
18
+ /**
19
+ * Timeline event structure from the relay API
20
+ * @typedef {Object} TimelineEvent
21
+ * @property {string} id - Unique event ID
22
+ * @property {TimelineEventType} event_type - Type of event
23
+ * @property {Object} data - Event-specific data
24
+ * @property {number} timestamp - Unix timestamp in milliseconds
25
+ * @property {string} session_id - Parent session ID
26
+ */
27
+
28
+ /**
29
+ * Export options
30
+ * @typedef {Object} ExportOptions
31
+ * @property {boolean} [includeThinking=true] - Include thinking blocks if supported
32
+ * @property {boolean} [prettyPrint=false] - Pretty-print JSON output
33
+ * @property {string} [projectSlug] - Project identifier for path generation
34
+ */
35
+
36
+ /**
37
+ * Base class for transcript exporters.
38
+ *
39
+ * Subclasses must implement:
40
+ * - export(events, options) - Convert timeline events to CLI format
41
+ * - getTranscriptPath(sessionId, projectSlug) - Get native transcript path
42
+ *
43
+ * @example
44
+ * ```js
45
+ * import { ClaudeTranscriptExporter } from './claude-exporter.js';
46
+ *
47
+ * const exporter = new ClaudeTranscriptExporter();
48
+ * const transcript = await exporter.export(timelineEvents);
49
+ * await exporter.write(timelineEvents, sessionId, projectSlug);
50
+ * ```
51
+ */
52
+ export class TranscriptExporter {
53
+ /**
54
+ * The CLI name this exporter targets
55
+ * @type {string}
56
+ */
57
+ name = 'base';
58
+
59
+ /**
60
+ * Human-readable exporter name
61
+ * @type {string}
62
+ */
63
+ displayName = 'Base Exporter';
64
+
65
+ /**
66
+ * Export timeline events to CLI-native transcript format.
67
+ *
68
+ * @abstract
69
+ * @param {TimelineEvent[]} events - Timeline events from relay API
70
+ * @param {ExportOptions} [options={}] - Export options
71
+ * @returns {Promise<string>} Transcript content in CLI-native format
72
+ * @throws {Error} If events array is invalid or export fails
73
+ */
74
+ async export(events, options = {}) {
75
+ throw new Error('TranscriptExporter.export() must be implemented by subclass');
76
+ }
77
+
78
+ /**
79
+ * Get the native transcript file path for a CLI.
80
+ *
81
+ * @abstract
82
+ * @param {string} sessionId - Session identifier
83
+ * @param {string} projectSlug - Project slug/identifier
84
+ * @returns {string} Absolute path to transcript file
85
+ */
86
+ getTranscriptPath(sessionId, projectSlug) {
87
+ throw new Error('TranscriptExporter.getTranscriptPath() must be implemented by subclass');
88
+ }
89
+
90
+ /**
91
+ * Export and write transcript to the CLI-native location.
92
+ *
93
+ * @param {TimelineEvent[]} events - Timeline events from relay API
94
+ * @param {string} sessionId - Session identifier
95
+ * @param {string} projectSlug - Project slug/identifier
96
+ * @param {ExportOptions} [options={}] - Export options
97
+ * @returns {Promise<string>} Path where transcript was written
98
+ */
99
+ async write(events, sessionId, projectSlug, options = {}) {
100
+ const transcript = await this.export(events, { ...options, projectSlug });
101
+ const path = this.getTranscriptPath(sessionId, projectSlug);
102
+
103
+ // Ensure directory exists
104
+ await mkdir(dirname(path), { recursive: true });
105
+
106
+ // Write transcript
107
+ await writeFile(path, transcript, 'utf-8');
108
+
109
+ return path;
110
+ }
111
+
112
+ /**
113
+ * Validate timeline events array.
114
+ *
115
+ * @protected
116
+ * @param {TimelineEvent[]} events - Events to validate
117
+ * @throws {Error} If events are invalid
118
+ */
119
+ validateEvents(events) {
120
+ if (!Array.isArray(events)) {
121
+ throw new Error('Events must be an array');
122
+ }
123
+
124
+ // Empty array is valid (new session with no messages)
125
+ if (events.length === 0) {
126
+ return;
127
+ }
128
+
129
+ // Check each event has required fields
130
+ for (let i = 0; i < events.length; i++) {
131
+ const event = events[i];
132
+ if (!event || typeof event !== 'object') {
133
+ throw new Error(`Event at index ${i} is not an object`);
134
+ }
135
+ if (!event.event_type) {
136
+ throw new Error(`Event at index ${i} is missing event_type`);
137
+ }
138
+ }
139
+ }
140
+
141
+ /**
142
+ * Sort events by timestamp.
143
+ *
144
+ * @protected
145
+ * @param {TimelineEvent[]} events - Events to sort
146
+ * @returns {TimelineEvent[]} Sorted events (oldest first)
147
+ */
148
+ sortEvents(events) {
149
+ return [...events].sort((a, b) => (a.timestamp || 0) - (b.timestamp || 0));
150
+ }
151
+ }
152
+
153
+ export default TranscriptExporter;
@@ -0,0 +1,415 @@
1
+ /**
2
+ * Fork Tracker
3
+ *
4
+ * Tracks parallel sessions when using fork mode teleportation.
5
+ * Monitors both local and cloud sessions, detects conflicts,
6
+ * and provides coordination utilities.
7
+ *
8
+ * @module lib/teleport/fork-tracker
9
+ */
10
+
11
+ import { exec } from 'child_process';
12
+ import { promisify } from 'util';
13
+
14
+ const execAsync = promisify(exec);
15
+
16
+ /**
17
+ * Fork session state
18
+ * @enum {string}
19
+ */
20
+ export const ForkSessionState = {
21
+ /** Session is active and making changes */
22
+ ACTIVE: 'active',
23
+ /** Session is idle (no recent activity) */
24
+ IDLE: 'idle',
25
+ /** Session has completed */
26
+ COMPLETED: 'completed',
27
+ /** Session encountered an error */
28
+ ERRORED: 'errored',
29
+ };
30
+
31
+ /**
32
+ * ForkTracker manages parallel sessions in fork mode.
33
+ */
34
+ export class ForkTracker {
35
+ /**
36
+ * Initialize ForkTracker
37
+ *
38
+ * @param {Object} config - Configuration
39
+ * @param {string} config.teleportId - Teleport ID
40
+ * @param {string} config.repoPath - Path to git repository
41
+ * @param {string} config.localBranch - Local session branch
42
+ * @param {string} config.cloudBranch - Cloud session branch
43
+ */
44
+ constructor(config) {
45
+ if (!config) {
46
+ throw new Error('ForkTracker config is required');
47
+ }
48
+ if (!config.teleportId) {
49
+ throw new Error('teleportId is required');
50
+ }
51
+ if (!config.repoPath) {
52
+ throw new Error('repoPath is required');
53
+ }
54
+
55
+ this.teleportId = config.teleportId;
56
+ this.repoPath = config.repoPath;
57
+ this.localBranch = config.localBranch || 'HEAD';
58
+ this.cloudBranch = config.cloudBranch || `teleport/${config.teleportId}`;
59
+
60
+ // Session states
61
+ this.localSession = {
62
+ state: ForkSessionState.ACTIVE,
63
+ lastActivityAt: new Date().toISOString(),
64
+ commitCount: 0,
65
+ lastCommitSha: null,
66
+ };
67
+
68
+ this.cloudSession = {
69
+ state: ForkSessionState.ACTIVE,
70
+ lastActivityAt: new Date().toISOString(),
71
+ commitCount: 0,
72
+ lastCommitSha: null,
73
+ };
74
+
75
+ // Fork point (common ancestor)
76
+ this.forkPoint = null;
77
+
78
+ // Conflict tracking
79
+ this.conflicts = [];
80
+ this.lastConflictCheck = null;
81
+ }
82
+
83
+ /**
84
+ * Initialize fork tracking.
85
+ * Records the fork point and initial state.
86
+ *
87
+ * @returns {Promise<Object>} Fork point info
88
+ */
89
+ async initialize() {
90
+ const cwd = this.repoPath;
91
+
92
+ try {
93
+ // Record current commit as fork point
94
+ const { stdout: sha } = await execAsync('git rev-parse HEAD', { cwd });
95
+ this.forkPoint = sha.trim();
96
+
97
+ // Get branch info
98
+ const { stdout: branch } = await execAsync('git rev-parse --abbrev-ref HEAD', { cwd });
99
+ this.localBranch = branch.trim();
100
+
101
+ return {
102
+ forkPoint: this.forkPoint,
103
+ localBranch: this.localBranch,
104
+ cloudBranch: this.cloudBranch,
105
+ timestamp: new Date().toISOString(),
106
+ };
107
+ } catch (error) {
108
+ throw new Error(`Failed to initialize fork tracker: ${error.message}`);
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Update local session state.
114
+ *
115
+ * @param {Object} update - Update data
116
+ * @param {ForkSessionState} [update.state] - New state
117
+ * @param {string} [update.commitSha] - Latest commit SHA
118
+ */
119
+ updateLocalSession(update) {
120
+ if (update.state) {
121
+ this.localSession.state = update.state;
122
+ }
123
+ if (update.commitSha) {
124
+ this.localSession.lastCommitSha = update.commitSha;
125
+ this.localSession.commitCount++;
126
+ }
127
+ this.localSession.lastActivityAt = new Date().toISOString();
128
+ }
129
+
130
+ /**
131
+ * Update cloud session state.
132
+ *
133
+ * @param {Object} update - Update data
134
+ * @param {ForkSessionState} [update.state] - New state
135
+ * @param {string} [update.commitSha] - Latest commit SHA
136
+ */
137
+ updateCloudSession(update) {
138
+ if (update.state) {
139
+ this.cloudSession.state = update.state;
140
+ }
141
+ if (update.commitSha) {
142
+ this.cloudSession.lastCommitSha = update.commitSha;
143
+ this.cloudSession.commitCount++;
144
+ }
145
+ this.cloudSession.lastActivityAt = new Date().toISOString();
146
+ }
147
+
148
+ /**
149
+ * Check for conflicts between local and cloud branches.
150
+ *
151
+ * @returns {Promise<Object>} Conflict check result
152
+ */
153
+ async checkConflicts() {
154
+ const cwd = this.repoPath;
155
+
156
+ try {
157
+ // Try to fetch cloud branch from origin (ignore if no remote)
158
+ await execAsync(`git fetch origin ${this.cloudBranch}`, { cwd }).catch(() => {});
159
+
160
+ // Determine cloud branch reference (origin/ prefix if remote exists, otherwise local)
161
+ let cloudRef;
162
+ try {
163
+ await execAsync(`git rev-parse origin/${this.cloudBranch}`, { cwd });
164
+ cloudRef = `origin/${this.cloudBranch}`;
165
+ } catch {
166
+ // No remote - try local branch
167
+ try {
168
+ await execAsync(`git rev-parse ${this.cloudBranch}`, { cwd });
169
+ cloudRef = this.cloudBranch;
170
+ } catch {
171
+ // Cloud branch doesn't exist yet
172
+ this.lastConflictCheck = new Date().toISOString();
173
+ this.conflicts = [];
174
+ return {
175
+ hasConflicts: false,
176
+ conflictingFiles: [],
177
+ localChanges: 0,
178
+ cloudChanges: 0,
179
+ checkedAt: this.lastConflictCheck,
180
+ };
181
+ }
182
+ }
183
+
184
+ // Get files changed in local branch since fork point
185
+ const { stdout: localChanges } = await execAsync(
186
+ `git diff --name-only ${this.forkPoint}..HEAD`,
187
+ { cwd }
188
+ ).catch(() => ({ stdout: '' }));
189
+
190
+ // Get files changed in cloud branch since fork point
191
+ const { stdout: cloudChanges } = await execAsync(
192
+ `git diff --name-only ${this.forkPoint}..${cloudRef}`,
193
+ { cwd }
194
+ ).catch(() => ({ stdout: '' }));
195
+
196
+ const localFiles = localChanges.trim().split('\n').filter(Boolean);
197
+ const cloudFiles = cloudChanges.trim().split('\n').filter(Boolean);
198
+
199
+ // Find overlapping files (potential conflicts)
200
+ const conflictingFiles = localFiles.filter(f => cloudFiles.includes(f));
201
+
202
+ this.lastConflictCheck = new Date().toISOString();
203
+
204
+ if (conflictingFiles.length > 0) {
205
+ this.conflicts = conflictingFiles;
206
+
207
+ return {
208
+ hasConflicts: true,
209
+ conflictingFiles,
210
+ localChanges: localFiles.length,
211
+ cloudChanges: cloudFiles.length,
212
+ checkedAt: this.lastConflictCheck,
213
+ };
214
+ }
215
+
216
+ this.conflicts = [];
217
+
218
+ return {
219
+ hasConflicts: false,
220
+ conflictingFiles: [],
221
+ localChanges: localFiles.length,
222
+ cloudChanges: cloudFiles.length,
223
+ checkedAt: this.lastConflictCheck,
224
+ };
225
+ } catch (error) {
226
+ throw new Error(`Failed to check conflicts: ${error.message}`);
227
+ }
228
+ }
229
+
230
+ /**
231
+ * Get divergence statistics between branches.
232
+ *
233
+ * @returns {Promise<Object>} Divergence stats
234
+ */
235
+ async getDivergence() {
236
+ const cwd = this.repoPath;
237
+
238
+ try {
239
+ // Try to fetch cloud branch from origin (ignore if no remote)
240
+ await execAsync(`git fetch origin ${this.cloudBranch}`, { cwd }).catch(() => {});
241
+
242
+ // Determine cloud branch reference
243
+ let cloudRef;
244
+ try {
245
+ await execAsync(`git rev-parse origin/${this.cloudBranch}`, { cwd });
246
+ cloudRef = `origin/${this.cloudBranch}`;
247
+ } catch {
248
+ // Try local branch
249
+ try {
250
+ await execAsync(`git rev-parse ${this.cloudBranch}`, { cwd });
251
+ cloudRef = this.cloudBranch;
252
+ } catch {
253
+ // Cloud branch doesn't exist - count local commits from fork point
254
+ if (this.forkPoint) {
255
+ const { stdout } = await execAsync(
256
+ `git rev-list --count ${this.forkPoint}..HEAD`,
257
+ { cwd }
258
+ ).catch(() => ({ stdout: '0' }));
259
+ return {
260
+ localAhead: parseInt(stdout.trim(), 10) || 0,
261
+ cloudAhead: 0,
262
+ diverged: false,
263
+ forkPoint: this.forkPoint,
264
+ };
265
+ }
266
+ return {
267
+ localAhead: this.localSession.commitCount,
268
+ cloudAhead: 0,
269
+ diverged: false,
270
+ forkPoint: this.forkPoint,
271
+ };
272
+ }
273
+ }
274
+
275
+ // Count commits ahead/behind
276
+ const { stdout: aheadBehind } = await execAsync(
277
+ `git rev-list --left-right --count HEAD...${cloudRef}`,
278
+ { cwd }
279
+ ).catch(() => ({ stdout: '0\t0' }));
280
+
281
+ const [localAhead, cloudAhead] = aheadBehind.trim().split('\t').map(Number);
282
+
283
+ return {
284
+ localAhead,
285
+ cloudAhead,
286
+ diverged: localAhead > 0 && cloudAhead > 0,
287
+ forkPoint: this.forkPoint,
288
+ };
289
+ } catch (error) {
290
+ // If cloud branch doesn't exist yet, only local has commits
291
+ return {
292
+ localAhead: this.localSession.commitCount,
293
+ cloudAhead: 0,
294
+ diverged: false,
295
+ forkPoint: this.forkPoint,
296
+ };
297
+ }
298
+ }
299
+
300
+ /**
301
+ * Generate merge plan for combining fork branches.
302
+ *
303
+ * @returns {Promise<Object>} Merge plan
304
+ */
305
+ async generateMergePlan() {
306
+ const conflicts = await this.checkConflicts();
307
+ const divergence = await this.getDivergence();
308
+
309
+ const plan = {
310
+ canAutoMerge: !conflicts.hasConflicts,
311
+ conflicts: conflicts.conflictingFiles,
312
+ divergence,
313
+ recommendedStrategy: null,
314
+ steps: [],
315
+ };
316
+
317
+ if (!conflicts.hasConflicts) {
318
+ // No conflicts - can auto-merge
319
+ if (divergence.localAhead === 0) {
320
+ plan.recommendedStrategy = 'fast-forward';
321
+ plan.steps = [
322
+ `git fetch origin ${this.cloudBranch}`,
323
+ `git merge origin/${this.cloudBranch}`,
324
+ ];
325
+ } else if (divergence.cloudAhead === 0) {
326
+ plan.recommendedStrategy = 'push';
327
+ plan.steps = [
328
+ `git push origin ${this.localBranch}:${this.cloudBranch}`,
329
+ ];
330
+ } else {
331
+ plan.recommendedStrategy = 'merge';
332
+ plan.steps = [
333
+ `git fetch origin ${this.cloudBranch}`,
334
+ `git merge origin/${this.cloudBranch}`,
335
+ 'git push origin HEAD',
336
+ ];
337
+ }
338
+ } else {
339
+ // Has conflicts - manual resolution needed
340
+ plan.recommendedStrategy = 'manual';
341
+ plan.steps = [
342
+ `git fetch origin ${this.cloudBranch}`,
343
+ `git merge origin/${this.cloudBranch}`,
344
+ '# Resolve conflicts manually',
345
+ 'git add <resolved-files>',
346
+ 'git commit -m "Merge cloud session changes"',
347
+ 'git push origin HEAD',
348
+ ];
349
+ }
350
+
351
+ return plan;
352
+ }
353
+
354
+ /**
355
+ * Get summary of fork state.
356
+ *
357
+ * @returns {Object} Fork state summary
358
+ */
359
+ getSummary() {
360
+ return {
361
+ teleportId: this.teleportId,
362
+ forkPoint: this.forkPoint,
363
+ localSession: {
364
+ ...this.localSession,
365
+ branch: this.localBranch,
366
+ },
367
+ cloudSession: {
368
+ ...this.cloudSession,
369
+ branch: this.cloudBranch,
370
+ },
371
+ conflicts: this.conflicts,
372
+ lastConflictCheck: this.lastConflictCheck,
373
+ };
374
+ }
375
+
376
+ /**
377
+ * Check if both sessions are still active.
378
+ *
379
+ * @returns {boolean} True if both sessions are active
380
+ */
381
+ areBothSessionsActive() {
382
+ return (
383
+ this.localSession.state === ForkSessionState.ACTIVE &&
384
+ this.cloudSession.state === ForkSessionState.ACTIVE
385
+ );
386
+ }
387
+
388
+ /**
389
+ * Mark local session as completed.
390
+ *
391
+ * @param {Object} [result] - Completion result
392
+ */
393
+ completeLocalSession(result) {
394
+ this.localSession.state = ForkSessionState.COMPLETED;
395
+ this.localSession.completedAt = new Date().toISOString();
396
+ if (result) {
397
+ this.localSession.result = result;
398
+ }
399
+ }
400
+
401
+ /**
402
+ * Mark cloud session as completed.
403
+ *
404
+ * @param {Object} [result] - Completion result
405
+ */
406
+ completeCloudSession(result) {
407
+ this.cloudSession.state = ForkSessionState.COMPLETED;
408
+ this.cloudSession.completedAt = new Date().toISOString();
409
+ if (result) {
410
+ this.cloudSession.result = result;
411
+ }
412
+ }
413
+ }
414
+
415
+ export default ForkTracker;