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.
- package/.claude/hooks/permission_request.mjs +326 -59
- package/.claude/hooks/post_tool_use.mjs +90 -0
- package/.claude/hooks/pre_tool_use.mjs +212 -293
- package/.claude/hooks/session-register.mjs +89 -104
- package/.claude/hooks/session_end.mjs +41 -42
- package/.claude/hooks/session_start.mjs +45 -60
- package/.claude/hooks/stop.mjs +752 -99
- package/.claude/hooks/user_prompt_submit.mjs +26 -3
- package/lib/cli/daemon-commands.js +1 -1
- package/lib/cli/teleport-commands.js +469 -0
- package/lib/daemon/daemon-v2.js +104 -0
- package/lib/daemon/lifecycle.js +56 -171
- package/lib/daemon/services/index.js +3 -0
- package/lib/daemon/services/polling-service.js +173 -0
- package/lib/daemon/services/queue-service.js +318 -0
- package/lib/daemon/services/session-service.js +115 -0
- package/lib/daemon/state.js +35 -0
- package/lib/daemon/task-executor-v2.js +413 -0
- package/lib/daemon/task-executor.js +270 -96
- package/lib/daemon/teleportation-daemon.js +709 -126
- package/lib/daemon/timeline-analyzer.js +215 -0
- package/lib/daemon/transcript-ingestion.js +696 -0
- package/lib/daemon/utils.js +91 -0
- package/lib/install/installer.js +184 -20
- package/lib/install/uhr-installer.js +136 -0
- package/lib/remote/providers/base-provider.js +46 -0
- package/lib/remote/providers/daytona-provider.js +58 -0
- package/lib/remote/providers/provider-factory.js +90 -19
- package/lib/remote/providers/sprites-provider.js +711 -0
- package/lib/teleport/exporters/claude-exporter.js +302 -0
- package/lib/teleport/exporters/gemini-exporter.js +307 -0
- package/lib/teleport/exporters/index.js +93 -0
- package/lib/teleport/exporters/interface.js +153 -0
- package/lib/teleport/fork-tracker.js +415 -0
- package/lib/teleport/git-committer.js +337 -0
- package/lib/teleport/index.js +48 -0
- package/lib/teleport/manager.js +620 -0
- package/lib/teleport/session-capture.js +282 -0
- package/package.json +6 -2
- package/teleportation-cli.cjs +488 -453
- package/.claude/hooks/heartbeat.mjs +0 -396
- package/lib/daemon/pid-manager.js +0 -183
|
@@ -55,6 +55,24 @@ const fetchJson = async (url, opts) => {
|
|
|
55
55
|
return res.json();
|
|
56
56
|
};
|
|
57
57
|
|
|
58
|
+
const PRE_TOOL_NETWORK_TIMEOUT_MS = parseInt(env.TELEPORTATION_PRE_TOOL_NETWORK_TIMEOUT_MS || '1500', 10);
|
|
59
|
+
const PRE_TOOL_TOTAL_BUDGET_MS = parseInt(env.TELEPORTATION_PRE_TOOL_TOTAL_BUDGET_MS || '3500', 10);
|
|
60
|
+
|
|
61
|
+
const withTimeout = async (promiseFactory, timeoutMs, label) => {
|
|
62
|
+
const controller = new AbortController();
|
|
63
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
64
|
+
try {
|
|
65
|
+
return await promiseFactory(controller.signal);
|
|
66
|
+
} catch (error) {
|
|
67
|
+
if (error?.name === 'AbortError') {
|
|
68
|
+
throw new Error(`${label} timed out after ${timeoutMs}ms`);
|
|
69
|
+
}
|
|
70
|
+
throw error;
|
|
71
|
+
} finally {
|
|
72
|
+
clearTimeout(timeoutId);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
58
76
|
(async () => {
|
|
59
77
|
// Debug: Log hook invocation
|
|
60
78
|
const hookLogFile = env.TELEPORTATION_HOOK_LOG || '/tmp/teleportation-hook.log';
|
|
@@ -68,17 +86,30 @@ const fetchJson = async (url, opts) => {
|
|
|
68
86
|
}
|
|
69
87
|
};
|
|
70
88
|
|
|
71
|
-
|
|
89
|
+
// Read and parse stdin to get hook input
|
|
72
90
|
let input = {};
|
|
73
91
|
try {
|
|
74
|
-
|
|
92
|
+
const raw = await readStdin();
|
|
93
|
+
input = JSON.parse(raw || '{}');
|
|
75
94
|
} catch (e) {
|
|
76
|
-
log(`
|
|
95
|
+
log(`[PreToolUse] Failed to parse stdin: ${e.message}`);
|
|
77
96
|
}
|
|
78
97
|
|
|
79
98
|
let { session_id, tool_name, tool_input } = input || {};
|
|
80
99
|
tool_input = tool_input && typeof tool_input === 'object' ? tool_input : {};
|
|
81
100
|
let claude_session_id = session_id; // Keep original ID
|
|
101
|
+
const hookStartMs = Date.now();
|
|
102
|
+
const remainingBudgetMs = () => PRE_TOOL_TOTAL_BUDGET_MS - (Date.now() - hookStartMs);
|
|
103
|
+
const hasBudget = (reserveMs = 150) => remainingBudgetMs() > reserveMs;
|
|
104
|
+
const getRequestTimeoutMs = () => {
|
|
105
|
+
const remaining = remainingBudgetMs();
|
|
106
|
+
if (remaining <= 200) return 0;
|
|
107
|
+
return Math.max(200, Math.min(PRE_TOOL_NETWORK_TIMEOUT_MS, remaining - 100));
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
// Detect message source
|
|
111
|
+
const isAutonomousTask = env.TELEPORTATION_TASK_MODE === 'true' || !!env.TELEPORTATION_PARENT_SESSION_ID;
|
|
112
|
+
const source = isAutonomousTask ? 'autonomous_task' : 'cli_interactive';
|
|
82
113
|
|
|
83
114
|
// 1. Recursion Guard (Critical Stability Fix)
|
|
84
115
|
// Prevent infinite hook-triggered tool loops
|
|
@@ -102,11 +133,18 @@ const fetchJson = async (url, opts) => {
|
|
|
102
133
|
const command = tool_input?.command || tool_input?.text || '';
|
|
103
134
|
if (typeof command === 'string') {
|
|
104
135
|
const trimmedCmd = command.trim().toLowerCase();
|
|
105
|
-
|
|
136
|
+
// Support multiple command formats: /away, teleportation away, teleport away, teleporation away (typo)
|
|
137
|
+
if (trimmedCmd === '/away' ||
|
|
138
|
+
trimmedCmd === 'teleportation away' ||
|
|
139
|
+
trimmedCmd === 'teleport away' ||
|
|
140
|
+
trimmedCmd === 'teleporation away') {
|
|
106
141
|
log('Detected /away command - setting away mode');
|
|
107
142
|
// Will set away mode after loading config
|
|
108
143
|
tool_input.__teleportation_away = true;
|
|
109
|
-
} else if (trimmedCmd === '/back' ||
|
|
144
|
+
} else if (trimmedCmd === '/back' ||
|
|
145
|
+
trimmedCmd === 'teleportation back' ||
|
|
146
|
+
trimmedCmd === 'teleport back' ||
|
|
147
|
+
trimmedCmd === 'teleporation back') {
|
|
110
148
|
log('Detected /back command - clearing away mode');
|
|
111
149
|
tool_input.__teleportation_back = true;
|
|
112
150
|
}
|
|
@@ -132,7 +170,6 @@ const fetchJson = async (url, opts) => {
|
|
|
132
170
|
const RELAY_API_KEY = env.RELAY_API_KEY || config.relayApiKey || '';
|
|
133
171
|
const DAEMON_PORT = config.daemonPort || env.TELEPORTATION_DAEMON_PORT || '3050';
|
|
134
172
|
const DAEMON_ENABLED = config.daemonEnabled !== false && env.TELEPORTATION_DAEMON_ENABLED !== 'false';
|
|
135
|
-
const CONTEXT_DELIVERY_ENABLED = config.contextDeliveryEnabled !== false && env.TELEPORTATION_CONTEXT_DELIVERY_ENABLED !== 'false';
|
|
136
173
|
|
|
137
174
|
// Fast polling timeout - how long to wait before handing off to daemon
|
|
138
175
|
// Default: 60 seconds - provides seamless experience before daemon handoff
|
|
@@ -156,157 +193,6 @@ const fetchJson = async (url, opts) => {
|
|
|
156
193
|
// All tool requests are sent to the remote approval system so the user
|
|
157
194
|
// can approve/deny from their mobile device. This enables true remote control.
|
|
158
195
|
|
|
159
|
-
// Helper: Format daemon work results into a human-readable message
|
|
160
|
-
// Enhanced for PRD-0013 Phase 1: Context Preservation
|
|
161
|
-
const formatDaemonUpdate = (results) => {
|
|
162
|
-
if (!results || results.length === 0) return '';
|
|
163
|
-
|
|
164
|
-
// Check if any browser tasks were completed
|
|
165
|
-
const hasBrowserTasks = results.some(r => {
|
|
166
|
-
const toolName = (r.tool_name || '').toLowerCase();
|
|
167
|
-
const command = (r.command || '').toLowerCase();
|
|
168
|
-
return toolName.includes('browser') || toolName.includes('mcp') ||
|
|
169
|
-
command.includes('browser') || command.includes('mcp');
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
const taskType = hasBrowserTasks ? 'browser/interactive task' : 'task';
|
|
173
|
-
let header = `🤖 **Daemon Work Update** (${results.length} ${taskType}${results.length > 1 ? 's' : ''} completed while you were away)\n\n`;
|
|
174
|
-
|
|
175
|
-
// PRD-0013 Phase 1: Show original context if available
|
|
176
|
-
// This helps Claude remember what the user originally requested
|
|
177
|
-
const firstResult = results[0];
|
|
178
|
-
if (firstResult?.original_context?.user_last_message) {
|
|
179
|
-
const context = firstResult.original_context;
|
|
180
|
-
const timeElapsed = context.timestamp
|
|
181
|
-
? Date.now() - context.timestamp
|
|
182
|
-
: null;
|
|
183
|
-
|
|
184
|
-
// Format time elapsed
|
|
185
|
-
let timeAgo = '';
|
|
186
|
-
if (timeElapsed) {
|
|
187
|
-
const seconds = Math.floor(timeElapsed / 1000);
|
|
188
|
-
const minutes = Math.floor(seconds / 60);
|
|
189
|
-
const hours = Math.floor(minutes / 60);
|
|
190
|
-
|
|
191
|
-
if (hours > 0) {
|
|
192
|
-
timeAgo = `${hours} hour${hours > 1 ? 's' : ''} ago`;
|
|
193
|
-
} else if (minutes > 0) {
|
|
194
|
-
timeAgo = `${minutes} minute${minutes > 1 ? 's' : ''} ago`;
|
|
195
|
-
} else {
|
|
196
|
-
timeAgo = `${seconds} second${seconds > 1 ? 's' : ''} ago`;
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
header += `**Original Request**${timeAgo ? ` (${timeAgo})` : ''}:\n`;
|
|
201
|
-
header += `> ${context.user_last_message}\n\n`;
|
|
202
|
-
|
|
203
|
-
// Show original tool_use_id if available (helps with debugging/tracing)
|
|
204
|
-
if (firstResult.original_tool_use_id) {
|
|
205
|
-
header += `_Results for tool ID: ${firstResult.original_tool_use_id}_\n\n`;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// PRD-0013 Phase 2: Show task description and next step suggestion if available
|
|
209
|
-
// For Phase 1, task_description will be null, so this won't appear yet
|
|
210
|
-
if (context.task_description) {
|
|
211
|
-
header += `**Next Step:**\n`;
|
|
212
|
-
header += `${context.task_description}\n\n`;
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
const formatOutput = (output, toolName) => {
|
|
217
|
-
if (!output || output.trim() === '') return '(No output)';
|
|
218
|
-
|
|
219
|
-
// Try to detect and format JSON output
|
|
220
|
-
try {
|
|
221
|
-
const parsed = JSON.parse(output);
|
|
222
|
-
// For browser snapshots or large JSON, provide a summary
|
|
223
|
-
if (parsed.type === 'snapshot' || parsed.type === 'accessibility') {
|
|
224
|
-
const url = parsed.url || parsed.page?.url || '';
|
|
225
|
-
const title = parsed.title || parsed.page?.title || '';
|
|
226
|
-
const elements = parsed.elements?.length || parsed.children?.length || 0;
|
|
227
|
-
return `Browser snapshot captured:\n • URL: ${url}\n • Title: ${title}\n • Elements: ${elements}\n • Full snapshot available in output`;
|
|
228
|
-
}
|
|
229
|
-
// For other JSON, format nicely
|
|
230
|
-
return JSON.stringify(parsed, null, 2);
|
|
231
|
-
} catch {
|
|
232
|
-
// Not JSON, return as-is but format better
|
|
233
|
-
return output;
|
|
234
|
-
}
|
|
235
|
-
};
|
|
236
|
-
|
|
237
|
-
const formatToolName = (toolName, command) => {
|
|
238
|
-
if (toolName && toolName.toLowerCase().includes('browser')) return '🌐 Browser';
|
|
239
|
-
if (toolName && toolName.toLowerCase().includes('mcp')) return '🔌 MCP Tool';
|
|
240
|
-
if (command && command.toLowerCase().includes('browser')) return '🌐 Browser';
|
|
241
|
-
if (command && command.toLowerCase().includes('mcp')) return '🔌 MCP Tool';
|
|
242
|
-
return toolName || 'Command';
|
|
243
|
-
};
|
|
244
|
-
|
|
245
|
-
const details = results.map(r => {
|
|
246
|
-
// Determine success: exit_code 0 OR if there's meaningful output (browser tasks may not use exit codes)
|
|
247
|
-
const hasOutput = (r.stdout && r.stdout.trim()) || (r.stderr && r.stderr.trim());
|
|
248
|
-
const isSuccess = r.exit_code === 0 || r.exit_code === null || (hasOutput && !r.stderr);
|
|
249
|
-
const status = isSuccess ? '✅ Success' : `❌ Failed${r.exit_code !== null ? ` (Exit: ${r.exit_code})` : ''}`;
|
|
250
|
-
const time = new Date(r.executed_at).toLocaleTimeString();
|
|
251
|
-
|
|
252
|
-
const toolName = formatToolName(r.tool_name, r.command);
|
|
253
|
-
const output = r.stdout || r.stderr || '';
|
|
254
|
-
const formattedOutput = formatOutput(output, r.tool_name);
|
|
255
|
-
|
|
256
|
-
// For browser tasks or large outputs, provide a summary first
|
|
257
|
-
const isBrowserTask = toolName.includes('Browser') || toolName.includes('MCP');
|
|
258
|
-
const outputPreview = isBrowserTask && output.length > 1000
|
|
259
|
-
? formattedOutput.split('\n').slice(0, 20).join('\n') + '\n...(see full output below)...'
|
|
260
|
-
: formattedOutput.length > 2000
|
|
261
|
-
? formattedOutput.substring(0, 2000) + '\n...(truncated, see full output for details)...'
|
|
262
|
-
: formattedOutput;
|
|
263
|
-
|
|
264
|
-
let resultText = `**${toolName}:** ${r.command || '(task completed)'}\n`;
|
|
265
|
-
resultText += `**Status:** ${status} at ${time}\n`;
|
|
266
|
-
|
|
267
|
-
if (output.trim()) {
|
|
268
|
-
resultText += `\n**Result:**\n`;
|
|
269
|
-
// Use code blocks only for structured data, plain text otherwise
|
|
270
|
-
if (formattedOutput.includes('\n') || formattedOutput.length > 100) {
|
|
271
|
-
resultText += `\`\`\`\n${outputPreview}\n\`\`\`\n`;
|
|
272
|
-
} else {
|
|
273
|
-
resultText += `${outputPreview}\n`;
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
return resultText;
|
|
278
|
-
}).join('\n---\n\n');
|
|
279
|
-
|
|
280
|
-
const successCount = results.filter(r => {
|
|
281
|
-
const hasOutput = (r.stdout && r.stdout.trim()) || (r.stderr && r.stderr.trim());
|
|
282
|
-
return r.exit_code === 0 || r.exit_code === null || (hasOutput && !r.stderr);
|
|
283
|
-
}).length;
|
|
284
|
-
const failCount = results.length - successCount;
|
|
285
|
-
const summary = `\n**Summary:** ${successCount} successful, ${failCount} failed.`;
|
|
286
|
-
|
|
287
|
-
// Add a prompt to ensure results are acknowledged
|
|
288
|
-
const browserTaskCount = results.filter(r => {
|
|
289
|
-
const toolName = (r.tool_name || '').toLowerCase();
|
|
290
|
-
const command = (r.command || '').toLowerCase();
|
|
291
|
-
return toolName.includes('browser') || toolName.includes('mcp') ||
|
|
292
|
-
command.includes('browser') || command.includes('mcp');
|
|
293
|
-
}).length;
|
|
294
|
-
|
|
295
|
-
const footer = browserTaskCount > 0
|
|
296
|
-
? `\n\n💡 **Note:** Browser task results are included above. Please review and summarize what was accomplished.`
|
|
297
|
-
: '';
|
|
298
|
-
|
|
299
|
-
let message = header + details + summary + footer;
|
|
300
|
-
|
|
301
|
-
// Increase limit for browser tasks (they need more space)
|
|
302
|
-
const maxLength = 10000; // Increased from 5000
|
|
303
|
-
if (message.length > maxLength) {
|
|
304
|
-
message = message.substring(0, maxLength) + '\n\n...(output truncated, check full results for complete details)...';
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
return message;
|
|
308
|
-
};
|
|
309
|
-
|
|
310
196
|
// Register session: relay first (source of truth), then daemon
|
|
311
197
|
const cwd = process.cwd();
|
|
312
198
|
const meta = await getSessionMetadata(cwd);
|
|
@@ -334,24 +220,33 @@ const fetchJson = async (url, opts) => {
|
|
|
334
220
|
log(`Model changed detected: ${lastModel} -> ${meta.current_model}`);
|
|
335
221
|
|
|
336
222
|
// Log model change to timeline
|
|
337
|
-
if (RELAY_API_URL && RELAY_API_KEY) {
|
|
223
|
+
if (RELAY_API_URL && RELAY_API_KEY && hasBudget()) {
|
|
338
224
|
try {
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
225
|
+
const timeoutMs = getRequestTimeoutMs();
|
|
226
|
+
if (timeoutMs > 0) {
|
|
227
|
+
await withTimeout(
|
|
228
|
+
(signal) => fetch(`${RELAY_API_URL}/api/timeline`, {
|
|
229
|
+
method: 'POST',
|
|
230
|
+
headers: {
|
|
231
|
+
'Content-Type': 'application/json',
|
|
232
|
+
'Authorization': `Bearer ${RELAY_API_KEY}`
|
|
233
|
+
},
|
|
234
|
+
body: JSON.stringify({
|
|
235
|
+
session_id,
|
|
236
|
+
type: 'model_changed',
|
|
237
|
+
source,
|
|
238
|
+
data: {
|
|
239
|
+
previous_model: lastModel,
|
|
240
|
+
new_model: meta.current_model,
|
|
241
|
+
timestamp: Date.now()
|
|
242
|
+
}
|
|
243
|
+
}),
|
|
244
|
+
signal
|
|
245
|
+
}),
|
|
246
|
+
timeoutMs,
|
|
247
|
+
'model change timeline post'
|
|
248
|
+
);
|
|
249
|
+
}
|
|
355
250
|
log(`Model change logged to timeline`);
|
|
356
251
|
} catch (e) {
|
|
357
252
|
log(`Failed to log model change: ${e.message}`);
|
|
@@ -368,11 +263,21 @@ const fetchJson = async (url, opts) => {
|
|
|
368
263
|
}
|
|
369
264
|
|
|
370
265
|
// 1. Register with relay first - makes session visible in mobile UI
|
|
371
|
-
if (session_id && RELAY_API_URL && RELAY_API_KEY) {
|
|
266
|
+
if (session_id && RELAY_API_URL && RELAY_API_KEY && hasBudget()) {
|
|
372
267
|
try {
|
|
373
268
|
log(`Registering session with relay: ${session_id}`);
|
|
374
269
|
const { ensureSessionRegistered } = await import('./session-register.mjs');
|
|
375
|
-
const
|
|
270
|
+
const timeoutMs = getRequestTimeoutMs();
|
|
271
|
+
if (timeoutMs <= 0) {
|
|
272
|
+
log('Skipping relay registration due to exhausted hook budget');
|
|
273
|
+
}
|
|
274
|
+
const regResult = timeoutMs > 0
|
|
275
|
+
? await withTimeout(
|
|
276
|
+
() => ensureSessionRegistered(session_id, cwd, config),
|
|
277
|
+
timeoutMs,
|
|
278
|
+
'relay session registration'
|
|
279
|
+
)
|
|
280
|
+
: false;
|
|
376
281
|
|
|
377
282
|
if (typeof regResult === 'object' && regResult.error === 'orphan_api_key') {
|
|
378
283
|
console.error(`\n⚠️ Teleportation: ${regResult.message || 'API key not linked to user.'}`);
|
|
@@ -382,9 +287,16 @@ const fetchJson = async (url, opts) => {
|
|
|
382
287
|
}
|
|
383
288
|
|
|
384
289
|
// If model changed, update session metadata immediately
|
|
385
|
-
if (modelChanged && (regResult === true || (typeof regResult === 'object' && regResult.success))) {
|
|
290
|
+
if (modelChanged && hasBudget() && (regResult === true || (typeof regResult === 'object' && regResult.success))) {
|
|
386
291
|
const { updateSessionMetadata } = await import('./session-register.mjs');
|
|
387
|
-
|
|
292
|
+
const timeoutMs = getRequestTimeoutMs();
|
|
293
|
+
if (timeoutMs > 0) {
|
|
294
|
+
await withTimeout(
|
|
295
|
+
() => updateSessionMetadata(session_id, cwd, config),
|
|
296
|
+
timeoutMs,
|
|
297
|
+
'session metadata update'
|
|
298
|
+
);
|
|
299
|
+
}
|
|
388
300
|
log(`Session metadata updated with new model`);
|
|
389
301
|
}
|
|
390
302
|
} catch (e) {
|
|
@@ -393,23 +305,33 @@ const fetchJson = async (url, opts) => {
|
|
|
393
305
|
}
|
|
394
306
|
|
|
395
307
|
// 2. Then register with daemon (local infrastructure for this session)
|
|
396
|
-
if (session_id && DAEMON_ENABLED) {
|
|
308
|
+
if (session_id && DAEMON_ENABLED && hasBudget()) {
|
|
397
309
|
try {
|
|
398
310
|
const daemonUrl = `http://127.0.0.1:${DAEMON_PORT}`;
|
|
399
311
|
log(`Registering session with daemon: ${session_id}`);
|
|
400
312
|
|
|
401
|
-
const
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
313
|
+
const timeoutMs = getRequestTimeoutMs();
|
|
314
|
+
const res = timeoutMs > 0
|
|
315
|
+
? await withTimeout(
|
|
316
|
+
(signal) => fetch(`${daemonUrl}/sessions/register`, {
|
|
317
|
+
method: 'POST',
|
|
318
|
+
headers: { 'Content-Type': 'application/json' },
|
|
319
|
+
body: JSON.stringify({ session_id, claude_session_id, cwd, meta }),
|
|
320
|
+
signal
|
|
321
|
+
}),
|
|
322
|
+
timeoutMs,
|
|
323
|
+
'daemon session registration'
|
|
324
|
+
).catch(e => {
|
|
325
|
+
log(`Daemon registration fetch error: ${e.message}`);
|
|
326
|
+
return null;
|
|
327
|
+
})
|
|
328
|
+
: null;
|
|
409
329
|
if (res && res.ok) {
|
|
410
330
|
log(`Session registered with daemon successfully`);
|
|
411
331
|
} else if (res) {
|
|
412
332
|
log(`Daemon registration returned status ${res.status}`);
|
|
333
|
+
} else if (timeoutMs <= 0) {
|
|
334
|
+
log('Skipping daemon registration due to exhausted hook budget');
|
|
413
335
|
}
|
|
414
336
|
} catch (e) {
|
|
415
337
|
log(`Warning: Failed to register session with daemon: ${e.message}`);
|
|
@@ -418,93 +340,72 @@ const fetchJson = async (url, opts) => {
|
|
|
418
340
|
|
|
419
341
|
// 3. Log tool_use event to timeline (before execution)
|
|
420
342
|
// This shows what Claude is attempting to do
|
|
421
|
-
if (session_id && RELAY_API_URL && RELAY_API_KEY && tool_name) {
|
|
343
|
+
if (session_id && RELAY_API_URL && RELAY_API_KEY && tool_name && hasBudget()) {
|
|
422
344
|
try {
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
345
|
+
const timeoutMs = getRequestTimeoutMs();
|
|
346
|
+
if (timeoutMs > 0) {
|
|
347
|
+
await withTimeout(
|
|
348
|
+
(signal) => fetch(`${RELAY_API_URL}/api/timeline`, {
|
|
349
|
+
method: 'POST',
|
|
350
|
+
headers: {
|
|
351
|
+
'Content-Type': 'application/json',
|
|
352
|
+
'Authorization': `Bearer ${RELAY_API_KEY}`
|
|
353
|
+
},
|
|
354
|
+
body: JSON.stringify({
|
|
355
|
+
session_id,
|
|
356
|
+
type: 'tool_use',
|
|
357
|
+
source,
|
|
358
|
+
data: {
|
|
359
|
+
tool_name,
|
|
360
|
+
tool_input: tool_input || {},
|
|
361
|
+
timestamp: Date.now()
|
|
362
|
+
}
|
|
363
|
+
}),
|
|
364
|
+
signal
|
|
365
|
+
}),
|
|
366
|
+
timeoutMs,
|
|
367
|
+
'tool_use timeline post'
|
|
368
|
+
);
|
|
369
|
+
}
|
|
439
370
|
log(`Logged tool_use event for ${tool_name}`);
|
|
440
371
|
} catch (e) {
|
|
441
372
|
log(`Failed to log tool_use: ${e.message}`);
|
|
442
373
|
}
|
|
443
374
|
}
|
|
444
375
|
|
|
445
|
-
//
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
const results = await fetchJson(`${RELAY_API_URL}/api/sessions/${session_id}/results/pending`, {
|
|
450
|
-
headers: { 'Authorization': `Bearer ${RELAY_API_KEY}` }
|
|
451
|
-
});
|
|
452
|
-
|
|
453
|
-
if (results && results.length > 0) {
|
|
454
|
-
log(`Found ${results.length} pending results. Formatting update and denying current request to deliver context.`);
|
|
455
|
-
|
|
456
|
-
// Mark results as delivered in parallel (best-effort, but wait before exiting)
|
|
457
|
-
try {
|
|
458
|
-
await Promise.allSettled(
|
|
459
|
-
results.map(r =>
|
|
460
|
-
fetch(`${RELAY_API_URL}/api/sessions/${session_id}/results/${r.result_id}/delivered`, {
|
|
461
|
-
method: 'POST',
|
|
462
|
-
headers: { 'Authorization': `Bearer ${RELAY_API_KEY}` }
|
|
463
|
-
}).catch(e => {
|
|
464
|
-
log(`Failed to mark result ${r.result_id} delivered: ${e.message}`);
|
|
465
|
-
})
|
|
466
|
-
)
|
|
467
|
-
);
|
|
468
|
-
} catch (markErr) {
|
|
469
|
-
log(`Warning: Error while marking results delivered: ${markErr.message}`);
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
const updateMessage = formatDaemonUpdate(results);
|
|
473
|
-
// Log the daemon update but allow the current tool to proceed
|
|
474
|
-
// This prevents blocking errors while still informing Claude of daemon work
|
|
475
|
-
log(`Daemon update delivered: ${updateMessage.substring(0, 200)}...`);
|
|
476
|
-
|
|
477
|
-
// Output the update message to Claude by denying the current request
|
|
478
|
-
// This forces Claude to read the update before retrying the tool
|
|
479
|
-
const out = {
|
|
480
|
-
hookSpecificOutput: {
|
|
481
|
-
hookEventName: 'PreToolUse',
|
|
482
|
-
permissionDecision: 'deny', // Deny to force reading the update
|
|
483
|
-
permissionDecisionReason: updateMessage
|
|
484
|
-
},
|
|
485
|
-
suppressOutput: true
|
|
486
|
-
};
|
|
487
|
-
stdout.write(JSON.stringify(out));
|
|
488
|
-
return exit(0);
|
|
489
|
-
}
|
|
490
|
-
} catch (e) {
|
|
491
|
-
log(`Warning: Failed to check pending results: ${e.message}`);
|
|
492
|
-
}
|
|
493
|
-
}
|
|
376
|
+
// NOTE: Context delivery via notification has been removed.
|
|
377
|
+
// With parent session context resumption (PR #123), autonomous tasks now
|
|
378
|
+
// automatically resume the parent session's conversation context on turn 1.
|
|
379
|
+
// This eliminates the need for blocking notifications to deliver daemon work updates.
|
|
494
380
|
|
|
495
381
|
// Helper: Update session daemon state
|
|
496
382
|
const updateSessionState = async (updates) => {
|
|
497
383
|
if (!session_id || !RELAY_API_URL || !RELAY_API_KEY) return;
|
|
384
|
+
if (!hasBudget()) {
|
|
385
|
+
log('Skipping session state update due to exhausted hook budget');
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
498
388
|
try {
|
|
499
389
|
log(`Updating session state: ${JSON.stringify(updates)}`);
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
390
|
+
const timeoutMs = getRequestTimeoutMs();
|
|
391
|
+
if (timeoutMs <= 0) {
|
|
392
|
+
log('Skipping daemon-state PATCH due to exhausted hook budget');
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
const res = await withTimeout(
|
|
396
|
+
(signal) => fetch(`${RELAY_API_URL}/api/sessions/${session_id}/daemon-state`, {
|
|
397
|
+
method: 'PATCH',
|
|
398
|
+
headers: {
|
|
399
|
+
'Content-Type': 'application/json',
|
|
400
|
+
'Authorization': `Bearer ${RELAY_API_KEY}`
|
|
401
|
+
},
|
|
402
|
+
body: JSON.stringify(updates),
|
|
403
|
+
signal
|
|
404
|
+
}),
|
|
405
|
+
timeoutMs,
|
|
406
|
+
'daemon-state patch'
|
|
407
|
+
);
|
|
408
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
508
409
|
} catch (e) {
|
|
509
410
|
log(`Warning: Failed to update session state: ${e.message}`);
|
|
510
411
|
}
|
|
@@ -513,23 +414,32 @@ const fetchJson = async (url, opts) => {
|
|
|
513
414
|
if (tool_input.__teleportation_away) {
|
|
514
415
|
await updateSessionState({ is_away: true });
|
|
515
416
|
// Log away_mode_changed event to timeline
|
|
516
|
-
if (RELAY_API_URL && RELAY_API_KEY) {
|
|
417
|
+
if (RELAY_API_URL && RELAY_API_KEY && hasBudget()) {
|
|
517
418
|
try {
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
419
|
+
const timeoutMs = getRequestTimeoutMs();
|
|
420
|
+
if (timeoutMs > 0) {
|
|
421
|
+
await withTimeout(
|
|
422
|
+
(signal) => fetch(`${RELAY_API_URL}/api/timeline`, {
|
|
423
|
+
method: 'POST',
|
|
424
|
+
headers: {
|
|
425
|
+
'Content-Type': 'application/json',
|
|
426
|
+
'Authorization': `Bearer ${RELAY_API_KEY}`
|
|
427
|
+
},
|
|
428
|
+
body: JSON.stringify({
|
|
429
|
+
session_id,
|
|
430
|
+
type: 'away_mode_changed',
|
|
431
|
+
source,
|
|
432
|
+
data: {
|
|
433
|
+
is_away: true,
|
|
434
|
+
timestamp: Date.now()
|
|
435
|
+
}
|
|
436
|
+
}),
|
|
437
|
+
signal
|
|
438
|
+
}),
|
|
439
|
+
timeoutMs,
|
|
440
|
+
'away-mode timeline post'
|
|
441
|
+
);
|
|
442
|
+
}
|
|
533
443
|
log(`Logged away_mode_changed (away=true) to timeline`);
|
|
534
444
|
} catch (e) {
|
|
535
445
|
log(`Failed to log away_mode_changed: ${e.message}`);
|
|
@@ -550,23 +460,32 @@ const fetchJson = async (url, opts) => {
|
|
|
550
460
|
if (tool_input.__teleportation_back) {
|
|
551
461
|
await updateSessionState({ is_away: false });
|
|
552
462
|
// Log away_mode_changed event to timeline
|
|
553
|
-
if (RELAY_API_URL && RELAY_API_KEY) {
|
|
463
|
+
if (RELAY_API_URL && RELAY_API_KEY && hasBudget()) {
|
|
554
464
|
try {
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
465
|
+
const timeoutMs = getRequestTimeoutMs();
|
|
466
|
+
if (timeoutMs > 0) {
|
|
467
|
+
await withTimeout(
|
|
468
|
+
(signal) => fetch(`${RELAY_API_URL}/api/timeline`, {
|
|
469
|
+
method: 'POST',
|
|
470
|
+
headers: {
|
|
471
|
+
'Content-Type': 'application/json',
|
|
472
|
+
'Authorization': `Bearer ${RELAY_API_KEY}`
|
|
473
|
+
},
|
|
474
|
+
body: JSON.stringify({
|
|
475
|
+
session_id,
|
|
476
|
+
type: 'away_mode_changed',
|
|
477
|
+
source,
|
|
478
|
+
data: {
|
|
479
|
+
is_away: false,
|
|
480
|
+
timestamp: Date.now()
|
|
481
|
+
}
|
|
482
|
+
}),
|
|
483
|
+
signal
|
|
484
|
+
}),
|
|
485
|
+
timeoutMs,
|
|
486
|
+
'away-mode timeline post'
|
|
487
|
+
);
|
|
488
|
+
}
|
|
570
489
|
log(`Logged away_mode_changed (away=false) to timeline`);
|
|
571
490
|
} catch (e) {
|
|
572
491
|
log(`Failed to log away_mode_changed: ${e.message}`);
|