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
@@ -1,132 +1,74 @@
1
- import { spawn } from 'child_process';
1
+ /**
2
+ * Daemon Lifecycle Management
3
+ *
4
+ * Thin adapter over @derivativelabs/agent-process.
5
+ * Provides startDaemon/stopDaemon/restartDaemon/getDaemonStatus
6
+ * for the CLI and daemon-commands module.
7
+ *
8
+ * PRD-0025: Replaced custom pid-manager.js with agent-process platform-native
9
+ * service management (launchd/systemd/pm2).
10
+ */
11
+
12
+ import { agentStart, agentStop, agentStatus } from '@derivativelabs/agent-process';
2
13
  import { fileURLToPath } from 'url';
3
14
  import { dirname, join } from 'path';
4
- import {
5
- checkDaemonStatus,
6
- acquirePidLock,
7
- releasePidLock,
8
- isProcessRunning
9
- } from './pid-manager.js';
10
15
 
11
16
  const __filename = fileURLToPath(import.meta.url);
12
17
  const __dirname = dirname(__filename);
13
18
 
14
- // Path to daemon entry point (relative to lifecycle.js location)
19
+ const AGENT_NAME = 'teleportation-daemon';
15
20
  const DAEMON_SCRIPT = join(__dirname, 'teleportation-daemon.js');
16
21
 
17
22
  /**
18
- * Start the daemon process
19
- * @param {Object} options - Start options
20
- * @param {boolean} options.detached - Run daemon as detached process (default: true)
21
- * @param {boolean} options.silent - Suppress output (default: true)
23
+ * Start the daemon process via agent-process
24
+ * @param {Object} options - Start options (kept for backward compat)
25
+ * @param {boolean} options.detached - Ignored (agent-process handles this)
26
+ * @param {boolean} options.silent - Ignored (agent-process handles this)
22
27
  * @returns {Promise<{pid: number, success: boolean}>}
23
28
  */
24
29
  export async function startDaemon(options = {}) {
25
- const { detached = true, silent = true } = options;
26
-
27
- // Check if daemon is already running
28
- const status = await checkDaemonStatus();
29
- if (status.running) {
30
- throw new Error(`Daemon already running with PID ${status.pid}`);
31
- }
32
-
33
- // Clean up stale PID file if exists
34
- if (status.stale) {
35
- await releasePidLock(status.pid);
30
+ // Check if already running
31
+ const current = await getDaemonStatus();
32
+ if (current.running) {
33
+ throw new Error(`Daemon already running with PID ${current.pid}`);
36
34
  }
37
35
 
38
- // Spawn daemon process
39
- const child = spawn(
40
- process.execPath, // Use same Node.js executable
41
- [DAEMON_SCRIPT],
42
- {
43
- detached,
44
- stdio: silent ? 'ignore' : 'inherit',
45
- env: {
46
- ...process.env,
47
- TELEPORTATION_DAEMON: 'true'
48
- }
36
+ const result = await agentStart({
37
+ name: AGENT_NAME,
38
+ script: DAEMON_SCRIPT,
39
+ env: {
40
+ ...process.env,
41
+ TELEPORTATION_DAEMON: 'true',
42
+ DAEMON_IDLE_TIMEOUT_MS: process.env.DAEMON_IDLE_TIMEOUT_MS || '0'
49
43
  }
50
- );
51
-
52
- // Detach from parent if requested
53
- if (detached) {
54
- child.unref();
55
- }
56
-
57
- // Wait a moment to ensure process started (increase wait time for CI)
58
- await new Promise(resolve => setTimeout(resolve, 1000));
59
-
60
- // Verify daemon is running (check multiple times for slow CI)
61
- let newStatus = await checkDaemonStatus();
62
- if (!newStatus.running) {
63
- // Wait a bit more and check again
64
- await new Promise(resolve => setTimeout(resolve, 1000));
65
- newStatus = await checkDaemonStatus();
66
- if (!newStatus.running) {
67
- throw new Error('Daemon failed to start');
68
- }
69
- }
44
+ });
70
45
 
71
46
  return {
72
- pid: child.pid,
47
+ pid: result.pid || null,
73
48
  success: true
74
49
  };
75
50
  }
76
51
 
77
52
  /**
78
- * Stop the daemon process
53
+ * Stop the daemon process via agent-process
79
54
  * @param {Object} options - Stop options
80
55
  * @param {number} options.timeout - Timeout in ms for graceful shutdown (default: 5000)
81
56
  * @param {boolean} options.force - Force kill if graceful shutdown fails (default: true)
82
57
  * @returns {Promise<{success: boolean, forced: boolean}>}
83
58
  */
84
59
  export async function stopDaemon(options = {}) {
85
- const { timeout = 5000, force = true } = options;
86
-
87
- // Check daemon status
88
- const status = await checkDaemonStatus();
89
- if (!status.running) {
60
+ const current = await getDaemonStatus();
61
+ if (!current.running) {
90
62
  return { success: true, forced: false };
91
63
  }
92
64
 
93
- const { pid } = status;
94
-
95
65
  try {
96
- // Send SIGTERM for graceful shutdown
97
- process.kill(pid, 'SIGTERM');
98
-
99
- // Wait for process to exit
100
- const startTime = Date.now();
101
- while (Date.now() - startTime < timeout) {
102
- if (!isProcessRunning(pid)) {
103
- await releasePidLock(pid);
104
- return { success: true, forced: false };
105
- }
106
- await new Promise(resolve => setTimeout(resolve, 100));
107
- }
108
-
109
- // Timeout reached, force kill if requested
110
- if (force) {
111
- process.kill(pid, 'SIGKILL');
112
-
113
- // Wait a moment for kill to take effect
114
- await new Promise(resolve => setTimeout(resolve, 200));
115
-
116
- if (!isProcessRunning(pid)) {
117
- await releasePidLock(pid);
118
- return { success: true, forced: true };
119
- }
120
-
121
- throw new Error('Failed to kill daemon process');
122
- }
123
-
124
- return { success: false, forced: false };
66
+ await agentStop(AGENT_NAME);
67
+ return { success: true, forced: false };
125
68
  } catch (err) {
126
- // Process might have already exited
127
- if (err.code === 'ESRCH') {
128
- await releasePidLock(pid);
129
- return { success: true, forced: false };
69
+ if (options.force !== false) {
70
+ // agent-process handles force kill internally
71
+ return { success: true, forced: true };
130
72
  }
131
73
  throw err;
132
74
  }
@@ -142,16 +84,13 @@ export async function stopDaemon(options = {}) {
142
84
  export async function restartDaemon(options = {}) {
143
85
  const { stopTimeout = 5000, force = true } = options;
144
86
 
145
- // Check if daemon is running
146
- const status = await checkDaemonStatus();
147
- const wasRunning = status.running;
87
+ const current = await getDaemonStatus();
88
+ const wasRunning = current.running;
148
89
 
149
- // Stop daemon if running
150
90
  if (wasRunning) {
151
91
  await stopDaemon({ timeout: stopTimeout, force });
152
92
  }
153
93
 
154
- // Start daemon
155
94
  const result = await startDaemon();
156
95
 
157
96
  return {
@@ -192,7 +131,7 @@ async function updateSessionDaemonState(sessionId, updates) {
192
131
  }
193
132
 
194
133
  export async function startDaemonIfNeeded(sessionId, reason = 'manual') {
195
- const status = await checkDaemonStatus();
134
+ const status = await getDaemonStatus();
196
135
 
197
136
  if (!status.running) {
198
137
  await startDaemon();
@@ -207,12 +146,11 @@ export async function startDaemonIfNeeded(sessionId, reason = 'manual') {
207
146
  }
208
147
 
209
148
  export async function stopDaemonIfNeeded(sessionId, reason = 'manual_stop') {
210
- const status = await checkDaemonStatus();
149
+ const status = await getDaemonStatus();
211
150
  if (!status.running) {
212
151
  return { stopped: false, reason: 'not_running' };
213
152
  }
214
153
 
215
- // If relay is configured, check if other sessions still have daemon running
216
154
  const { url, key } = getRelayConfig();
217
155
  if (url && key) {
218
156
  try {
@@ -232,20 +170,17 @@ export async function stopDaemonIfNeeded(sessionId, reason = 'manual_stop') {
232
170
  }
233
171
  }
234
172
  } catch (err) {
235
- // Fail open: if we can't query sessions, we still attempt to stop daemon
236
173
  if (process.env.DEBUG) {
237
174
  console.error('[lifecycle] Failed to query sessions before stop:', err.message);
238
175
  }
239
176
  }
240
177
  }
241
178
 
242
- // Update daemon_state before stopping
243
179
  if (sessionId) {
244
180
  await updateSessionDaemonState(sessionId, {
245
181
  status: 'stopped',
246
182
  started_reason: null,
247
183
  is_away: false
248
- // stopped_reason could be added later if DaemonState schema is extended
249
184
  });
250
185
  }
251
186
 
@@ -254,83 +189,33 @@ export async function stopDaemonIfNeeded(sessionId, reason = 'manual_stop') {
254
189
  }
255
190
 
256
191
  /**
257
- * Get daemon status
192
+ * Get daemon status via agent-process
258
193
  * @returns {Promise<{running: boolean, pid: number|null, uptime: number|null}>}
259
194
  */
260
195
  export async function getDaemonStatus() {
261
- const status = await checkDaemonStatus();
262
-
263
- // TODO: Add uptime calculation once daemon stores start time
264
- // For now, we can only report running status and PID
265
- return {
266
- running: status.running,
267
- pid: status.pid,
268
- uptime: null // Will be implemented when daemon stores start timestamp
269
- };
196
+ try {
197
+ const status = await agentStatus(AGENT_NAME);
198
+ return {
199
+ running: status.state === 'online' || status.running || false,
200
+ pid: status.pid || null,
201
+ uptime: status.uptime || null
202
+ };
203
+ } catch (err) {
204
+ return { running: false, pid: null, uptime: null };
205
+ }
270
206
  }
271
207
 
272
208
  /**
273
- * Setup signal handlers for graceful daemon shutdown
274
- * @param {Function} cleanupCallback - Async function to call before exit
209
+ * Alias for getDaemonStatus (backward compat with pid-manager API)
275
210
  */
276
- export function setupSignalHandlers(cleanupCallback) {
277
- const handleSignal = async (signal) => {
278
- console.log(`Received ${signal}, shutting down gracefully...`);
279
-
280
- try {
281
- // Run cleanup callback
282
- if (cleanupCallback) {
283
- await cleanupCallback();
284
- }
285
-
286
- // Release PID lock
287
- await releasePidLock(process.pid);
288
-
289
- process.exit(0);
290
- } catch (err) {
291
- console.error('Error during cleanup:', err);
292
- process.exit(1);
293
- }
294
- };
295
-
296
- // Handle termination signals
297
- process.on('SIGTERM', () => handleSignal('SIGTERM'));
298
- process.on('SIGINT', () => handleSignal('SIGINT'));
299
-
300
- // Handle uncaught errors
301
- process.on('uncaughtException', async (err) => {
302
- console.error('Uncaught exception:', err);
303
- try {
304
- if (cleanupCallback) {
305
- await cleanupCallback();
306
- }
307
- await releasePidLock(process.pid);
308
- } catch (cleanupErr) {
309
- console.error('Error during cleanup:', cleanupErr);
310
- }
311
- process.exit(1);
312
- });
313
-
314
- process.on('unhandledRejection', async (reason) => {
315
- console.error('Unhandled rejection:', reason);
316
- try {
317
- if (cleanupCallback) {
318
- await cleanupCallback();
319
- }
320
- await releasePidLock(process.pid);
321
- } catch (cleanupErr) {
322
- console.error('Error during cleanup:', cleanupErr);
323
- }
324
- process.exit(1);
325
- });
326
- }
211
+ export const checkDaemonStatus = getDaemonStatus;
327
212
 
328
213
  export default {
329
214
  startDaemon,
330
215
  stopDaemon,
331
216
  restartDaemon,
332
217
  getDaemonStatus,
333
- setupSignalHandlers,
218
+ checkDaemonStatus,
334
219
  startDaemonIfNeeded,
335
220
  stopDaemonIfNeeded
336
221
  };
@@ -0,0 +1,3 @@
1
+ export { SessionService } from './session-service.js';
2
+ export { QueueService } from './queue-service.js';
3
+ export { PollingService } from './polling-service.js';
@@ -0,0 +1,173 @@
1
+ /**
2
+ * Polling Service
3
+ *
4
+ * Periodically polls the relay API for updates.
5
+ */
6
+
7
+ import { executeTaskTurn } from '../task-executor-v2.js';
8
+ import { ingestTranscriptToTimeline } from '../transcript-ingestion.js';
9
+ import { debugLog } from '../utils.js';
10
+
11
+ export class PollingService {
12
+ constructor(state, config) {
13
+ this.name = 'polling';
14
+ this.state = state;
15
+ this.config = config;
16
+ }
17
+
18
+ async start(ctx) {
19
+ this.ctx = ctx;
20
+ console.log('[PollingService] Started');
21
+ }
22
+
23
+ async stop() {
24
+ console.log('[PollingService] Stopped');
25
+ }
26
+
27
+ getStats() {
28
+ return {
29
+ ingestionsInProgress: this.state.ingestionInProgress.size
30
+ };
31
+ }
32
+
33
+ /**
34
+ * Main polling loop iteration
35
+ */
36
+ async poll() {
37
+ if (this.state.isShuttingDown) return;
38
+
39
+ for (const [session_id, sessionData] of this.state.sessions) {
40
+ this.state.sessionActivity.set(session_id, Date.now());
41
+
42
+ // 1) Poll Approvals
43
+ await this.pollApprovals(session_id);
44
+
45
+ // 2) Poll Inbox Messages
46
+ await this.pollInbox(session_id);
47
+
48
+ // 3) Poll Tasks
49
+ await this.pollTasks(session_id);
50
+
51
+ // 4) Transcript Ingestion
52
+ await this.runTranscriptIngestion(session_id, sessionData);
53
+ }
54
+ }
55
+
56
+ async pollApprovals(session_id) {
57
+ if (!this.config.relayApiUrl || !this.config.relayApiKey) return;
58
+ try {
59
+ const response = await fetch(
60
+ `${this.config.relayApiUrl}/api/approvals?status=allowed&session_id=${session_id}`,
61
+ { headers: { 'Authorization': `Bearer ${this.config.relayApiKey}` } }
62
+ );
63
+
64
+ if (response.ok) {
65
+ const approvals = await response.json();
66
+ for (const approval of approvals) {
67
+ if (this.state.approvalQueue.find(a => a.approval_id === approval.id)) continue;
68
+ if (this.state.executions.has(approval.id)) continue;
69
+ if (approval.acknowledgedAt || approval.decision_location) continue;
70
+
71
+ this.state.approvalQueue.push({
72
+ approval_id: approval.id,
73
+ session_id: approval.session_id,
74
+ tool_name: approval.tool_name,
75
+ tool_input: approval.tool_input,
76
+ queued_at: Date.now(),
77
+ tool_use_id: approval.tool_use_id || null,
78
+ conversation_context: approval.conversation_context || null
79
+ });
80
+
81
+ console.log(`[PollingService] Approval discovered: ${approval.id} (${approval.tool_name})`);
82
+ }
83
+ }
84
+ } catch (err) {
85
+ console.error(`[PollingService] Approval polling error: ${err.message}`);
86
+ }
87
+ }
88
+
89
+ async pollInbox(session_id) {
90
+ if (!this.config.relayApiUrl || !this.config.relayApiKey) return;
91
+ try {
92
+ const response = await fetch(
93
+ `${this.config.relayApiUrl}/api/messages/pending?session_id=${encodeURIComponent(session_id)}&agent_id=daemon`,
94
+ { headers: { 'Authorization': `Bearer ${this.config.relayApiKey}` } }
95
+ );
96
+
97
+ if (response.ok) {
98
+ const inboxMessage = await response.json();
99
+ if (inboxMessage && inboxMessage.id && inboxMessage.text) {
100
+ // Note: handleInboxMessage logic should be here or in another service
101
+ // For now, we'll just log it. In a full refactor, we'd move the handler too.
102
+ console.log(`[PollingService] Inbox message discovered for ${session_id}: ${inboxMessage.id}`);
103
+ }
104
+ }
105
+ } catch (err) {
106
+ // 404 is expected if no pending messages
107
+ if (err.status !== 404) {
108
+ console.error(`[PollingService] Inbox polling error: ${err.message}`);
109
+ }
110
+ }
111
+ }
112
+
113
+ async pollTasks(session_id) {
114
+ if (!this.config.relayApiUrl || !this.config.relayApiKey) return;
115
+ try {
116
+ const response = await fetch(
117
+ `${this.config.relayApiUrl}/api/sessions/${encodeURIComponent(session_id)}/tasks`,
118
+ { headers: { 'Authorization': `Bearer ${this.config.relayApiKey}` } }
119
+ );
120
+
121
+ if (response.ok) {
122
+ const tasks = await response.json();
123
+ for (const task of tasks) {
124
+ if (task.status === 'stopped' || task.status === 'completed') continue;
125
+
126
+ try {
127
+ const result = await executeTaskTurn({
128
+ task_id: task.id,
129
+ session_id: task.session_id,
130
+ config: {
131
+ relayApiUrl: this.config.relayApiUrl,
132
+ apiKey: this.config.relayApiKey
133
+ }
134
+ });
135
+
136
+ if (result.executed) {
137
+ console.log(`[PollingService] Task ${task.id.slice(0, 8)}... executed turn`);
138
+ }
139
+ } catch (err) {
140
+ console.error(`[PollingService] Task execution error: ${err.message}`);
141
+ }
142
+ }
143
+ }
144
+ } catch (err) {
145
+ console.error(`[PollingService] Task polling error: ${err.message}`);
146
+ }
147
+ }
148
+
149
+ async runTranscriptIngestion(session_id, sessionData) {
150
+ if (this.state.ingestionInProgress.has(session_id)) return;
151
+
152
+ const claude_session_id = sessionData.claude_session_id || session_id;
153
+ const cwd = sessionData.cwd || process.cwd();
154
+
155
+ const promise = ingestTranscriptToTimeline({
156
+ claude_session_id,
157
+ parent_session_id: session_id,
158
+ task_id: null,
159
+ cwd,
160
+ config: {
161
+ relayApiUrl: this.config.relayApiUrl,
162
+ apiKey: this.config.relayApiKey
163
+ },
164
+ maxEvents: 100
165
+ }).catch(err => {
166
+ console.error(`[PollingService] Ingestion error: ${err.message}`);
167
+ }).finally(() => {
168
+ this.state.ingestionInProgress.delete(session_id);
169
+ });
170
+
171
+ this.state.ingestionInProgress.set(session_id, promise);
172
+ }
173
+ }