teleportation-cli 1.1.4 → 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 (48) hide show
  1. package/.claude/hooks/config-loader.mjs +88 -34
  2. package/.claude/hooks/permission_request.mjs +392 -82
  3. package/.claude/hooks/post_tool_use.mjs +90 -0
  4. package/.claude/hooks/pre_tool_use.mjs +247 -305
  5. package/.claude/hooks/session-register.mjs +94 -105
  6. package/.claude/hooks/session_end.mjs +41 -42
  7. package/.claude/hooks/session_start.mjs +45 -60
  8. package/.claude/hooks/stop.mjs +752 -99
  9. package/.claude/hooks/user_prompt_submit.mjs +26 -3
  10. package/README.md +7 -0
  11. package/lib/auth/api-key.js +12 -0
  12. package/lib/auth/token-refresh.js +286 -0
  13. package/lib/cli/daemon-commands.js +1 -1
  14. package/lib/cli/teleport-commands.js +469 -0
  15. package/lib/daemon/daemon-v2.js +104 -0
  16. package/lib/daemon/lifecycle.js +56 -171
  17. package/lib/daemon/response-classifier.js +15 -1
  18. package/lib/daemon/services/index.js +3 -0
  19. package/lib/daemon/services/polling-service.js +173 -0
  20. package/lib/daemon/services/queue-service.js +318 -0
  21. package/lib/daemon/services/session-service.js +115 -0
  22. package/lib/daemon/state.js +35 -0
  23. package/lib/daemon/task-executor-v2.js +413 -0
  24. package/lib/daemon/task-executor.js +1235 -0
  25. package/lib/daemon/teleportation-daemon.js +770 -25
  26. package/lib/daemon/timeline-analyzer.js +215 -0
  27. package/lib/daemon/transcript-ingestion.js +696 -0
  28. package/lib/daemon/utils.js +91 -0
  29. package/lib/install/installer.js +184 -20
  30. package/lib/install/uhr-installer.js +136 -0
  31. package/lib/remote/providers/base-provider.js +46 -0
  32. package/lib/remote/providers/daytona-provider.js +58 -0
  33. package/lib/remote/providers/provider-factory.js +90 -19
  34. package/lib/remote/providers/sprites-provider.js +711 -0
  35. package/lib/teleport/exporters/claude-exporter.js +302 -0
  36. package/lib/teleport/exporters/gemini-exporter.js +307 -0
  37. package/lib/teleport/exporters/index.js +93 -0
  38. package/lib/teleport/exporters/interface.js +153 -0
  39. package/lib/teleport/fork-tracker.js +415 -0
  40. package/lib/teleport/git-committer.js +337 -0
  41. package/lib/teleport/index.js +48 -0
  42. package/lib/teleport/manager.js +620 -0
  43. package/lib/teleport/session-capture.js +282 -0
  44. package/package.json +11 -5
  45. package/teleportation-cli.cjs +632 -451
  46. package/.claude/hooks/heartbeat.mjs +0 -396
  47. package/lib/daemon/agentic-executor.js +0 -803
  48. package/lib/daemon/pid-manager.js +0 -160
@@ -1,396 +0,0 @@
1
- #!/usr/bin/env bun
2
- /**
3
- * Heartbeat Background Process
4
- * Runs continuously while Claude Code session is active
5
- * Sends periodic heartbeats to relay API to keep session alive
6
- *
7
- * This is NOT a hook - it's a background process spawned by session_start.mjs
8
- * It runs detached and continues until killed by session_end.mjs
9
- */
10
-
11
- import { env, exit, pid as processPid } from 'node:process';
12
- import { writeFile, unlink } from 'fs/promises';
13
- import { tmpdir, homedir } from 'os';
14
- import { join, dirname } from 'path';
15
- import { fileURLToPath, pathToFileURL } from 'url';
16
-
17
- // Read configuration from environment variables (secure - not visible in process list)
18
- const SESSION_ID = process.argv[2] || env.SESSION_ID;
19
- const RELAY_API_URL = env.RELAY_API_URL;
20
- const RELAY_API_KEY = env.RELAY_API_KEY;
21
- const HEARTBEAT_INTERVAL = Math.max(100, Math.min(600000,
22
- parseInt(env.HEARTBEAT_INTERVAL || '120000', 10)
23
- )); // Default 2 minutes, min 100ms (for testing), max 10min
24
- const START_DELAY = Math.max(100, Math.min(60000,
25
- parseInt(env.START_DELAY || '5000', 10)
26
- )); // Default 5 seconds, min 100ms (for testing), max 1min
27
- const MAX_FAILURES = Math.max(1, Math.min(10,
28
- parseInt(env.MAX_FAILURES || '3', 10)
29
- )); // Default 3, min 1, max 10
30
- const FETCH_TIMEOUT = Math.max(100, Math.min(30000,
31
- parseInt(env.FETCH_TIMEOUT || '5000', 10)
32
- )); // Default 5s, min 100ms (for testing), max 30s
33
- const DEBUG = env.TELEPORTATION_DEBUG === 'true';
34
-
35
- // Get __dirname equivalent for ES modules
36
- const __filename = fileURLToPath(import.meta.url);
37
- const __dirname = dirname(__filename);
38
-
39
- if (!SESSION_ID || !RELAY_API_URL || !RELAY_API_KEY) {
40
- console.error('[Heartbeat] Missing required environment variables: SESSION_ID, RELAY_API_URL, RELAY_API_KEY');
41
- exit(1);
42
- }
43
-
44
- const PID_FILE = join(tmpdir(), `teleportation-heartbeat-${SESSION_ID}.pid`);
45
- let intervalHandle = null;
46
- let heartbeatCount = 0;
47
- let failureCount = 0;
48
- let lastDaemonCheck = 0;
49
- // Module cache for lifecycle and pid-manager (imported once, reused)
50
- let lifecycleModule = null;
51
- let pidManagerModule = null;
52
- // Flag to prevent concurrent daemon start attempts
53
- let daemonStartInProgress = false;
54
- // Make interval configurable via environment variable (default 60 seconds)
55
- const DAEMON_CHECK_INTERVAL = Math.max(10000, Math.min(300000,
56
- parseInt(env.DAEMON_CHECK_INTERVAL || '60000', 10)
57
- )); // Default 60 seconds, min 10s, max 5min
58
-
59
- /**
60
- * Update daemon state on relay
61
- * @param {'running' | 'stopped'} status - Daemon status
62
- * @param {string} reason - Reason for state change
63
- */
64
- async function updateRelayDaemonState(status, reason) {
65
- try {
66
- const controller = new AbortController();
67
- const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT);
68
-
69
- const response = await fetch(`${RELAY_API_URL}/api/sessions/${SESSION_ID}/daemon-state`, {
70
- method: 'PATCH',
71
- headers: {
72
- 'Content-Type': 'application/json',
73
- 'Authorization': `Bearer ${RELAY_API_KEY}`
74
- },
75
- body: JSON.stringify({
76
- status,
77
- started_at: status === 'running' ? Date.now() : null,
78
- started_reason: status === 'running' ? reason : null
79
- }),
80
- signal: controller.signal
81
- });
82
-
83
- clearTimeout(timeoutId);
84
-
85
- if (!response.ok && DEBUG) {
86
- console.log(`[Heartbeat] Failed to update relay daemon state: ${response.status}`);
87
- }
88
- } catch (error) {
89
- if (DEBUG) {
90
- console.log(`[Heartbeat] Error updating relay daemon state: ${error.message}`);
91
- }
92
- throw error;
93
- }
94
- }
95
-
96
- /**
97
- * Check if daemon is running and start it if not
98
- */
99
- async function ensureDaemonRunning() {
100
- // Early return with throttle check BEFORE any async operations
101
- // This prevents multiple concurrent executions of the expensive module import logic
102
- const now = Date.now();
103
- if (now - lastDaemonCheck < DAEMON_CHECK_INTERVAL) {
104
- return;
105
- }
106
- // Set timestamp immediately after check to prevent race conditions
107
- lastDaemonCheck = now;
108
-
109
- try {
110
- // Try to import lifecycle module from installed location
111
- const possibleLocations = [
112
- // npm global install location
113
- '/usr/local/lib/node_modules/teleportation-cli/lib/daemon/lifecycle.js',
114
- // User's npm global install (common on macOS with nvm/fnm)
115
- join(homedir(), '.local', 'lib', 'node_modules', 'teleportation-cli', 'lib', 'daemon', 'lifecycle.js'),
116
- // Legacy location
117
- join(homedir(), '.teleportation', 'lib', 'daemon', 'lifecycle.js'),
118
- // Relative to hooks dir (development)
119
- join(__dirname, '..', '..', 'lib', 'daemon', 'lifecycle.js'),
120
- // Environment override
121
- env.TELEPORTATION_LIFECYCLE_PATH
122
- ].filter(Boolean);
123
-
124
- const { access } = await import('fs/promises');
125
- let lifecyclePath = null;
126
- for (const location of possibleLocations) {
127
- try {
128
- await access(location);
129
- lifecyclePath = location;
130
- break;
131
- } catch {
132
- // Try next location
133
- }
134
- }
135
-
136
- if (!lifecyclePath) {
137
- if (DEBUG) {
138
- console.log('[Heartbeat] Lifecycle module not found, skipping daemon check');
139
- }
140
- return;
141
- }
142
-
143
- // Cache module imports to avoid re-importing on every check
144
- // Use pathToFileURL for proper Windows path handling
145
- const pidManagerPath = join(dirname(lifecyclePath), 'pid-manager.js');
146
-
147
- if (!lifecycleModule || !pidManagerModule) {
148
- // Only import once, then cache
149
- // Use pathToFileURL for cross-platform compatibility (especially Windows)
150
- lifecycleModule = await import(pathToFileURL(lifecyclePath).href);
151
- pidManagerModule = await import(pathToFileURL(pidManagerPath).href);
152
- }
153
-
154
- const lifecycle = lifecycleModule;
155
- const { checkDaemonStatus } = pidManagerModule;
156
-
157
- // Check daemon status
158
- const status = await checkDaemonStatus();
159
- if (!status.running && !daemonStartInProgress) {
160
- // Set flag to prevent concurrent start attempts
161
- daemonStartInProgress = true;
162
-
163
- if (DEBUG) {
164
- console.log(`[Heartbeat] Daemon not running, attempting to start...`);
165
- }
166
-
167
- try {
168
- // Use startDaemonIfNeeded to both start the daemon AND update the relay
169
- // This ensures the mobile UI knows the daemon is running
170
- await lifecycle.startDaemonIfNeeded(SESSION_ID, 'heartbeat');
171
- if (DEBUG) {
172
- console.log(`[Heartbeat] Daemon started and relay updated successfully`);
173
- }
174
- } catch (error) {
175
- // Don't fail heartbeat if daemon start fails - it might already be starting
176
- if (DEBUG) {
177
- console.log(`[Heartbeat] Daemon start attempt: ${error.message}`);
178
- }
179
- } finally {
180
- // Always reset flag, even if start fails
181
- daemonStartInProgress = false;
182
- }
183
- } else if (status.running) {
184
- // Daemon is running - ensure relay knows about it
185
- // This handles the case where daemon was started manually or by another session
186
- try {
187
- await updateRelayDaemonState('running', 'heartbeat');
188
- if (DEBUG) {
189
- console.log(`[Heartbeat] Synced daemon running state to relay`);
190
- }
191
- } catch (error) {
192
- // Ignore errors - just a sync, not critical
193
- if (DEBUG) {
194
- console.log(`[Heartbeat] Failed to sync daemon state: ${error.message}`);
195
- }
196
- }
197
- }
198
- } catch (error) {
199
- // Silently fail - daemon check shouldn't break heartbeat
200
- if (DEBUG) {
201
- console.log(`[Heartbeat] Error checking daemon: ${error.message}`);
202
- }
203
- }
204
- }
205
-
206
- /**
207
- * Check for pending approvals and handle them
208
- */
209
- async function checkAndHandlePendingApprovals() {
210
- try {
211
- // Fetch pending and allowed approvals for this session
212
- const controller = new AbortController();
213
- const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT);
214
-
215
- const [pendingResponse, allowedResponse] = await Promise.all([
216
- fetch(`${RELAY_API_URL}/api/approvals?status=pending&session_id=${SESSION_ID}`, {
217
- headers: {
218
- 'Authorization': `Bearer ${RELAY_API_KEY}`
219
- },
220
- signal: controller.signal
221
- }),
222
- fetch(`${RELAY_API_URL}/api/approvals?status=allowed&session_id=${SESSION_ID}`, {
223
- headers: {
224
- 'Authorization': `Bearer ${RELAY_API_KEY}`
225
- },
226
- signal: controller.signal
227
- })
228
- ]);
229
-
230
- clearTimeout(timeoutId);
231
-
232
- if (!pendingResponse.ok && !allowedResponse.ok) {
233
- return; // Skip if API calls fail
234
- }
235
-
236
- const pending = pendingResponse.ok ? await pendingResponse.json() : [];
237
- const allowed = allowedResponse.ok ? await allowedResponse.json() : [];
238
-
239
- // Find allowed approvals that haven't been acknowledged yet
240
- const unacknowledged = allowed.filter(a => !a.acknowledgedAt);
241
-
242
- if (unacknowledged.length > 0 && DEBUG) {
243
- console.log(`[Heartbeat] Found ${unacknowledged.length} approved but unacknowledged approval(s)`);
244
- }
245
-
246
- // Acknowledge approved approvals (fire-and-forget, don't block heartbeat)
247
- for (const approval of unacknowledged) {
248
- try {
249
- fetch(`${RELAY_API_URL}/api/approvals/${approval.id}/ack`, {
250
- method: 'POST',
251
- headers: {
252
- 'Content-Type': 'application/json',
253
- 'Authorization': `Bearer ${RELAY_API_KEY}`
254
- },
255
- body: JSON.stringify({ processed: true })
256
- }).catch(() => {}); // Ignore errors - acknowledgment is optional
257
- } catch (e) {
258
- // Ignore errors
259
- }
260
- }
261
-
262
- // Log if there are pending approvals waiting for user decision
263
- if (pending.length > 0 && DEBUG) {
264
- console.log(`[Heartbeat] Session has ${pending.length} pending approval(s) waiting for user decision`);
265
- }
266
- } catch (error) {
267
- // Silently fail - approval checking shouldn't break heartbeat
268
- if (DEBUG) {
269
- console.log(`[Heartbeat] Error checking approvals: ${error.message}`);
270
- }
271
- }
272
- }
273
-
274
- /**
275
- * Send heartbeat to relay API
276
- */
277
- async function sendHeartbeat() {
278
- // Increment count before sending (so first heartbeat is #1, not #0)
279
- heartbeatCount++;
280
-
281
- try {
282
- // Add timeout to prevent hanging on unreachable servers
283
- const controller = new AbortController();
284
- const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT);
285
-
286
- const response = await fetch(`${RELAY_API_URL}/api/sessions/${SESSION_ID}/heartbeat`, {
287
- method: 'POST',
288
- headers: {
289
- 'Content-Type': 'application/json',
290
- 'Authorization': `Bearer ${RELAY_API_KEY}`
291
- },
292
- body: JSON.stringify({
293
- timestamp: new Date().toISOString(),
294
- pid: processPid,
295
- count: heartbeatCount
296
- }),
297
- signal: controller.signal
298
- });
299
-
300
- clearTimeout(timeoutId); // Clear timeout if request succeeds
301
-
302
- if (response.ok) {
303
- failureCount = 0; // Reset failure count on success
304
-
305
- // Check daemon status and start if needed (asynchronously, don't block heartbeat)
306
- ensureDaemonRunning().catch(() => {});
307
-
308
- // Check for pending approvals after successful heartbeat
309
- // Do this asynchronously so it doesn't block the heartbeat
310
- checkAndHandlePendingApprovals().catch(() => {});
311
-
312
- if (DEBUG) {
313
- console.log(`[Heartbeat] Sent #${heartbeatCount} for session ${SESSION_ID}`);
314
- }
315
- } else {
316
- heartbeatCount--; // Rollback count on failure
317
- failureCount++;
318
- console.error(`[Heartbeat] Failed (${response.status}): ${await response.text()}`);
319
-
320
- if (failureCount >= MAX_FAILURES) {
321
- console.error(`[Heartbeat] Max failures reached (${MAX_FAILURES}), stopping`);
322
- await cleanup();
323
- }
324
- }
325
- } catch (error) {
326
- heartbeatCount--; // Rollback count on error
327
- failureCount++;
328
- console.error(`[Heartbeat] Error sending heartbeat:`, error.message);
329
-
330
- if (failureCount >= MAX_FAILURES) {
331
- console.error(`[Heartbeat] Max failures reached (${MAX_FAILURES}), stopping`);
332
- await cleanup();
333
- }
334
- }
335
- }
336
-
337
- /**
338
- * Cleanup and exit
339
- */
340
- async function cleanup() {
341
- console.log(`[Heartbeat] Cleaning up session ${SESSION_ID}`);
342
-
343
- if (intervalHandle) {
344
- clearInterval(intervalHandle);
345
- intervalHandle = null;
346
- }
347
-
348
- // Remove PID file
349
- try {
350
- await unlink(PID_FILE);
351
- } catch (error) {
352
- // Ignore errors - file might not exist
353
- }
354
-
355
- exit(0);
356
- }
357
-
358
- /**
359
- * Start heartbeat loop
360
- */
361
- async function start() {
362
- try {
363
- // Write PID file with session_id for validation (prevents killing wrong process)
364
- await writeFile(PID_FILE, JSON.stringify({
365
- pid: processPid,
366
- session_id: SESSION_ID,
367
- started_at: Date.now()
368
- }), { mode: 0o600 });
369
-
370
- if (DEBUG) {
371
- console.log(`[Heartbeat] Started for session ${SESSION_ID} (PID: ${processPid})`);
372
- console.log(`[Heartbeat] Interval: ${HEARTBEAT_INTERVAL}ms, Start delay: ${START_DELAY}ms, Max failures: ${MAX_FAILURES}, Fetch timeout: ${FETCH_TIMEOUT}ms`);
373
- }
374
-
375
- // Wait for initial delay before first heartbeat
376
- setTimeout(() => {
377
- // Send first heartbeat immediately after delay
378
- sendHeartbeat();
379
-
380
- // Then set up interval for subsequent heartbeats
381
- intervalHandle = setInterval(sendHeartbeat, HEARTBEAT_INTERVAL);
382
- }, START_DELAY);
383
-
384
- // Handle graceful shutdown
385
- process.on('SIGTERM', cleanup);
386
- process.on('SIGINT', cleanup);
387
- process.on('SIGHUP', cleanup);
388
-
389
- } catch (error) {
390
- console.error(`[Heartbeat] Failed to start:`, error.message);
391
- exit(1);
392
- }
393
- }
394
-
395
- // Start the heartbeat process
396
- start();