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
- if (process.env.DEBUG) {
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
  }
@@ -287,11 +287,11 @@ export async function installGeminiHooks(sourceGeminiHooksDir) {
287
287
  settings.hooks = settings.hooks || {};
288
288
 
289
289
  const hooksConfig = {
290
- BeforeTool: [{ command: `node ${join(destHooksDir, 'before_tool.mjs')}`, timeout: 65000 }],
291
- AfterTool: [{ command: `node ${join(destHooksDir, 'after_tool.mjs')}`, timeout: 10000 }],
292
- SessionStart: [{ command: `node ${join(destHooksDir, 'session_start.mjs')}`, timeout: 15000 }],
293
- AfterAgent: [{ command: `node ${join(destHooksDir, 'after_agent.mjs')}`, timeout: 15000 }],
294
- SessionEnd: [{ command: `node ${join(destHooksDir, 'session_end.mjs')}`, timeout: 10000 }]
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 router
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: `node ${join(projectHooksDir, 'pre_tool_use.mjs')}`
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: `node ${join(projectHooksDir, 'stop.mjs')}`
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: `node ${join(projectHooksDir, 'session_start.mjs')}`
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: `node ${join(projectHooksDir, 'session_end.mjs')}`
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: `node ${join(projectHooksDir, 'notification.mjs')}`
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: `node ${join(projectHooksDir, 'user_prompt_submit.mjs')}`
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "teleportation-cli",
3
- "version": "1.2.1",
3
+ "version": "1.3.0",
4
4
  "description": "Remote approval system for Claude Code - approve AI coding changes from your phone",
5
5
  "type": "module",
6
6
  "main": "teleportation-cli.cjs",
@@ -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.1.4';
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)}`);