teleportation-cli 1.0.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/config-loader.mjs +93 -0
- package/.claude/hooks/heartbeat.mjs +331 -0
- package/.claude/hooks/notification.mjs +35 -0
- package/.claude/hooks/permission_request.mjs +307 -0
- package/.claude/hooks/post_tool_use.mjs +137 -0
- package/.claude/hooks/pre_tool_use.mjs +451 -0
- package/.claude/hooks/session-register.mjs +274 -0
- package/.claude/hooks/session_end.mjs +256 -0
- package/.claude/hooks/session_start.mjs +308 -0
- package/.claude/hooks/stop.mjs +277 -0
- package/.claude/hooks/user_prompt_submit.mjs +91 -0
- package/LICENSE +21 -0
- package/README.md +243 -0
- package/lib/auth/api-key.js +110 -0
- package/lib/auth/credentials.js +341 -0
- package/lib/backup/manager.js +461 -0
- package/lib/cli/daemon-commands.js +299 -0
- package/lib/cli/index.js +303 -0
- package/lib/cli/session-commands.js +294 -0
- package/lib/cli/snapshot-commands.js +223 -0
- package/lib/cli/worktree-commands.js +291 -0
- package/lib/config/manager.js +306 -0
- package/lib/daemon/lifecycle.js +336 -0
- package/lib/daemon/pid-manager.js +160 -0
- package/lib/daemon/teleportation-daemon.js +2009 -0
- package/lib/handoff/config.js +102 -0
- package/lib/handoff/example.js +152 -0
- package/lib/handoff/git-handoff.js +351 -0
- package/lib/handoff/handoff.js +277 -0
- package/lib/handoff/index.js +25 -0
- package/lib/handoff/session-state.js +238 -0
- package/lib/install/installer.js +555 -0
- package/lib/machine-coders/claude-code-adapter.js +329 -0
- package/lib/machine-coders/example.js +239 -0
- package/lib/machine-coders/gemini-cli-adapter.js +406 -0
- package/lib/machine-coders/index.js +103 -0
- package/lib/machine-coders/interface.js +168 -0
- package/lib/router/classifier.js +251 -0
- package/lib/router/example.js +92 -0
- package/lib/router/index.js +69 -0
- package/lib/router/mech-llms-client.js +277 -0
- package/lib/router/models.js +188 -0
- package/lib/router/router.js +382 -0
- package/lib/session/cleanup.js +100 -0
- package/lib/session/metadata.js +258 -0
- package/lib/session/mute-checker.js +114 -0
- package/lib/session-registry/manager.js +302 -0
- package/lib/snapshot/manager.js +390 -0
- package/lib/utils/errors.js +166 -0
- package/lib/utils/logger.js +148 -0
- package/lib/utils/retry.js +155 -0
- package/lib/worktree/manager.js +301 -0
- package/package.json +66 -0
- package/teleportation-cli.cjs +2987 -0
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* PermissionRequest Hook
|
|
4
|
+
*
|
|
5
|
+
* This hook fires when Claude Code is about to ask the user for permission.
|
|
6
|
+
* This is the RIGHT place to create remote approvals because:
|
|
7
|
+
* 1. We know Claude Code needs user permission (not auto-approved)
|
|
8
|
+
* 2. We can intercept and handle remotely if user is away
|
|
9
|
+
*
|
|
10
|
+
* Flow:
|
|
11
|
+
* - If user is PRESENT: Let Claude Code show its normal permission dialog
|
|
12
|
+
* - If user is AWAY: Create remote approval and wait for mobile response
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { stdin, stdout, exit, env } from 'node:process';
|
|
16
|
+
import { appendFileSync } from 'node:fs';
|
|
17
|
+
import { readFile } from 'node:fs/promises';
|
|
18
|
+
import { fileURLToPath } from 'url';
|
|
19
|
+
import { dirname, join } from 'path';
|
|
20
|
+
import { homedir } from 'os';
|
|
21
|
+
|
|
22
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
23
|
+
const __dirname = dirname(__filename);
|
|
24
|
+
|
|
25
|
+
// Lazy-load metadata extraction
|
|
26
|
+
let extractSessionMetadata = null;
|
|
27
|
+
async function getSessionMetadata(cwd) {
|
|
28
|
+
if (!extractSessionMetadata) {
|
|
29
|
+
try {
|
|
30
|
+
// Try multiple paths for the metadata module
|
|
31
|
+
const possiblePaths = [
|
|
32
|
+
join(__dirname, '..', '..', 'lib', 'session', 'metadata.js'),
|
|
33
|
+
join(homedir(), '.teleportation', 'lib', 'session', 'metadata.js'),
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
for (const path of possiblePaths) {
|
|
37
|
+
try {
|
|
38
|
+
const mod = await import('file://' + path);
|
|
39
|
+
extractSessionMetadata = mod.extractSessionMetadata;
|
|
40
|
+
break;
|
|
41
|
+
} catch (e) {
|
|
42
|
+
// Try next path
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
} catch (e) {
|
|
46
|
+
return {};
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (!extractSessionMetadata) return {};
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
return await extractSessionMetadata(cwd);
|
|
54
|
+
} catch (e) {
|
|
55
|
+
return {};
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Load version info from ~/.teleportation/version.json
|
|
60
|
+
async function loadVersionInfo() {
|
|
61
|
+
try {
|
|
62
|
+
const versionFile = join(homedir(), '.teleportation', 'version.json');
|
|
63
|
+
const content = await readFile(versionFile, 'utf8');
|
|
64
|
+
return JSON.parse(content);
|
|
65
|
+
} catch (e) {
|
|
66
|
+
// Version file may not exist on first run - this is expected
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const readStdin = () => new Promise((resolve, reject) => {
|
|
72
|
+
let data = '';
|
|
73
|
+
stdin.setEncoding('utf8');
|
|
74
|
+
stdin.on('data', chunk => data += chunk);
|
|
75
|
+
stdin.on('end', () => resolve(data));
|
|
76
|
+
stdin.on('error', reject);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
|
|
80
|
+
|
|
81
|
+
const isValidSessionId = (id) => {
|
|
82
|
+
return id && /^[a-f0-9-]{36}$/i.test(id);
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const fetchJson = async (url, opts) => {
|
|
86
|
+
const res = await fetch(url, opts);
|
|
87
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
88
|
+
return res.json();
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
(async () => {
|
|
92
|
+
const hookLogFile = env.TELEPORTATION_HOOK_LOG || '/tmp/teleportation-hook.log';
|
|
93
|
+
const log = (msg) => {
|
|
94
|
+
const timestamp = new Date().toISOString();
|
|
95
|
+
try {
|
|
96
|
+
appendFileSync(hookLogFile, `[${timestamp}] [PermissionRequest] ${msg}\n`);
|
|
97
|
+
} catch (e) {}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
log('=== PermissionRequest Hook invoked ===');
|
|
101
|
+
|
|
102
|
+
const raw = await readStdin();
|
|
103
|
+
let input;
|
|
104
|
+
try {
|
|
105
|
+
input = JSON.parse(raw || '{}');
|
|
106
|
+
} catch (e) {
|
|
107
|
+
log(`ERROR: Invalid JSON: ${e.message}`);
|
|
108
|
+
return exit(0);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const { session_id, tool_name, tool_input, cwd } = input || {};
|
|
112
|
+
log(`Session: ${session_id}, Tool: ${tool_name}, CWD: ${cwd}`);
|
|
113
|
+
|
|
114
|
+
// Validate session_id
|
|
115
|
+
if (!isValidSessionId(session_id)) {
|
|
116
|
+
log(`ERROR: Invalid session_id format: ${session_id}`);
|
|
117
|
+
return exit(0);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Load config
|
|
121
|
+
let config;
|
|
122
|
+
try {
|
|
123
|
+
const { loadConfig } = await import('./config-loader.mjs');
|
|
124
|
+
config = await loadConfig();
|
|
125
|
+
} catch (e) {
|
|
126
|
+
config = {
|
|
127
|
+
relayApiUrl: env.RELAY_API_URL || '',
|
|
128
|
+
relayApiKey: env.RELAY_API_KEY || '',
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const RELAY_API_URL = env.RELAY_API_URL || config.relayApiUrl || '';
|
|
133
|
+
const RELAY_API_KEY = env.RELAY_API_KEY || config.relayApiKey || '';
|
|
134
|
+
const POLLING_INTERVAL_MS = parseInt(env.APPROVAL_POLL_INTERVAL_MS || '2000', 10);
|
|
135
|
+
const APPROVAL_TIMEOUT_MS = parseInt(env.APPROVAL_TIMEOUT_MS || '300000', 10); // 5 min default
|
|
136
|
+
|
|
137
|
+
if (!RELAY_API_URL || !RELAY_API_KEY) {
|
|
138
|
+
log('No relay config - letting Claude Code handle permission locally');
|
|
139
|
+
return exit(0);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Check if session is in "away" mode
|
|
143
|
+
let isAway = false;
|
|
144
|
+
try {
|
|
145
|
+
const state = await fetchJson(`${RELAY_API_URL}/api/sessions/${session_id}/daemon-state`, {
|
|
146
|
+
headers: { 'Authorization': `Bearer ${RELAY_API_KEY}` }
|
|
147
|
+
});
|
|
148
|
+
isAway = !!state.is_away;
|
|
149
|
+
log(`Session away status: ${isAway}`);
|
|
150
|
+
} catch (e) {
|
|
151
|
+
// Fail-safe: if relay is down, assume user is present (safer default)
|
|
152
|
+
const failSafe = env.AWAY_CHECK_FAIL_SAFE || 'present';
|
|
153
|
+
isAway = failSafe === 'away';
|
|
154
|
+
log(`Could not check away status: ${e.message} - using fail-safe: ${failSafe}`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// If user is NOT away, don't create an approval - let Claude Code handle it locally
|
|
158
|
+
// This prevents stale approvals from appearing in the mobile UI
|
|
159
|
+
if (!isAway) {
|
|
160
|
+
log('User is present - letting Claude Code show permission dialog (no remote approval created)');
|
|
161
|
+
return exit(0);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// User is AWAY - create remote approval and poll for decision
|
|
165
|
+
log(`Creating remote approval for ${tool_name}...`);
|
|
166
|
+
|
|
167
|
+
// Extract session metadata (project name, hostname, branch, etc.)
|
|
168
|
+
let meta = {};
|
|
169
|
+
try {
|
|
170
|
+
const workingDir = cwd || process.cwd();
|
|
171
|
+
meta = await getSessionMetadata(workingDir);
|
|
172
|
+
meta.session_id = session_id;
|
|
173
|
+
|
|
174
|
+
// Add version info
|
|
175
|
+
const versionInfo = await loadVersionInfo();
|
|
176
|
+
if (versionInfo) {
|
|
177
|
+
meta.teleportation_version = versionInfo.version;
|
|
178
|
+
meta.protocol_version = versionInfo.protocol_version;
|
|
179
|
+
} else {
|
|
180
|
+
log('Warning: Version info not found (version.json may not exist yet)');
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Log metadata status
|
|
184
|
+
if (!meta.project_name && !meta.hostname) {
|
|
185
|
+
log('Warning: Metadata extraction returned empty - metadata module may not be installed');
|
|
186
|
+
} else {
|
|
187
|
+
log(`Session metadata: project=${meta.project_name}, hostname=${meta.hostname}, branch=${meta.current_branch}`);
|
|
188
|
+
}
|
|
189
|
+
} catch (e) {
|
|
190
|
+
log(`Warning: Failed to extract metadata: ${e.message}`);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Invalidate old pending approvals
|
|
194
|
+
try {
|
|
195
|
+
await fetchJson(`${RELAY_API_URL}/api/approvals/invalidate`, {
|
|
196
|
+
method: 'POST',
|
|
197
|
+
headers: {
|
|
198
|
+
'Content-Type': 'application/json',
|
|
199
|
+
'Authorization': `Bearer ${RELAY_API_KEY}`
|
|
200
|
+
},
|
|
201
|
+
body: JSON.stringify({ session_id, reason: 'New permission request' })
|
|
202
|
+
});
|
|
203
|
+
} catch (e) {
|
|
204
|
+
log(`Warning: Failed to invalidate old approvals: ${e.message}`);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Create approval request with metadata
|
|
208
|
+
let approvalId;
|
|
209
|
+
try {
|
|
210
|
+
const created = await fetchJson(`${RELAY_API_URL}/api/approvals`, {
|
|
211
|
+
method: 'POST',
|
|
212
|
+
headers: {
|
|
213
|
+
'Content-Type': 'application/json',
|
|
214
|
+
'Authorization': `Bearer ${RELAY_API_KEY}`
|
|
215
|
+
},
|
|
216
|
+
body: JSON.stringify({ session_id, tool_name, tool_input, meta })
|
|
217
|
+
});
|
|
218
|
+
approvalId = created.id;
|
|
219
|
+
log(`Approval created: ${approvalId}`);
|
|
220
|
+
} catch (e) {
|
|
221
|
+
log(`ERROR creating approval: ${e.message}`);
|
|
222
|
+
return exit(0); // Let Claude Code handle it
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Poll for remote approval decision
|
|
226
|
+
log('Polling for remote approval decision...');
|
|
227
|
+
const AUTO_AWAY_TIMEOUT_MS = parseInt(env.AUTO_AWAY_TIMEOUT_MS || '300000', 10); // 5 min default
|
|
228
|
+
const startTime = Date.now();
|
|
229
|
+
let hasSetAutoAway = false;
|
|
230
|
+
let consecutiveFailures = 0;
|
|
231
|
+
const MAX_CONSECUTIVE_FAILURES = 5;
|
|
232
|
+
|
|
233
|
+
const deadline = Date.now() + APPROVAL_TIMEOUT_MS;
|
|
234
|
+
while (Date.now() < deadline) {
|
|
235
|
+
try {
|
|
236
|
+
const status = await fetchJson(`${RELAY_API_URL}/api/approvals/${approvalId}`, {
|
|
237
|
+
headers: { 'Authorization': `Bearer ${RELAY_API_KEY}` }
|
|
238
|
+
});
|
|
239
|
+
consecutiveFailures = 0; // Reset on success
|
|
240
|
+
|
|
241
|
+
if (status.status === 'allowed') {
|
|
242
|
+
log('Remote approval: ALLOWED');
|
|
243
|
+
const out = {
|
|
244
|
+
hookSpecificOutput: {
|
|
245
|
+
hookEventName: 'PermissionRequest',
|
|
246
|
+
permissionDecision: 'allow',
|
|
247
|
+
permissionDecisionReason: 'Approved remotely via Teleportation'
|
|
248
|
+
},
|
|
249
|
+
suppressOutput: true
|
|
250
|
+
};
|
|
251
|
+
stdout.write(JSON.stringify(out));
|
|
252
|
+
return exit(0);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (status.status === 'denied') {
|
|
256
|
+
log('Remote approval: DENIED');
|
|
257
|
+
const out = {
|
|
258
|
+
hookSpecificOutput: {
|
|
259
|
+
hookEventName: 'PermissionRequest',
|
|
260
|
+
permissionDecision: 'deny',
|
|
261
|
+
permissionDecisionReason: 'Denied remotely via Teleportation'
|
|
262
|
+
},
|
|
263
|
+
suppressOutput: true
|
|
264
|
+
};
|
|
265
|
+
stdout.write(JSON.stringify(out));
|
|
266
|
+
return exit(0);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (status.status === 'invalidated') {
|
|
270
|
+
log('Approval was invalidated - letting Claude Code handle');
|
|
271
|
+
return exit(0);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Auto-set away after timeout (if not already away)
|
|
275
|
+
if (!hasSetAutoAway && (Date.now() - startTime) > AUTO_AWAY_TIMEOUT_MS) {
|
|
276
|
+
const timeoutMinutes = Math.round(AUTO_AWAY_TIMEOUT_MS / 1000 / 60);
|
|
277
|
+
log(`Approval waiting >${timeoutMinutes} minutes - auto-setting away mode`);
|
|
278
|
+
try {
|
|
279
|
+
await fetchJson(`${RELAY_API_URL}/api/sessions/${session_id}/daemon-state`, {
|
|
280
|
+
method: 'PATCH',
|
|
281
|
+
headers: {
|
|
282
|
+
'Content-Type': 'application/json',
|
|
283
|
+
'Authorization': `Bearer ${RELAY_API_KEY}`
|
|
284
|
+
},
|
|
285
|
+
body: JSON.stringify({ is_away: true })
|
|
286
|
+
});
|
|
287
|
+
hasSetAutoAway = true;
|
|
288
|
+
} catch (e) {
|
|
289
|
+
log(`Failed to auto-set away: ${e.message}`);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
} catch (e) {
|
|
293
|
+
consecutiveFailures++;
|
|
294
|
+
if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES) {
|
|
295
|
+
log(`Too many consecutive failures (${consecutiveFailures}/${MAX_CONSECUTIVE_FAILURES}) - aborting poll`);
|
|
296
|
+
return exit(0);
|
|
297
|
+
}
|
|
298
|
+
log(`Poll error (${consecutiveFailures}/${MAX_CONSECUTIVE_FAILURES}): ${e.message}`);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
await sleep(POLLING_INTERVAL_MS);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Timeout - let Claude Code handle it
|
|
305
|
+
log('Approval timeout - letting Claude Code handle');
|
|
306
|
+
return exit(0);
|
|
307
|
+
})();
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* PostToolUse Hook
|
|
4
|
+
*
|
|
5
|
+
* This hook fires AFTER a tool has been executed.
|
|
6
|
+
* If we get here, the tool was approved (either auto or manually) and ran.
|
|
7
|
+
*
|
|
8
|
+
* Purpose: Record tool executions to the timeline for activity history.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { stdin, stdout, exit, env } from 'node:process';
|
|
12
|
+
import { appendFileSync } from 'node:fs';
|
|
13
|
+
|
|
14
|
+
const readStdin = () => new Promise((resolve, reject) => {
|
|
15
|
+
let data = '';
|
|
16
|
+
stdin.setEncoding('utf8');
|
|
17
|
+
stdin.on('data', chunk => data += chunk);
|
|
18
|
+
stdin.on('end', () => resolve(data));
|
|
19
|
+
stdin.on('error', reject);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const isValidSessionId = (id) => {
|
|
23
|
+
return id && /^[a-f0-9-]{36}$/i.test(id);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const TIMELINE_OUTPUT_PREVIEW_MAX_LENGTH = 500;
|
|
27
|
+
|
|
28
|
+
const fetchJson = async (url, opts) => {
|
|
29
|
+
const res = await fetch(url, opts);
|
|
30
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
31
|
+
return res.json();
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
(async () => {
|
|
35
|
+
const hookLogFile = env.TELEPORTATION_HOOK_LOG || '/tmp/teleportation-hook.log';
|
|
36
|
+
const log = (msg) => {
|
|
37
|
+
const timestamp = new Date().toISOString();
|
|
38
|
+
try {
|
|
39
|
+
appendFileSync(hookLogFile, `[${timestamp}] [PostToolUse] ${msg}\n`);
|
|
40
|
+
} catch (e) {}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
log('=== PostToolUse Hook invoked ===');
|
|
44
|
+
|
|
45
|
+
const raw = await readStdin();
|
|
46
|
+
let input;
|
|
47
|
+
try {
|
|
48
|
+
input = JSON.parse(raw || '{}');
|
|
49
|
+
} catch (e) {
|
|
50
|
+
log(`ERROR: Invalid JSON: ${e.message}`);
|
|
51
|
+
return exit(0);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const { session_id, tool_name, tool_input, tool_output } = input || {};
|
|
55
|
+
log(`Session: ${session_id}, Tool: ${tool_name}`);
|
|
56
|
+
|
|
57
|
+
// Validate session_id
|
|
58
|
+
if (!isValidSessionId(session_id)) {
|
|
59
|
+
log(`ERROR: Invalid session_id format: ${session_id}`);
|
|
60
|
+
return exit(0);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Load config
|
|
64
|
+
let config;
|
|
65
|
+
try {
|
|
66
|
+
const { loadConfig } = await import('./config-loader.mjs');
|
|
67
|
+
config = await loadConfig();
|
|
68
|
+
} catch (e) {
|
|
69
|
+
config = {
|
|
70
|
+
relayApiUrl: env.RELAY_API_URL || '',
|
|
71
|
+
relayApiKey: env.RELAY_API_KEY || '',
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const RELAY_API_URL = env.RELAY_API_URL || config.relayApiUrl || '';
|
|
76
|
+
const RELAY_API_KEY = env.RELAY_API_KEY || config.relayApiKey || '';
|
|
77
|
+
|
|
78
|
+
if (!RELAY_API_URL || !RELAY_API_KEY || !session_id) {
|
|
79
|
+
log('No relay config or session - skipping timeline log');
|
|
80
|
+
return exit(0);
|
|
81
|
+
}
|
|
82
|
+
|
|
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
|
|
85
|
+
try {
|
|
86
|
+
await fetchJson(`${RELAY_API_URL}/api/approvals/invalidate`, {
|
|
87
|
+
method: 'POST',
|
|
88
|
+
headers: {
|
|
89
|
+
'Content-Type': 'application/json',
|
|
90
|
+
'Authorization': `Bearer ${RELAY_API_KEY}`
|
|
91
|
+
},
|
|
92
|
+
body: JSON.stringify({
|
|
93
|
+
session_id,
|
|
94
|
+
reason: `Tool ${tool_name} executed (auto-approved by Claude Code)`
|
|
95
|
+
})
|
|
96
|
+
});
|
|
97
|
+
log(`Cleared pending approvals after tool execution: ${tool_name}`);
|
|
98
|
+
} catch (e) {
|
|
99
|
+
log(`Failed to clear pending approvals: ${e.message}`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Record tool execution to timeline
|
|
103
|
+
try {
|
|
104
|
+
await fetchJson(`${RELAY_API_URL}/api/timeline`, {
|
|
105
|
+
method: 'POST',
|
|
106
|
+
headers: {
|
|
107
|
+
'Content-Type': 'application/json',
|
|
108
|
+
'Authorization': `Bearer ${RELAY_API_KEY}`
|
|
109
|
+
},
|
|
110
|
+
body: JSON.stringify({
|
|
111
|
+
session_id,
|
|
112
|
+
type: 'tool_executed',
|
|
113
|
+
data: {
|
|
114
|
+
tool_name,
|
|
115
|
+
tool_input,
|
|
116
|
+
// Include truncated output for context
|
|
117
|
+
tool_output_preview: (() => {
|
|
118
|
+
if (!tool_output) return null;
|
|
119
|
+
try {
|
|
120
|
+
const stringified = JSON.stringify(tool_output);
|
|
121
|
+
const truncated = stringified.slice(0, TIMELINE_OUTPUT_PREVIEW_MAX_LENGTH);
|
|
122
|
+
return stringified.length > TIMELINE_OUTPUT_PREVIEW_MAX_LENGTH ? truncated + '...' : truncated;
|
|
123
|
+
} catch (e) {
|
|
124
|
+
return '[Unserializable output]';
|
|
125
|
+
}
|
|
126
|
+
})()
|
|
127
|
+
}
|
|
128
|
+
})
|
|
129
|
+
});
|
|
130
|
+
log(`Recorded tool execution: ${tool_name}`);
|
|
131
|
+
} catch (e) {
|
|
132
|
+
log(`Failed to record to timeline: ${e.message}`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// PostToolUse hooks don't need to output anything
|
|
136
|
+
return exit(0);
|
|
137
|
+
})();
|