teleportation-cli 1.0.1 → 1.1.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.
- package/.claude/hooks/heartbeat.mjs +0 -0
- package/.claude/hooks/permission_request.mjs +74 -23
- package/.claude/hooks/post_tool_use.mjs +5 -2
- package/.claude/hooks/pre_tool_use.mjs +45 -3
- package/.claude/hooks/stop.mjs +447 -19
- package/.claude/hooks/user_prompt_submit.mjs +57 -0
- package/lib/config/manager.js +164 -0
- package/lib/daemon/teleportation-daemon.js +315 -51
- package/lib/machine-coders/claude-code-adapter.js +191 -37
- package/lib/machine-coders/gemini-cli-adapter.js +3 -0
- package/lib/settings/manager.js +392 -0
- package/package.json +2 -3
|
File without changes
|
|
@@ -101,15 +101,15 @@ const fetchJson = async (url, opts) => {
|
|
|
101
101
|
|
|
102
102
|
const raw = await readStdin();
|
|
103
103
|
let input;
|
|
104
|
-
try {
|
|
105
|
-
input = JSON.parse(raw || '{}');
|
|
104
|
+
try {
|
|
105
|
+
input = JSON.parse(raw || '{}');
|
|
106
106
|
} catch (e) {
|
|
107
107
|
log(`ERROR: Invalid JSON: ${e.message}`);
|
|
108
108
|
return exit(0);
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
-
const { session_id, tool_name, tool_input, cwd } = input || {};
|
|
112
|
-
log(`Session: ${session_id}, Tool: ${tool_name}, CWD: ${cwd}`);
|
|
111
|
+
const { session_id, tool_name, tool_input, cwd, tool_use_id, transcript_path } = input || {};
|
|
112
|
+
log(`Session: ${session_id}, Tool: ${tool_name}, CWD: ${cwd}, tool_use_id: ${tool_use_id || 'none'}`);
|
|
113
113
|
|
|
114
114
|
// Validate session_id
|
|
115
115
|
if (!isValidSessionId(session_id)) {
|
|
@@ -157,15 +157,10 @@ const fetchJson = async (url, opts) => {
|
|
|
157
157
|
log(`Could not check away status: ${e.message} - using fail-safe: ${failSafe}`);
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
-
//
|
|
161
|
-
//
|
|
162
|
-
if
|
|
163
|
-
|
|
164
|
-
return exit(0);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// User is AWAY - create remote approval and poll for decision
|
|
168
|
-
log(`Creating remote approval for ${tool_name}...`);
|
|
160
|
+
// ALWAYS create remote approval so it's visible in mobile UI
|
|
161
|
+
// If user is present, we'll also let Claude Code show its native prompt
|
|
162
|
+
// The PostToolUse hook will invalidate the approval if user approves locally
|
|
163
|
+
log(`Creating remote approval for ${tool_name} (away=${isAway})...`);
|
|
169
164
|
|
|
170
165
|
// Extract session metadata (project name, hostname, branch, etc.)
|
|
171
166
|
let meta = {};
|
|
@@ -207,7 +202,48 @@ const fetchJson = async (url, opts) => {
|
|
|
207
202
|
log(`Warning: Failed to invalidate old approvals: ${e.message}`);
|
|
208
203
|
}
|
|
209
204
|
|
|
210
|
-
//
|
|
205
|
+
// Build conversation context (PRD-0013 Phase 1)
|
|
206
|
+
let conversation_context = null;
|
|
207
|
+
if (transcript_path) {
|
|
208
|
+
try {
|
|
209
|
+
const transcriptData = await readFile(transcript_path, 'utf-8');
|
|
210
|
+
const transcript = JSON.parse(transcriptData);
|
|
211
|
+
|
|
212
|
+
// Find last user message
|
|
213
|
+
let lastUserMessage = null;
|
|
214
|
+
for (let i = transcript.length - 1; i >= 0; i--) {
|
|
215
|
+
if (transcript[i]?.role === 'user' && transcript[i]?.content) {
|
|
216
|
+
// Extract text from content array
|
|
217
|
+
const textContent = Array.isArray(transcript[i].content)
|
|
218
|
+
? transcript[i].content.find(c => c?.type === 'text')?.text
|
|
219
|
+
: transcript[i].content;
|
|
220
|
+
if (textContent) {
|
|
221
|
+
lastUserMessage = textContent;
|
|
222
|
+
break;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (lastUserMessage) {
|
|
228
|
+
conversation_context = {
|
|
229
|
+
user_last_message: lastUserMessage.slice(0, 500), // Truncate to 500 chars
|
|
230
|
+
claude_reasoning: null, // Phase 2: extract Claude's reasoning
|
|
231
|
+
timestamp: Date.now()
|
|
232
|
+
};
|
|
233
|
+
log(`Extracted conversation context: user_message="${lastUserMessage.slice(0, 50)}..."`);
|
|
234
|
+
}
|
|
235
|
+
} catch (e) {
|
|
236
|
+
log(`Warning: Failed to extract conversation context: ${e.message}`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Fallback: Generate tool_use_id if not provided (defensive programming)
|
|
241
|
+
const effective_tool_use_id = tool_use_id || `tool_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
|
242
|
+
if (!tool_use_id) {
|
|
243
|
+
log(`Warning: tool_use_id not provided, generated fallback: ${effective_tool_use_id}`);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Create approval request with metadata and context (PRD-0013)
|
|
211
247
|
let approvalId;
|
|
212
248
|
try {
|
|
213
249
|
const created = await fetchJson(`${RELAY_API_URL}/api/approvals`, {
|
|
@@ -216,17 +252,35 @@ const fetchJson = async (url, opts) => {
|
|
|
216
252
|
'Content-Type': 'application/json',
|
|
217
253
|
'Authorization': `Bearer ${RELAY_API_KEY}`
|
|
218
254
|
},
|
|
219
|
-
body: JSON.stringify({
|
|
255
|
+
body: JSON.stringify({
|
|
256
|
+
session_id,
|
|
257
|
+
tool_name,
|
|
258
|
+
tool_input,
|
|
259
|
+
meta,
|
|
260
|
+
tool_use_id: effective_tool_use_id,
|
|
261
|
+
conversation_context,
|
|
262
|
+
task_description: null, // Phase 2: infer multi-step tasks
|
|
263
|
+
transcript_excerpt: null // Phase 2: include recent messages
|
|
264
|
+
})
|
|
220
265
|
});
|
|
221
266
|
approvalId = created.id;
|
|
222
|
-
log(`Approval created: ${approvalId}`);
|
|
267
|
+
log(`Approval created: ${approvalId} (tool_use_id: ${effective_tool_use_id})`);
|
|
223
268
|
} catch (e) {
|
|
224
269
|
log(`ERROR creating approval: ${e.message}`);
|
|
225
270
|
return exit(0); // Let Claude Code handle it
|
|
226
271
|
}
|
|
227
272
|
|
|
228
|
-
//
|
|
229
|
-
|
|
273
|
+
// If user is PRESENT: return immediately and let Claude Code show its native prompt
|
|
274
|
+
// The approval is now visible in mobile UI. If user approves locally:
|
|
275
|
+
// - PostToolUse will invalidate the approval
|
|
276
|
+
// - Mobile UI will filter it out (any timeline event after approval = stale)
|
|
277
|
+
if (!isAway) {
|
|
278
|
+
log(`User is present - approval ${approvalId} created for mobile visibility, letting Claude Code handle locally`);
|
|
279
|
+
return exit(0);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// User is AWAY - poll for remote approval decision
|
|
283
|
+
log(`User is away - polling for remote approval decision (timeout=${FAST_APPROVAL_TIMEOUT_MS}ms)...`);
|
|
230
284
|
let consecutiveFailures = 0;
|
|
231
285
|
const MAX_CONSECUTIVE_FAILURES = 5;
|
|
232
286
|
|
|
@@ -241,7 +295,6 @@ const fetchJson = async (url, opts) => {
|
|
|
241
295
|
if (status.status === 'allowed') {
|
|
242
296
|
log('Remote approval: ALLOWED');
|
|
243
297
|
// Acknowledge on the fast-path to prevent duplicate daemon execution.
|
|
244
|
-
// CRITICAL: If ACK fails, we must NOT proceed with local execution to avoid duplicates.
|
|
245
298
|
try {
|
|
246
299
|
const ackRes = await fetch(`${RELAY_API_URL}/api/approvals/${approvalId}/ack`, {
|
|
247
300
|
method: 'POST',
|
|
@@ -252,7 +305,6 @@ const fetchJson = async (url, opts) => {
|
|
|
252
305
|
}
|
|
253
306
|
} catch (e) {
|
|
254
307
|
log(`ERROR: Failed to ack approval ${approvalId}: ${e.message} - aborting to prevent duplicate execution`);
|
|
255
|
-
// Return early without allowing - let daemon handle it instead
|
|
256
308
|
const out = {
|
|
257
309
|
hookSpecificOutput: {
|
|
258
310
|
hookEventName: 'PermissionRequest',
|
|
@@ -306,8 +358,7 @@ const fetchJson = async (url, opts) => {
|
|
|
306
358
|
await sleep(POLLING_INTERVAL_MS);
|
|
307
359
|
}
|
|
308
360
|
|
|
309
|
-
//
|
|
310
|
-
// Instead, hand off to daemon (background) and inform Claude.
|
|
361
|
+
// Timeout while user is away - hand off to daemon
|
|
311
362
|
log('Fast-path approval timeout - handing off to daemon');
|
|
312
363
|
|
|
313
364
|
try {
|
|
@@ -327,7 +378,7 @@ const fetchJson = async (url, opts) => {
|
|
|
327
378
|
hookSpecificOutput: {
|
|
328
379
|
hookEventName: 'PermissionRequest',
|
|
329
380
|
permissionDecision: 'deny',
|
|
330
|
-
permissionDecisionReason: '⏳ Teleportation: waiting for mobile approval timed out. This request was handed off to the daemon and will run in the background once approved.
|
|
381
|
+
permissionDecisionReason: '⏳ Teleportation: waiting for mobile approval timed out. This request was handed off to the daemon and will run in the background once approved.'
|
|
331
382
|
},
|
|
332
383
|
suppressOutput: true
|
|
333
384
|
};
|
|
@@ -81,7 +81,9 @@ const fetchJson = async (url, opts) => {
|
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
// Clear any pending approvals for this session since the tool executed successfully
|
|
84
|
-
// This handles the case where Claude Code auto-approved the tool
|
|
84
|
+
// This handles the case where Claude Code auto-approved the tool locally
|
|
85
|
+
// We set handled_location: 'local' to explicitly mark these as locally-handled
|
|
86
|
+
// (not approved via mobile UI) - this prevents race conditions in the frontend
|
|
85
87
|
try {
|
|
86
88
|
await fetchJson(`${RELAY_API_URL}/api/approvals/invalidate`, {
|
|
87
89
|
method: 'POST',
|
|
@@ -91,7 +93,8 @@ const fetchJson = async (url, opts) => {
|
|
|
91
93
|
},
|
|
92
94
|
body: JSON.stringify({
|
|
93
95
|
session_id,
|
|
94
|
-
reason: `Tool ${tool_name} executed (auto-approved by Claude Code)
|
|
96
|
+
reason: `Tool ${tool_name} executed (auto-approved by Claude Code)`,
|
|
97
|
+
handled_location: 'local'
|
|
95
98
|
})
|
|
96
99
|
});
|
|
97
100
|
log(`Cleared pending approvals after tool execution: ${tool_name}`);
|
|
@@ -144,6 +144,7 @@ const fetchJson = async (url, opts) => {
|
|
|
144
144
|
// can approve/deny from their mobile device. This enables true remote control.
|
|
145
145
|
|
|
146
146
|
// Helper: Format daemon work results into a human-readable message
|
|
147
|
+
// Enhanced for PRD-0013 Phase 1: Context Preservation
|
|
147
148
|
const formatDaemonUpdate = (results) => {
|
|
148
149
|
if (!results || results.length === 0) return '';
|
|
149
150
|
|
|
@@ -151,12 +152,53 @@ const fetchJson = async (url, opts) => {
|
|
|
151
152
|
const hasBrowserTasks = results.some(r => {
|
|
152
153
|
const toolName = (r.tool_name || '').toLowerCase();
|
|
153
154
|
const command = (r.command || '').toLowerCase();
|
|
154
|
-
return toolName.includes('browser') || toolName.includes('mcp') ||
|
|
155
|
+
return toolName.includes('browser') || toolName.includes('mcp') ||
|
|
155
156
|
command.includes('browser') || command.includes('mcp');
|
|
156
157
|
});
|
|
157
|
-
|
|
158
|
+
|
|
158
159
|
const taskType = hasBrowserTasks ? 'browser/interactive task' : 'task';
|
|
159
|
-
|
|
160
|
+
let header = `🤖 **Daemon Work Update** (${results.length} ${taskType}${results.length > 1 ? 's' : ''} completed while you were away)\n\n`;
|
|
161
|
+
|
|
162
|
+
// PRD-0013 Phase 1: Show original context if available
|
|
163
|
+
// This helps Claude remember what the user originally requested
|
|
164
|
+
const firstResult = results[0];
|
|
165
|
+
if (firstResult?.original_context?.user_last_message) {
|
|
166
|
+
const context = firstResult.original_context;
|
|
167
|
+
const timeElapsed = context.timestamp
|
|
168
|
+
? Date.now() - context.timestamp
|
|
169
|
+
: null;
|
|
170
|
+
|
|
171
|
+
// Format time elapsed
|
|
172
|
+
let timeAgo = '';
|
|
173
|
+
if (timeElapsed) {
|
|
174
|
+
const seconds = Math.floor(timeElapsed / 1000);
|
|
175
|
+
const minutes = Math.floor(seconds / 60);
|
|
176
|
+
const hours = Math.floor(minutes / 60);
|
|
177
|
+
|
|
178
|
+
if (hours > 0) {
|
|
179
|
+
timeAgo = `${hours} hour${hours > 1 ? 's' : ''} ago`;
|
|
180
|
+
} else if (minutes > 0) {
|
|
181
|
+
timeAgo = `${minutes} minute${minutes > 1 ? 's' : ''} ago`;
|
|
182
|
+
} else {
|
|
183
|
+
timeAgo = `${seconds} second${seconds > 1 ? 's' : ''} ago`;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
header += `**Original Request**${timeAgo ? ` (${timeAgo})` : ''}:\n`;
|
|
188
|
+
header += `> ${context.user_last_message}\n\n`;
|
|
189
|
+
|
|
190
|
+
// Show original tool_use_id if available (helps with debugging/tracing)
|
|
191
|
+
if (firstResult.original_tool_use_id) {
|
|
192
|
+
header += `_Results for tool ID: ${firstResult.original_tool_use_id}_\n\n`;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// PRD-0013 Phase 2: Show task description and next step suggestion if available
|
|
196
|
+
// For Phase 1, task_description will be null, so this won't appear yet
|
|
197
|
+
if (context.task_description) {
|
|
198
|
+
header += `**Next Step:**\n`;
|
|
199
|
+
header += `${context.task_description}\n\n`;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
160
202
|
|
|
161
203
|
const formatOutput = (output, toolName) => {
|
|
162
204
|
if (!output || output.trim() === '') return '(No output)';
|