teleportation-cli 1.2.1 → 1.3.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.
|
@@ -196,7 +196,7 @@ function updateSessionMarker(sessionId) {
|
|
|
196
196
|
].filter(Boolean);
|
|
197
197
|
|
|
198
198
|
// Import fs/promises once outside the loop
|
|
199
|
-
const { access } = await import('fs/promises');
|
|
199
|
+
const { access, readFile } = await import('fs/promises');
|
|
200
200
|
let daemonScript = null;
|
|
201
201
|
for (const location of possibleLocations) {
|
|
202
202
|
try {
|
|
@@ -217,6 +217,20 @@ function updateSessionMarker(sessionId) {
|
|
|
217
217
|
return exit(0);
|
|
218
218
|
}
|
|
219
219
|
|
|
220
|
+
// Staleness check: detect outdated daemon that imports removed heartbeat-manager module
|
|
221
|
+
try {
|
|
222
|
+
const daemonSource = await readFile(daemonScript, 'utf-8');
|
|
223
|
+
if (/require\s*\(\s*['"]\.\/heartbeat-manager(?:\.js)?['"]\s*\)|from\s+['"]\.\/heartbeat-manager(?:\.js)?['"]/.test(daemonSource)) {
|
|
224
|
+
console.error('[SessionStart] ⚠️ Installed daemon is stale (imports removed heartbeat-manager module).');
|
|
225
|
+
console.error('[SessionStart] Run: teleportation install-hooks');
|
|
226
|
+
// Continue without daemon rather than crash
|
|
227
|
+
try { process.stdout.write(JSON.stringify({ suppressOutput: true })); } catch {}
|
|
228
|
+
return exit(0);
|
|
229
|
+
}
|
|
230
|
+
} catch {
|
|
231
|
+
// If we can't read the file, let agentStart handle the error
|
|
232
|
+
}
|
|
233
|
+
|
|
220
234
|
// Start daemon using agent-runtime
|
|
221
235
|
// This handles retries, health checks, and platform-native service installation
|
|
222
236
|
try {
|
|
@@ -229,6 +229,10 @@ const OUTPUT_PREVIEW_LONG = 1000; // Full output displays
|
|
|
229
229
|
const heartbeatState = new Map();
|
|
230
230
|
let lastHeartbeatTime = 0;
|
|
231
231
|
|
|
232
|
+
// Track which sessions have had their first heartbeat failure logged.
|
|
233
|
+
// Prevents log spam while ensuring operators see at least one failure per session.
|
|
234
|
+
const heartbeatFailureLogged = new Set();
|
|
235
|
+
|
|
232
236
|
// In-memory registry of active teleportation sessions handled by this daemon
|
|
233
237
|
const sessions = new Map();
|
|
234
238
|
|
|
@@ -265,6 +269,7 @@ setInterval(() => {
|
|
|
265
269
|
sessions.delete(sessionId);
|
|
266
270
|
sessionActivity.delete(sessionId);
|
|
267
271
|
heartbeatState.delete(sessionId);
|
|
272
|
+
heartbeatFailureLogged.delete(sessionId);
|
|
268
273
|
sessionCleanedCount++;
|
|
269
274
|
|
|
270
275
|
if (process.env.DEBUG) {
|
|
@@ -1037,7 +1042,8 @@ async function ackAndLogCancellation(session_id, message, triggeredBy = {}) {
|
|
|
1037
1042
|
try {
|
|
1038
1043
|
await fetch(`${RELAY_API_URL}/api/messages/${encodeURIComponent(message.id)}/ack`, {
|
|
1039
1044
|
method: 'POST',
|
|
1040
|
-
headers: { 'Authorization': `Bearer ${RELAY_API_KEY}` }
|
|
1045
|
+
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${RELAY_API_KEY}` },
|
|
1046
|
+
body: JSON.stringify({ session_id })
|
|
1041
1047
|
});
|
|
1042
1048
|
logDebug(`[daemon] Acknowledged message ${message.id}`);
|
|
1043
1049
|
} catch (ackError) {
|
|
@@ -1098,7 +1104,8 @@ async function handleInboxMessage(session_id, message) {
|
|
|
1098
1104
|
logWarn(`[daemon] Failed to check is_away state, skipping auto-continue for safety`);
|
|
1099
1105
|
await fetch(`${RELAY_API_URL}/api/messages/${encodeURIComponent(message.id)}/ack`, {
|
|
1100
1106
|
method: 'POST',
|
|
1101
|
-
headers: { 'Authorization': `Bearer ${RELAY_API_KEY}` }
|
|
1107
|
+
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${RELAY_API_KEY}` },
|
|
1108
|
+
body: JSON.stringify({ session_id })
|
|
1102
1109
|
}).catch(() => {});
|
|
1103
1110
|
return;
|
|
1104
1111
|
}
|
|
@@ -1131,7 +1138,8 @@ async function handleInboxMessage(session_id, message) {
|
|
|
1131
1138
|
// Still acknowledge the message to prevent re-delivery
|
|
1132
1139
|
await fetch(`${RELAY_API_URL}/api/messages/${encodeURIComponent(message.id)}/ack`, {
|
|
1133
1140
|
method: 'POST',
|
|
1134
|
-
headers: { 'Authorization': `Bearer ${RELAY_API_KEY}` }
|
|
1141
|
+
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${RELAY_API_KEY}` },
|
|
1142
|
+
body: JSON.stringify({ session_id })
|
|
1135
1143
|
});
|
|
1136
1144
|
return;
|
|
1137
1145
|
}
|
|
@@ -1355,8 +1363,10 @@ async function handleInboxMessage(session_id, message) {
|
|
|
1355
1363
|
const ackResponse = await fetch(`${RELAY_API_URL}/api/messages/${encodeURIComponent(message.id)}/ack`, {
|
|
1356
1364
|
method: 'POST',
|
|
1357
1365
|
headers: {
|
|
1366
|
+
'Content-Type': 'application/json',
|
|
1358
1367
|
'Authorization': `Bearer ${RELAY_API_KEY}`
|
|
1359
|
-
}
|
|
1368
|
+
},
|
|
1369
|
+
body: JSON.stringify({ session_id, is_user_prompt: true })
|
|
1360
1370
|
});
|
|
1361
1371
|
|
|
1362
1372
|
if (ackResponse.ok) {
|
|
@@ -1715,11 +1725,12 @@ function cleanupOldExecutions() {
|
|
|
1715
1725
|
console.log(`[daemon] Cleaned up ${removed} old execution(s) from cache`);
|
|
1716
1726
|
}
|
|
1717
1727
|
|
|
1718
|
-
// Clean up heartbeatState for sessions that no longer exist
|
|
1728
|
+
// Clean up heartbeatState and heartbeatFailureLogged for sessions that no longer exist
|
|
1719
1729
|
let heartbeatRemoved = 0;
|
|
1720
1730
|
for (const sessionId of heartbeatState.keys()) {
|
|
1721
1731
|
if (!sessions.has(sessionId)) {
|
|
1722
1732
|
heartbeatState.delete(sessionId);
|
|
1733
|
+
heartbeatFailureLogged.delete(sessionId);
|
|
1723
1734
|
heartbeatRemoved++;
|
|
1724
1735
|
}
|
|
1725
1736
|
}
|
|
@@ -2868,7 +2879,7 @@ async function main() {
|
|
|
2868
2879
|
if (isShuttingDown || sessions.size === 0) return;
|
|
2869
2880
|
for (const [sessionId] of sessions) {
|
|
2870
2881
|
try {
|
|
2871
|
-
await fetch(`${RELAY_API_URL}/api/sessions/${encodeURIComponent(sessionId)}/heartbeat`, {
|
|
2882
|
+
const hbResponse = await fetch(`${RELAY_API_URL}/api/sessions/${encodeURIComponent(sessionId)}/heartbeat`, {
|
|
2872
2883
|
method: 'POST',
|
|
2873
2884
|
headers: {
|
|
2874
2885
|
'Content-Type': 'application/json',
|
|
@@ -2877,8 +2888,23 @@ async function main() {
|
|
|
2877
2888
|
body: JSON.stringify({ timestamp: Date.now() }),
|
|
2878
2889
|
signal: AbortSignal.timeout(5000)
|
|
2879
2890
|
});
|
|
2891
|
+
if (!hbResponse.ok) {
|
|
2892
|
+
const errMsg = `HTTP ${hbResponse.status}`;
|
|
2893
|
+
if (!heartbeatFailureLogged.has(sessionId)) {
|
|
2894
|
+
heartbeatFailureLogged.add(sessionId);
|
|
2895
|
+
console.warn(`[daemon] Heartbeat rejected for ${sessionId}: ${errMsg} (further failures for this session suppressed unless DEBUG is set)`);
|
|
2896
|
+
} else if (process.env.DEBUG) {
|
|
2897
|
+
console.error(`[daemon] Heartbeat rejected for ${sessionId}: ${errMsg}`);
|
|
2898
|
+
}
|
|
2899
|
+
}
|
|
2880
2900
|
} catch (err) {
|
|
2881
|
-
|
|
2901
|
+
// Always log the first heartbeat failure per session so operators
|
|
2902
|
+
// know heartbeats are not reaching the relay. Subsequent failures
|
|
2903
|
+
// for the same session are only logged in DEBUG mode to avoid spam.
|
|
2904
|
+
if (!heartbeatFailureLogged.has(sessionId)) {
|
|
2905
|
+
heartbeatFailureLogged.add(sessionId);
|
|
2906
|
+
console.warn(`[daemon] Heartbeat failed for ${sessionId}: ${err.message} (further failures for this session suppressed unless DEBUG is set)`);
|
|
2907
|
+
} else if (process.env.DEBUG) {
|
|
2882
2908
|
console.error(`[daemon] Heartbeat failed for ${sessionId}: ${err.message}`);
|
|
2883
2909
|
}
|
|
2884
2910
|
}
|
package/lib/install/installer.js
CHANGED
|
@@ -287,11 +287,11 @@ export async function installGeminiHooks(sourceGeminiHooksDir) {
|
|
|
287
287
|
settings.hooks = settings.hooks || {};
|
|
288
288
|
|
|
289
289
|
const hooksConfig = {
|
|
290
|
-
BeforeTool: [{ command: `
|
|
291
|
-
AfterTool: [{ command: `
|
|
292
|
-
SessionStart: [{ command: `
|
|
293
|
-
AfterAgent: [{ command: `
|
|
294
|
-
SessionEnd: [{ command: `
|
|
290
|
+
BeforeTool: [{ command: `bun ${join(destHooksDir, 'before_tool.mjs')}`, timeout: 65000 }],
|
|
291
|
+
AfterTool: [{ command: `bun ${join(destHooksDir, 'after_tool.mjs')}`, timeout: 10000 }],
|
|
292
|
+
SessionStart: [{ command: `bun ${join(destHooksDir, 'session_start.mjs')}`, timeout: 15000 }],
|
|
293
|
+
AfterAgent: [{ command: `bun ${join(destHooksDir, 'after_agent.mjs')}`, timeout: 15000 }],
|
|
294
|
+
SessionEnd: [{ command: `bun ${join(destHooksDir, 'session_end.mjs')}`, timeout: 10000 }]
|
|
295
295
|
};
|
|
296
296
|
|
|
297
297
|
// Standardize: Ensure BeforeAgent is also set if needed (parity with Claude's UserPromptSubmit)
|
|
@@ -316,7 +316,10 @@ export async function installDaemon() {
|
|
|
316
316
|
|
|
317
317
|
const daemonFiles = [
|
|
318
318
|
'teleportation-daemon.js',
|
|
319
|
-
'lifecycle.js'
|
|
319
|
+
'lifecycle.js',
|
|
320
|
+
'task-executor-v2.js',
|
|
321
|
+
'transcript-ingestion.js',
|
|
322
|
+
'timeline-analyzer.js',
|
|
320
323
|
];
|
|
321
324
|
|
|
322
325
|
const installed = [];
|
|
@@ -352,7 +355,7 @@ export async function installDaemon() {
|
|
|
352
355
|
|
|
353
356
|
/**
|
|
354
357
|
* Copy daemon dependency modules to ~/.teleportation/
|
|
355
|
-
* This includes machine-coders and
|
|
358
|
+
* This includes machine-coders, router, utils, and auth
|
|
356
359
|
*/
|
|
357
360
|
export async function installDaemonModules() {
|
|
358
361
|
const sourceLibDir = join(getTeleportationDir(), 'lib');
|
|
@@ -377,6 +380,19 @@ export async function installDaemonModules() {
|
|
|
377
380
|
'models.js',
|
|
378
381
|
'mech-llms-client.js'
|
|
379
382
|
]
|
|
383
|
+
},
|
|
384
|
+
{
|
|
385
|
+
name: 'utils',
|
|
386
|
+
files: [
|
|
387
|
+
'logger.js',
|
|
388
|
+
'log-sanitizer.js'
|
|
389
|
+
]
|
|
390
|
+
},
|
|
391
|
+
{
|
|
392
|
+
name: 'auth',
|
|
393
|
+
files: [
|
|
394
|
+
'credentials.js'
|
|
395
|
+
]
|
|
380
396
|
}
|
|
381
397
|
];
|
|
382
398
|
|
|
@@ -513,42 +529,56 @@ export async function createSettings() {
|
|
|
513
529
|
matcher: ".*",
|
|
514
530
|
hooks: [{
|
|
515
531
|
type: "command",
|
|
516
|
-
command: `
|
|
532
|
+
command: `bun ${join(projectHooksDir, 'pre_tool_use.mjs')}`
|
|
517
533
|
}]
|
|
518
534
|
}],
|
|
519
535
|
Stop: [{
|
|
520
536
|
matcher: ".*",
|
|
521
537
|
hooks: [{
|
|
522
538
|
type: "command",
|
|
523
|
-
command: `
|
|
539
|
+
command: `bun ${join(projectHooksDir, 'stop.mjs')}`
|
|
524
540
|
}]
|
|
525
541
|
}],
|
|
526
542
|
SessionStart: [{
|
|
527
543
|
matcher: ".*",
|
|
528
544
|
hooks: [{
|
|
529
545
|
type: "command",
|
|
530
|
-
command: `
|
|
546
|
+
command: `bun ${join(projectHooksDir, 'session_start.mjs')}`
|
|
531
547
|
}]
|
|
532
548
|
}],
|
|
533
549
|
SessionEnd: [{
|
|
534
550
|
matcher: ".*",
|
|
535
551
|
hooks: [{
|
|
536
552
|
type: "command",
|
|
537
|
-
command: `
|
|
553
|
+
command: `bun ${join(projectHooksDir, 'session_end.mjs')}`
|
|
538
554
|
}]
|
|
539
555
|
}],
|
|
540
556
|
Notification: [{
|
|
541
557
|
matcher: ".*",
|
|
542
558
|
hooks: [{
|
|
543
559
|
type: "command",
|
|
544
|
-
command: `
|
|
560
|
+
command: `bun ${join(projectHooksDir, 'notification.mjs')}`
|
|
545
561
|
}]
|
|
546
562
|
}],
|
|
547
563
|
UserPromptSubmit: [{
|
|
548
564
|
matcher: ".*",
|
|
549
565
|
hooks: [{
|
|
550
566
|
type: "command",
|
|
551
|
-
command: `
|
|
567
|
+
command: `bun ${join(projectHooksDir, 'user_prompt_submit.mjs')}`
|
|
568
|
+
}]
|
|
569
|
+
}],
|
|
570
|
+
PermissionRequest: [{
|
|
571
|
+
matcher: ".*",
|
|
572
|
+
hooks: [{
|
|
573
|
+
type: "command",
|
|
574
|
+
command: `bun ${join(projectHooksDir, 'permission_request.mjs')}`
|
|
575
|
+
}]
|
|
576
|
+
}],
|
|
577
|
+
PostToolUse: [{
|
|
578
|
+
matcher: ".*",
|
|
579
|
+
hooks: [{
|
|
580
|
+
type: "command",
|
|
581
|
+
command: `bun ${join(projectHooksDir, 'post_tool_use.mjs')}`
|
|
552
582
|
}]
|
|
553
583
|
}]
|
|
554
584
|
}
|
package/package.json
CHANGED
package/teleportation-cli.cjs
CHANGED
|
@@ -7,7 +7,7 @@ const path = require('path');
|
|
|
7
7
|
const { execSync } = require('child_process');
|
|
8
8
|
const os = require('os');
|
|
9
9
|
|
|
10
|
-
const CLI_VERSION = '1.
|
|
10
|
+
const CLI_VERSION = '1.2.2';
|
|
11
11
|
const HOME_DIR = os.homedir();
|
|
12
12
|
// Teleportation project directory (for development)
|
|
13
13
|
// In production, hooks will be installed globally
|
|
@@ -1295,7 +1295,63 @@ async function commandTest() {
|
|
|
1295
1295
|
console.log(c.red(' ❌ FAIL - Hook execution error\n'));
|
|
1296
1296
|
failed++;
|
|
1297
1297
|
}
|
|
1298
|
-
|
|
1298
|
+
|
|
1299
|
+
// Test 6: Approval Round-Trip
|
|
1300
|
+
console.log(c.yellow('Test 6: Approval Round-Trip'));
|
|
1301
|
+
if (creds.RELAY_API_KEY && relayUrl) {
|
|
1302
|
+
try {
|
|
1303
|
+
// Create a test approval
|
|
1304
|
+
const createRes = await fetch(`${relayUrl}/api/approvals`, {
|
|
1305
|
+
method: 'POST',
|
|
1306
|
+
headers: {
|
|
1307
|
+
'Content-Type': 'application/json',
|
|
1308
|
+
'Authorization': `Bearer ${creds.RELAY_API_KEY}`
|
|
1309
|
+
},
|
|
1310
|
+
body: JSON.stringify({
|
|
1311
|
+
session_id: 'test-smoke-' + Date.now(),
|
|
1312
|
+
tool_name: 'Read',
|
|
1313
|
+
tool_input: { file_path: '/tmp/smoke-test.txt' }
|
|
1314
|
+
}),
|
|
1315
|
+
signal: AbortSignal.timeout(10000)
|
|
1316
|
+
});
|
|
1317
|
+
|
|
1318
|
+
if (!createRes.ok) throw new Error(`Create failed: ${createRes.status}`);
|
|
1319
|
+
const { id: approvalId } = await createRes.json();
|
|
1320
|
+
|
|
1321
|
+
// Approve it
|
|
1322
|
+
const decideRes = await fetch(`${relayUrl}/api/approvals/${approvalId}/decision`, {
|
|
1323
|
+
method: 'POST',
|
|
1324
|
+
headers: {
|
|
1325
|
+
'Content-Type': 'application/json',
|
|
1326
|
+
'Authorization': `Bearer ${creds.RELAY_API_KEY}`
|
|
1327
|
+
},
|
|
1328
|
+
body: JSON.stringify({ decision: 'allow', reason: 'smoke test' }),
|
|
1329
|
+
signal: AbortSignal.timeout(10000)
|
|
1330
|
+
});
|
|
1331
|
+
|
|
1332
|
+
if (!decideRes.ok) throw new Error(`Decide failed: ${decideRes.status}`);
|
|
1333
|
+
|
|
1334
|
+
// Verify
|
|
1335
|
+
const checkRes = await fetch(`${relayUrl}/api/approvals/${approvalId}`, {
|
|
1336
|
+
headers: { 'Authorization': `Bearer ${creds.RELAY_API_KEY}` },
|
|
1337
|
+
signal: AbortSignal.timeout(10000)
|
|
1338
|
+
});
|
|
1339
|
+
const approval = await checkRes.json();
|
|
1340
|
+
|
|
1341
|
+
if (approval.status === 'allowed') {
|
|
1342
|
+
console.log(c.green(' ✅ PASS - Approval round-trip works\n'));
|
|
1343
|
+
passed++;
|
|
1344
|
+
} else {
|
|
1345
|
+
throw new Error(`Expected status=allowed, got ${approval.status}`);
|
|
1346
|
+
}
|
|
1347
|
+
} catch (e) {
|
|
1348
|
+
console.log(c.red(` ❌ FAIL - Approval round-trip: ${e.message}\n`));
|
|
1349
|
+
failed++;
|
|
1350
|
+
}
|
|
1351
|
+
} else {
|
|
1352
|
+
console.log(c.yellow(' ⏭️ SKIP - No credentials for round-trip test\n'));
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1299
1355
|
// Summary
|
|
1300
1356
|
console.log(c.purple('Test Summary:'));
|
|
1301
1357
|
console.log(` Passed: ${c.green(passed)}`);
|