teleportation-cli 1.4.3 → 1.4.5

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.
@@ -294,6 +294,13 @@ const sessionPidCache = new Map(); // sessionId -> number (claude_pid)
294
294
  const lastPidCheck = new Map(); // sessionId -> number (timestamp of last check)
295
295
  const PID_CHECK_INTERVAL_MS = parseInt(process.env.DAEMON_PID_CHECK_INTERVAL_MS || '30000', 10); // 30s default
296
296
 
297
+ // Sessions discovered from the session log on daemon restart get a grace period
298
+ // to receive a fresh hook registration (with a PID marker file). If no PID marker
299
+ // appears within LOG_RECOVERY_GRACE_MS, the session is treated as stale and removed.
300
+ // This prevents ghost sessions from other projects being heartbeated indefinitely.
301
+ const logRecoveredSessions = new Set(); // sessionIds recovered from session-events.log
302
+ const LOG_RECOVERY_GRACE_MS = parseInt(process.env.DAEMON_LOG_RECOVERY_GRACE_MS || '300000', 10); // 5 min default
303
+
297
304
  // Transcript ingestion throttling: Prevents concurrent ingestion runs per session
298
305
  // Map<session_id, Promise> tracks in-progress ingestion promises
299
306
  // If ingestion takes >5 seconds, prevents stacking multiple concurrent calls
@@ -844,6 +851,8 @@ async function handleRequest(req, res) {
844
851
  // Clear stale PID cache so the new session's PID is read fresh from marker file
845
852
  sessionPidCache.delete(session_id);
846
853
  lastPidCheck.delete(session_id);
854
+ // Fresh registration clears the log-recovery flag — this session now has a live hook
855
+ logRecoveredSessions.delete(session_id);
847
856
 
848
857
  const sessionEntry = {
849
858
  session_id,
@@ -1653,6 +1662,7 @@ async function cleanupDeadSession(session_id, pid) {
1653
1662
  sessionPidCache.delete(session_id);
1654
1663
  lastPidCheck.delete(session_id);
1655
1664
  ingestionInProgress.delete(session_id);
1665
+ logRecoveredSessions.delete(session_id);
1656
1666
  stoppedSessions.add(session_id); // Prevent re-activation from stale registration attempts
1657
1667
 
1658
1668
  // Delete marker file
@@ -1721,7 +1731,21 @@ async function pollRelayAPI() {
1721
1731
  await cleanupDeadSession(session_id, pid);
1722
1732
  continue;
1723
1733
  }
1724
- // PID is alive (or no PID file exists - backward compat: treat as alive)
1734
+ if (pid === null && logRecoveredSessions.has(session_id)) {
1735
+ // Session was recovered from the log but has no PID marker file yet.
1736
+ // Give it a grace period to receive a fresh hook registration.
1737
+ // After that, treat it as stale and clean it up to prevent ghost sessions
1738
+ // from other projects being heartbeated indefinitely.
1739
+ const sessionData = sessions.get(session_id);
1740
+ const age = pidCheckNow - (sessionData?.registered_at || pidCheckNow);
1741
+ if (age > LOG_RECOVERY_GRACE_MS) {
1742
+ logInfo(`[daemon] Log-recovered session ${session_id.slice(0, 8)}... has no PID marker after ${Math.round(age / 60000)}min — removing as stale`);
1743
+ await cleanupDeadSession(session_id, null);
1744
+ continue;
1745
+ }
1746
+ // Still within grace period — treat as alive, but don't reset the log-recovery flag
1747
+ }
1748
+ // PID is alive (or no PID file, still within grace period)
1725
1749
  sessionActivity.set(session_id, pidCheckNow);
1726
1750
  }
1727
1751
  // Between PID checks, do NOT refresh sessionActivity - let the cleanup
@@ -3047,6 +3071,10 @@ async function discoverSessionsFromLog() {
3047
3071
  for (const session of activeSessions) {
3048
3072
  sessions.set(session.session_id, session);
3049
3073
  sessionActivity.set(session.session_id, session.last_heartbeat);
3074
+ // Mark as log-recovered so PID check can apply a grace period.
3075
+ // A fresh session_start.mjs hook registration (which creates a PID marker file)
3076
+ // will clear this flag — the session is then treated as fully live.
3077
+ logRecoveredSessions.add(session.session_id);
3050
3078
  }
3051
3079
 
3052
3080
  if (activeSessions.length > 0) {
@@ -12,7 +12,6 @@ import { homedir, tmpdir } from 'os';
12
12
  import { join } from 'path';
13
13
  import { createHash } from 'node:crypto';
14
14
  import { sanitizeEventData } from '../utils/log-sanitizer.js';
15
- import { normalizeTranscriptEvents, normalizeTranscriptEntry } from '../intelligence/schema.js';
16
15
 
17
16
  // ============================================================================
18
17
  // Configuration Constants
@@ -591,13 +590,10 @@ function extractTimelineEvents(transcript, fromIndex = 0, sessionId = '') {
591
590
  * @param {Object} context - Normalization context
592
591
  * @returns {Array} Normalized intelligence events
593
592
  */
594
- function normalizeTimelineEventsForIntelligence(events, context = {}) {
595
- try {
596
- return normalizeTranscriptEvents(events, context);
597
- } catch (error) {
598
- console.warn(`[transcript] Intelligence normalization failed: ${error.message}`);
599
- return [];
600
- }
593
+ function normalizeTimelineEventsForIntelligence(_events, _context = {}) {
594
+ // TODO(transcript-intelligence): re-enable when ../intelligence/schema.js lands.
595
+ // Callers handle empty arrays safely (no-op pipeline).
596
+ return [];
601
597
  }
602
598
 
603
599
  /**
@@ -887,16 +883,9 @@ export async function getTranscriptLength(claude_session_id) {
887
883
  * Extract normalized transcript entries for the intelligence pipeline.
888
884
  * Operates on raw transcript messages (not timeline events).
889
885
  */
890
- function extractNormalizedTranscriptEntries(transcript, fromIndex = 0, sessionId = '', harness = 'claude-code') {
891
- const normalized = [];
892
- for (let i = fromIndex; i < transcript.length; i++) {
893
- normalized.push(normalizeTranscriptEntry(transcript[i], {
894
- sessionId,
895
- messageIndex: i,
896
- harness,
897
- }));
898
- }
899
- return normalized;
886
+ function extractNormalizedTranscriptEntries(_transcript, _fromIndex = 0, _sessionId = '', _harness = 'claude-code') {
887
+ // TODO(transcript-intelligence): re-enable when ../intelligence/schema.js lands.
888
+ return [];
900
889
  }
901
890
 
902
891
  function toSessionFilename(sessionId) {
@@ -100,28 +100,36 @@ export async function installViaUhr(manifestPath, hooksDir, options = {}) {
100
100
  return { success: false, reason: 'UHR CLI not found (checked PATH and node_modules/.bin/uhr)' };
101
101
  }
102
102
 
103
+ // Pass all supported platforms so UHR lockfile includes cursor + gemini-cli.
104
+ // Without this, UHR only tracks claude-code and warns that cursor/gemini-cli
105
+ // hooks are "not in lockfile platforms" (uhr.gm bug report, PR #10 fix).
106
+ const platforms = 'claude-code,gemini-cli,cursor';
107
+
103
108
  const warnings = [];
104
109
  try {
105
- const output = execSync(`"${uhrBin}" install "${tempManifestPath}"`, {
110
+ const result = execSync(`"${uhrBin}" install "${tempManifestPath}" --platforms ${platforms}`, {
106
111
  encoding: 'utf8',
107
112
  stdio: ['ignore', 'pipe', 'pipe'],
108
113
  timeout: 30000,
109
114
  });
110
115
 
111
- // Collect any warning lines from stdout
112
- if (output) {
113
- const lines = output.split('\n').filter(Boolean);
114
- for (const line of lines) {
115
- if (/warn/i.test(line)) {
116
- warnings.push(line.trim());
117
- }
116
+ // Collect warning lines from both stdout and stderr (UHR emits warnings to stderr)
117
+ for (const output of [result, result?.stderr]) {
118
+ if (!output) continue;
119
+ for (const line of output.split('\n').filter(Boolean)) {
120
+ if (/warn/i.test(line)) warnings.push(line.trim());
118
121
  }
119
122
  }
120
123
 
121
124
  return { success: true, warnings };
122
125
  } catch (err) {
126
+ // execSync throws when exit code != 0; collect stderr for diagnostics
127
+ const stderr = err?.stderr || '';
128
+ for (const line of stderr.split('\n').filter(Boolean)) {
129
+ if (/warn/i.test(line)) warnings.push(line.trim());
130
+ }
123
131
  const msg = err instanceof Error ? err.message : String(err);
124
- return { success: false, reason: msg || 'uhr install failed' };
132
+ return { success: false, reason: msg || 'uhr install failed', warnings };
125
133
  }
126
134
  }
127
135
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "teleportation-cli",
3
- "version": "1.4.3",
3
+ "version": "1.4.5",
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",
@@ -55,6 +55,9 @@
55
55
  "!lib/**/*.log",
56
56
  ".claude/hooks/*.mjs",
57
57
  "!.claude/hooks/*.test.mjs",
58
+ ".gemini/hooks/*.mjs",
59
+ ".gemini/hooks/shared/*.mjs",
60
+ "teleportation.uhr.json",
58
61
  "scripts/sync-transcripts.sh",
59
62
  "teleportation-cli.cjs",
60
63
  "README.md",
@@ -3466,6 +3466,20 @@ async function commandInstallHooks() {
3466
3466
  uhrResult.warnings.forEach(w => console.log(c.yellow(` ⚠️ ${w}`)));
3467
3467
  }
3468
3468
  installed = true;
3469
+
3470
+ // UHR only handles Claude Code — also install Cursor hooks via legacy installer
3471
+ try {
3472
+ const installerPath = path.join(TELEPORTATION_DIR, 'lib', 'install', 'installer.js');
3473
+ const { checkCursorIde, installCursorHooks } = await import('file://' + installerPath);
3474
+ if (checkCursorIde().valid) {
3475
+ const sourceHooksDir = path.join(TELEPORTATION_DIR, '.claude', 'hooks');
3476
+ await installCursorHooks(sourceHooksDir);
3477
+ console.log(c.green(' ✅ Cursor IDE hooks installed'));
3478
+ console.log(c.dim(' Config: ~/.cursor/hooks.json'));
3479
+ }
3480
+ } catch (e) {
3481
+ // Cursor not installed or hooks install failed — non-fatal
3482
+ }
3469
3483
  } else {
3470
3484
  console.log(c.yellow(` ⚠️ UHR install failed: ${uhrResult.reason}`));
3471
3485
  console.log(c.dim(' Falling back to legacy installer...'));
@@ -3492,6 +3506,11 @@ async function commandInstallHooks() {
3492
3506
  console.log(c.dim(' Directory: ~/.gemini/hooks/'));
3493
3507
  }
3494
3508
 
3509
+ if (result.cursorHooksInstalled > 0) {
3510
+ console.log(c.green(` ✅ Cursor IDE hooks installed`));
3511
+ console.log(c.dim(' Config: ~/.cursor/hooks.json'));
3512
+ }
3513
+
3495
3514
  if (result.libFilesInstalled > 0) {
3496
3515
  console.log(c.green(` ✅ ${result.libFilesInstalled} shared library files installed`));
3497
3516
  }
@@ -0,0 +1,76 @@
1
+ {
2
+ "$schema": "https://uhr.dev/schema/manifest.v1.json",
3
+ "name": "teleportation",
4
+ "version": "1.2.0",
5
+ "description": "Remote approval system for Claude Code — approve AI coding changes from any device",
6
+ "homepage": "https://github.com/dundas/teleportation-private",
7
+ "hooks": [
8
+ {
9
+ "id": "pre-tool-use",
10
+ "on": "beforeToolExecution",
11
+ "command": "bun __HOOKS_DIR__/pre_tool_use.mjs",
12
+ "tools": ["*"],
13
+ "blocking": true,
14
+ "timeout": 300000,
15
+ "platforms": ["claude-code", "gemini-cli", "cursor"]
16
+ },
17
+ {
18
+ "id": "session-start",
19
+ "on": "sessionStart",
20
+ "command": "bun __HOOKS_DIR__/session_start.mjs",
21
+ "platforms": ["claude-code", "gemini-cli", "cursor"]
22
+ },
23
+ {
24
+ "id": "session-end",
25
+ "on": "sessionEnd",
26
+ "command": "bun __HOOKS_DIR__/session_end.mjs",
27
+ "platforms": ["claude-code", "gemini-cli", "cursor"]
28
+ },
29
+ {
30
+ "id": "stop",
31
+ "on": "stop",
32
+ "command": "bun __HOOKS_DIR__/stop.mjs",
33
+ "platforms": ["claude-code", "cursor"]
34
+ },
35
+ {
36
+ "id": "notification",
37
+ "on": "notification",
38
+ "command": "bun __HOOKS_DIR__/notification.mjs",
39
+ "platforms": ["claude-code"]
40
+ },
41
+ {
42
+ "id": "user-prompt-submit",
43
+ "on": "beforePromptSubmit",
44
+ "command": "bun __HOOKS_DIR__/user_prompt_submit.mjs",
45
+ "platforms": ["claude-code", "cursor"]
46
+ },
47
+ {
48
+ "id": "permission-request",
49
+ "on": "permissionRequest",
50
+ "command": "bun __HOOKS_DIR__/permission_request.mjs",
51
+ "blocking": true,
52
+ "timeout": 300000,
53
+ "platforms": ["claude-code"]
54
+ },
55
+ {
56
+ "id": "post-tool-use",
57
+ "on": "afterToolExecution",
58
+ "command": "bun __HOOKS_DIR__/post_tool_use.mjs",
59
+ "tools": ["*"],
60
+ "platforms": ["claude-code", "cursor"]
61
+ }
62
+ ],
63
+ "permissions": {
64
+ "allow": ["read", "grep", "fetch"],
65
+ "deny": []
66
+ },
67
+ "ordering": {
68
+ "pre-tool-use": {
69
+ "runBefore": ["*"]
70
+ }
71
+ },
72
+ "meta": {
73
+ "minUhrVersion": "0.1.0",
74
+ "license": "MIT"
75
+ }
76
+ }