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
@@ -0,0 +1,215 @@
1
+ /**
2
+ * Timeline Analyzer
3
+ *
4
+ * Analyzes timeline events to determine task state without in-memory tracking.
5
+ * This makes the daemon stateless and crash-resistant.
6
+ *
7
+ * @module lib/daemon/timeline-analyzer
8
+ */
9
+
10
+ /**
11
+ * Fetch timeline events for a session
12
+ * @param {string} session_id - Teleportation session ID
13
+ * @param {Object} config - Configuration with relayApiUrl and apiKey
14
+ * @returns {Promise<Array>} Timeline events
15
+ */
16
+ export async function fetchTimeline(session_id, config) {
17
+ const { relayApiUrl, apiKey } = config;
18
+
19
+ const response = await fetch(`${relayApiUrl}/api/timeline/${session_id}`, {
20
+ headers: { 'Authorization': `Bearer ${apiKey}` }
21
+ });
22
+
23
+ if (!response.ok) {
24
+ throw new Error(`Failed to fetch timeline: ${response.status}`);
25
+ }
26
+
27
+ const data = await response.json();
28
+ return data.events || [];
29
+ }
30
+
31
+ /**
32
+ * Analyze timeline to determine current task state
33
+ * @param {Array} events - Timeline events (sorted chronologically)
34
+ * @param {string} task_id - Task ID to analyze
35
+ * @returns {Object} Current state analysis
36
+ */
37
+ export function analyzeTaskState(events, task_id) {
38
+ // Filter events for this specific task
39
+ const taskEvents = events.filter(e => e.meta?.task_id === task_id);
40
+
41
+ if (taskEvents.length === 0) {
42
+ return {
43
+ state: 'not_started',
44
+ turn_count: 0,
45
+ ready_for_execution: true,
46
+ prompt: null,
47
+ claude_session_id: null,
48
+ };
49
+ }
50
+
51
+ // Sort by timestamp to ensure correct order
52
+ taskEvents.sort((a, b) => a.timestamp - b.timestamp);
53
+
54
+ const lastEvent = taskEvents[taskEvents.length - 1];
55
+
56
+ // Count turns (each assistant_response indicates a completed turn)
57
+ const turn_count = taskEvents.filter(e =>
58
+ e.type === 'assistant_response' && e.source === 'autonomous_task'
59
+ ).length;
60
+
61
+ // Find the latest claude_session_id from task execution
62
+ let claude_session_id = null;
63
+ for (let i = taskEvents.length - 1; i >= 0; i--) {
64
+ if (taskEvents[i].meta?.claude_session_id) {
65
+ claude_session_id = taskEvents[i].meta.claude_session_id;
66
+ break;
67
+ }
68
+ }
69
+
70
+ // Check for pending approvals
71
+ const pendingApprovals = taskEvents.filter(e =>
72
+ e.type === 'approval_requested' &&
73
+ !taskEvents.some(later =>
74
+ later.type === 'approval_decided' &&
75
+ later.meta?.approval_id === e.meta?.approval_id
76
+ )
77
+ );
78
+
79
+ if (pendingApprovals.length > 0) {
80
+ return {
81
+ state: 'waiting_approval',
82
+ turn_count,
83
+ ready_for_execution: false,
84
+ reason: `Waiting for ${pendingApprovals.length} approval(s)`,
85
+ claude_session_id,
86
+ pending_approvals: pendingApprovals.length,
87
+ };
88
+ }
89
+
90
+ // Check if task was explicitly paused
91
+ if (lastEvent.type === 'task_paused') {
92
+ return {
93
+ state: 'paused',
94
+ turn_count,
95
+ ready_for_execution: false,
96
+ reason: lastEvent.meta?.reason || 'Task paused',
97
+ claude_session_id,
98
+ waiting_for_user_message: true,
99
+ };
100
+ }
101
+
102
+ // Check if task was stopped
103
+ if (lastEvent.type === 'task_stopped') {
104
+ return {
105
+ state: 'stopped',
106
+ turn_count,
107
+ ready_for_execution: false,
108
+ reason: lastEvent.meta?.reason || 'Task stopped',
109
+ claude_session_id,
110
+ };
111
+ }
112
+
113
+ // Check if task completed successfully
114
+ if (lastEvent.type === 'task_completed') {
115
+ return {
116
+ state: 'completed',
117
+ turn_count,
118
+ ready_for_execution: false,
119
+ claude_session_id,
120
+ };
121
+ }
122
+
123
+ // Check if waiting for budget increase
124
+ if (lastEvent.type === 'task_budget_hit') {
125
+ return {
126
+ state: 'budget_exhausted',
127
+ turn_count,
128
+ ready_for_execution: false,
129
+ reason: 'Budget exhausted',
130
+ claude_session_id,
131
+ };
132
+ }
133
+
134
+ // If last event is assistant_response, ready for next turn
135
+ // Use stop_reason to determine if Claude is done (works like CLI)
136
+ if (lastEvent.type === 'assistant_response') {
137
+ const stopReason = lastEvent.meta?.stop_reason;
138
+
139
+ // Claude uses "end_turn" when it's done with the current turn and waiting for input
140
+ // This is the natural stopping point, just like in the CLI
141
+ if (stopReason === 'end_turn') {
142
+ return {
143
+ state: 'ready_for_next_turn',
144
+ turn_count,
145
+ ready_for_execution: true,
146
+ claude_session_id,
147
+ };
148
+ }
149
+
150
+ // Other stop reasons (max_tokens, stop_sequence, etc.)
151
+ return {
152
+ state: 'ready_for_next_turn',
153
+ turn_count,
154
+ ready_for_execution: true,
155
+ claude_session_id,
156
+ };
157
+ }
158
+
159
+ // Default: ready to execute if we have task_started
160
+ const hasStarted = taskEvents.some(e => e.type === 'task_started');
161
+
162
+ return {
163
+ state: hasStarted ? 'in_progress' : 'not_started',
164
+ turn_count,
165
+ ready_for_execution: true,
166
+ claude_session_id,
167
+ };
168
+ }
169
+
170
+ /**
171
+ * Determine the next prompt to use for task execution
172
+ * @param {Object} state - Current state from analyzeTaskState
173
+ * @param {Object} task - Task object from Redis
174
+ * @returns {string|null} Prompt to use, or null if not ready
175
+ */
176
+ export function getNextPrompt(state, task) {
177
+ if (!state.ready_for_execution) {
178
+ return null;
179
+ }
180
+
181
+ // First turn: use original task description
182
+ if (state.turn_count === 0) {
183
+ return task.task;
184
+ }
185
+
186
+ // If paused and resumed with user message, use that message
187
+ if (task.pending_question) {
188
+ return task.pending_question;
189
+ }
190
+
191
+ // Default continuation prompt
192
+ return 'Continue working on the task.';
193
+ }
194
+
195
+ /**
196
+ * Check if task should stop based on timeline analysis
197
+ * @param {Object} state - Current state from analyzeTaskState
198
+ * @param {number} maxTurns - Maximum allowed turns
199
+ * @returns {Object|null} Stop reason if should stop, null otherwise
200
+ */
201
+ export function shouldStopTask(state, maxTurns = 100) {
202
+ if (state.state === 'stopped' || state.state === 'completed') {
203
+ return { should_stop: true, reason: state.state };
204
+ }
205
+
206
+ if (state.turn_count >= maxTurns) {
207
+ return { should_stop: true, reason: `Max turns limit reached (${maxTurns})` };
208
+ }
209
+
210
+ if (state.state === 'budget_exhausted') {
211
+ return { should_stop: true, reason: 'Budget exhausted' };
212
+ }
213
+
214
+ return null;
215
+ }