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,318 @@
1
+ /**
2
+ * Queue Service
3
+ *
4
+ * Manages approval queue and execution of approved tools.
5
+ */
6
+
7
+ import { spawn } from 'child_process';
8
+ import { truncateOutput, validateApprovalId, validateSessionId, validateToolName, buildToolPrompt } from '../utils.js';
9
+
10
+ export class QueueService {
11
+ constructor(state, config) {
12
+ this.name = 'queue';
13
+ this.state = state;
14
+ this.config = config;
15
+ this.maxQueueSize = config.maxQueueSize || 1000;
16
+ this.maxExecutions = config.maxExecutions || 1000;
17
+ this.childTimeoutMs = config.childTimeoutMs || 600000;
18
+ }
19
+
20
+ async start(ctx) {
21
+ this.ctx = ctx;
22
+ const { server } = ctx;
23
+
24
+ // Register routes
25
+ server.addRoute('POST', '/approvals/handoff', (req) => this.handleHandoff(req));
26
+ server.addRoute('GET', '/executions/', (req) => this.handleGetExecution(req));
27
+
28
+ console.log('[QueueService] Started');
29
+ }
30
+
31
+ async stop() {
32
+ console.log('[QueueService] Stopped');
33
+ }
34
+
35
+ getStats() {
36
+ return {
37
+ queueSize: this.state.approvalQueue.length,
38
+ executionsCount: this.state.executions.size
39
+ };
40
+ }
41
+
42
+ async handleHandoff(req) {
43
+ try {
44
+ const body = await req.json();
45
+ const { approval_id, session_id, tool_name, tool_input } = body;
46
+
47
+ validateApprovalId(approval_id);
48
+ validateSessionId(session_id);
49
+ validateToolName(tool_name);
50
+
51
+ if (this.state.approvalQueue.length >= this.maxQueueSize) {
52
+ return Response.json({
53
+ error: 'Approval queue full',
54
+ queue_size: this.state.approvalQueue.length
55
+ }, { status: 503 });
56
+ }
57
+
58
+ if (!this.state.approvalQueue.find(a => a.approval_id === approval_id)) {
59
+ this.state.approvalQueue.push({
60
+ approval_id,
61
+ session_id,
62
+ tool_name,
63
+ tool_input,
64
+ queued_at: Date.now()
65
+ });
66
+ console.log(`[QueueService] Approval queued: ${approval_id} (${tool_name})`);
67
+ }
68
+
69
+ return Response.json({ ok: true, queued: true });
70
+ } catch (error) {
71
+ return Response.json({ error: error.message }, { status: 400 });
72
+ }
73
+ }
74
+
75
+ async handleGetExecution(req) {
76
+ const url = new URL(req.url);
77
+ const approval_id = url.pathname.split('/executions/')[1];
78
+
79
+ if (!approval_id) {
80
+ return Response.json({ error: 'approval_id is required' }, { status: 400 });
81
+ }
82
+
83
+ const execution = this.state.executions.get(approval_id);
84
+ if (!execution) {
85
+ return Response.json({ error: 'not_found' }, { status: 404 });
86
+ }
87
+
88
+ return Response.json(execution);
89
+ }
90
+
91
+ /**
92
+ * Process one item from the queue
93
+ */
94
+ async processNext() {
95
+ if (this.state.approvalQueue.length === 0 || this.state.isShuttingDown) return;
96
+
97
+ const approval = this.state.approvalQueue.shift();
98
+ const { approval_id, session_id, tool_name, tool_input } = approval;
99
+
100
+ if (this.state.executions.has(approval_id)) {
101
+ if (this.state.executions.get(approval_id).status === 'executing') {
102
+ return;
103
+ }
104
+ }
105
+
106
+ // LRU cleanup
107
+ if (this.state.executions.size >= this.maxExecutions) {
108
+ let oldestKey = null;
109
+ let oldestTime = Infinity;
110
+ for (const [id, exec] of this.state.executions) {
111
+ if (exec.completed_at && exec.completed_at < oldestTime) {
112
+ oldestTime = exec.completed_at;
113
+ oldestKey = id;
114
+ }
115
+ }
116
+ if (oldestKey) this.state.executions.delete(oldestKey);
117
+ }
118
+
119
+ this.state.executions.set(approval_id, {
120
+ approval_id,
121
+ status: 'executing',
122
+ started_at: Date.now(),
123
+ completed_at: null,
124
+ exit_code: null,
125
+ stdout: '',
126
+ stderr: '',
127
+ error: null,
128
+ child_process: null
129
+ });
130
+
131
+ try {
132
+ // Acknowledge
133
+ await this.ackApproval(approval_id);
134
+
135
+ const prompt = buildToolPrompt(tool_name, tool_input);
136
+ const result = await this.spawnClaudeProcess(session_id, prompt, {
137
+ onSpawn: (child) => {
138
+ const exec = this.state.executions.get(approval_id);
139
+ if (exec) exec.child_process = child;
140
+ }
141
+ });
142
+
143
+ this.state.executions.set(approval_id, {
144
+ ...this.state.executions.get(approval_id),
145
+ status: result.success ? 'completed' : 'failed',
146
+ completed_at: Date.now(),
147
+ exit_code: result.exit_code,
148
+ stdout: result.stdout,
149
+ stderr: result.stderr,
150
+ error: result.error
151
+ });
152
+
153
+ await this.reportExecutionStatus(approval_id, result);
154
+ await this.storeExecutionResult(session_id, approval_id, tool_name, tool_input?.command, result);
155
+
156
+ } catch (error) {
157
+ console.error(`[QueueService] Execution error: ${error.message}`);
158
+ this.state.executions.set(approval_id, {
159
+ ...this.state.executions.get(approval_id),
160
+ status: 'failed',
161
+ completed_at: Date.now(),
162
+ error: error.message
163
+ });
164
+ }
165
+ }
166
+
167
+ async ackApproval(approval_id) {
168
+ if (!this.config.relayApiUrl || !this.config.relayApiKey) return;
169
+ try {
170
+ await fetch(`${this.config.relayApiUrl}/api/approvals/${approval_id}/ack`, {
171
+ method: 'POST',
172
+ headers: {
173
+ 'Content-Type': 'application/json',
174
+ 'Authorization': `Bearer ${this.config.relayApiKey}`
175
+ },
176
+ body: JSON.stringify({ processed: true })
177
+ });
178
+ } catch (err) {
179
+ console.error(`[QueueService] Ack failed: ${err.message}`);
180
+ }
181
+ }
182
+
183
+ async reportExecutionStatus(approval_id, result) {
184
+ if (!this.config.relayApiUrl || !this.config.relayApiKey) return;
185
+ try {
186
+ await fetch(`${this.config.relayApiUrl}/api/approvals/${approval_id}/executed`, {
187
+ method: 'POST',
188
+ headers: {
189
+ 'Content-Type': 'application/json',
190
+ 'Authorization': `Bearer ${this.config.relayApiKey}`
191
+ },
192
+ body: JSON.stringify({
193
+ success: result.success,
194
+ exit_code: result.exit_code,
195
+ stdout: result.stdout?.slice(0, 10000),
196
+ stderr: result.stderr?.slice(0, 10000),
197
+ error: result.error,
198
+ duration_ms: result.duration_ms
199
+ })
200
+ });
201
+ } catch (err) {
202
+ console.error(`[QueueService] Status report failed: ${err.message}`);
203
+ }
204
+ }
205
+
206
+ async storeExecutionResult(session_id, approval_id, tool_name, command, result) {
207
+ if (!this.config.relayApiUrl || !this.config.relayApiKey) return;
208
+ const payload = {
209
+ approval_id,
210
+ command: command || '',
211
+ tool_name,
212
+ exit_code: result.exit_code ?? null,
213
+ stdout: (result.stdout || '').slice(0, 10000),
214
+ stderr: (result.stderr || '').slice(0, 10000),
215
+ executed_at: result.executed_at || Date.now()
216
+ };
217
+
218
+ try {
219
+ await fetch(`${this.config.relayApiUrl}/api/sessions/${encodeURIComponent(session_id)}/results`, {
220
+ method: 'POST',
221
+ headers: {
222
+ 'Content-Type': 'application/json',
223
+ 'Authorization': `Bearer ${this.config.relayApiKey}`
224
+ },
225
+ body: JSON.stringify(payload)
226
+ });
227
+ } catch (err) {
228
+ console.error(`[QueueService] Result storage failed: ${err.message}`);
229
+ }
230
+ }
231
+
232
+ async spawnClaudeProcess(session_id, prompt, options = {}) {
233
+ const session = this.state.sessions.get(session_id);
234
+ if (!session) throw new Error(`Session not found: ${session_id}`);
235
+
236
+ return new Promise((resolve) => {
237
+ const cwd = session.cwd || process.cwd();
238
+ const startedAt = Date.now();
239
+
240
+ let isToolExecution = false;
241
+ let commandToRun = '';
242
+ let agentPrompt = '';
243
+
244
+ try {
245
+ const promptObj = JSON.parse(prompt);
246
+ if (promptObj.parameters?.command) {
247
+ isToolExecution = true;
248
+ commandToRun = promptObj.parameters.command;
249
+ }
250
+ } catch (e) {}
251
+
252
+ if (!isToolExecution) agentPrompt = prompt;
253
+
254
+ let child;
255
+ if (isToolExecution) {
256
+ child = spawn('sh', ['-c', commandToRun], {
257
+ cwd,
258
+ stdio: 'pipe',
259
+ env: { ...process.env, TELEPORTATION_DAEMON_CHILD: 'true' }
260
+ });
261
+ } else {
262
+ const cliBin = process.env.CLAUDE_CLI_PATH || 'claude';
263
+ const resumeSessionId = session.claude_session_id || session_id;
264
+ const args = ['--resume', resumeSessionId, '-p', agentPrompt, '--dangerously-skip-permissions'];
265
+
266
+ child = spawn(cliBin, args, {
267
+ cwd,
268
+ stdio: 'pipe',
269
+ env: { ...process.env, TELEPORTATION_DAEMON_CHILD: 'true', CI: 'true' }
270
+ });
271
+ }
272
+
273
+ if (options.onSpawn) options.onSpawn(child);
274
+ if (child.stdin) child.stdin.end();
275
+
276
+ let stdout = '';
277
+ let stderr = '';
278
+ let timedOut = false;
279
+
280
+ child.stdout.on('data', (data) => { stdout += data.toString(); });
281
+ child.stderr.on('data', (data) => { stderr += data.toString(); });
282
+
283
+ const timeout = setTimeout(() => {
284
+ timedOut = true;
285
+ child.kill('SIGTERM');
286
+ }, this.childTimeoutMs);
287
+
288
+ child.on('close', (code) => {
289
+ clearTimeout(timeout);
290
+ const executedAt = Date.now();
291
+ resolve({
292
+ success: code === 0 && !timedOut,
293
+ exit_code: code,
294
+ stdout: truncateOutput(stdout, 'STDOUT'),
295
+ stderr: truncateOutput(stderr, 'STDERR'),
296
+ error: timedOut ? 'Execution timed out' : null,
297
+ duration_ms: executedAt - startedAt,
298
+ started_at: startedAt,
299
+ executed_at: executedAt
300
+ });
301
+ });
302
+
303
+ child.on('error', (err) => {
304
+ clearTimeout(timeout);
305
+ resolve({
306
+ success: false,
307
+ exit_code: -1,
308
+ stdout: '',
309
+ stderr: '',
310
+ error: err.message,
311
+ duration_ms: 0,
312
+ started_at: startedAt,
313
+ executed_at: Date.now()
314
+ });
315
+ });
316
+ });
317
+ }
318
+ }
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Session Service
3
+ *
4
+ * Manages Claude Code sessions, registration, and liveness monitoring.
5
+ */
6
+
7
+ // NOTE: HeartbeatManager removed in PRD-0025 — daemon handles heartbeats inline.
8
+
9
+ export class SessionService {
10
+ constructor(state, config) {
11
+ this.name = 'sessions';
12
+ this.state = state;
13
+ this.config = config;
14
+ }
15
+
16
+ async start(ctx) {
17
+ this.ctx = ctx;
18
+ const { server } = ctx;
19
+
20
+ // Register routes
21
+ server.addRoute('POST', '/sessions/register', (req) => this.handleRegister(req));
22
+ server.addRoute('POST', '/sessions/stop', (req) => this.handleStop(req));
23
+
24
+ console.log('[SessionService] Started');
25
+ }
26
+
27
+ async stop() {
28
+ console.log('[SessionService] Stopped');
29
+ }
30
+
31
+ getStats() {
32
+ return {
33
+ activeSessions: this.state.sessions.size,
34
+ stoppedSessions: this.state.stoppedSessions.size
35
+ };
36
+ }
37
+
38
+ async handleRegister(req) {
39
+ try {
40
+ const body = await req.json();
41
+ const { session_id, claude_session_id, cwd, meta } = body;
42
+
43
+ if (!session_id) {
44
+ return Response.json({ error: 'session_id is required' }, { status: 400 });
45
+ }
46
+
47
+ if (this.state.stoppedSessions.has(session_id)) {
48
+ return Response.json({
49
+ error: 'session_stopped',
50
+ message: 'This session has ended and cannot be re-activated'
51
+ }, { status: 403 });
52
+ }
53
+
54
+ this.state.sessions.set(session_id, {
55
+ session_id,
56
+ claude_session_id: claude_session_id || session_id,
57
+ cwd: cwd || process.cwd(),
58
+ meta: {
59
+ ...(meta || {}),
60
+ daemon_pid: process.pid
61
+ },
62
+ registered_at: Date.now()
63
+ });
64
+
65
+ this.state.sessionActivity.set(session_id, Date.now());
66
+ this.state.lastSessionActivityAt = Date.now();
67
+
68
+ console.log(`[SessionService] Session registered: ${session_id}`);
69
+
70
+ // Update daemon_state on relay
71
+ if (this.config.relayApiUrl && this.config.relayApiKey) {
72
+ fetch(
73
+ `${this.config.relayApiUrl}/api/sessions/${encodeURIComponent(session_id)}/daemon-state`,
74
+ {
75
+ method: 'PATCH',
76
+ headers: {
77
+ 'Authorization': `Bearer ${this.config.relayApiKey}`,
78
+ 'Content-Type': 'application/json'
79
+ },
80
+ body: JSON.stringify({
81
+ status: 'running',
82
+ started_reason: 'session_registered'
83
+ })
84
+ }
85
+ ).catch(err => {
86
+ console.error(`[SessionService] Failed to update daemon_state for ${session_id}: ${err.message}`);
87
+ });
88
+ }
89
+
90
+ return Response.json({ ok: true });
91
+ } catch (error) {
92
+ return Response.json({ error: error.message }, { status: 500 });
93
+ }
94
+ }
95
+
96
+ async handleStop(req) {
97
+ try {
98
+ const body = await req.json();
99
+ const { session_id } = body;
100
+
101
+ if (!session_id) {
102
+ return Response.json({ error: 'session_id is required' }, { status: 400 });
103
+ }
104
+
105
+ this.state.sessions.delete(session_id);
106
+ this.state.stoppedSessions.add(session_id);
107
+
108
+ console.log(`[SessionService] Session stopped: ${session_id}`);
109
+
110
+ return Response.json({ ok: true });
111
+ } catch (error) {
112
+ return Response.json({ error: error.message }, { status: 500 });
113
+ }
114
+ }
115
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Daemon State
3
+ *
4
+ * Shared in-memory state for the teleportation daemon.
5
+ * This object is passed to services to allow them to share data.
6
+ */
7
+
8
+ export const createDaemonState = () => ({
9
+ // In-memory registry of active teleportation sessions handled by this daemon
10
+ sessions: new Map(),
11
+
12
+ // Track sessions that have been explicitly stopped and should not be re-activated
13
+ stoppedSessions: new Set(),
14
+
15
+ // Session activity tracking for cleanup
16
+ sessionActivity: new Map(), // sessionId -> lastActivityTimestamp
17
+
18
+ // Heartbeat tracking: session_id -> { count, lastSent }
19
+ heartbeatState: new Map(),
20
+
21
+ // Transcript ingestion throttling: Map<session_id, Promise>
22
+ ingestionInProgress: new Map(),
23
+
24
+ // Approval queue: FIFO queue of pending approvals
25
+ approvalQueue: [],
26
+
27
+ // Execution tracking: approval_id -> execution details
28
+ executions: new Map(),
29
+
30
+ // Tracking last time we had any registered sessions
31
+ lastSessionActivityAt: Date.now(),
32
+
33
+ // Shutdown flag
34
+ isShuttingDown: false
35
+ });