teleportation-cli 1.3.0 → 1.4.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.
@@ -34,9 +34,10 @@ async function loadVersionInfo() {
34
34
  * @param {string} session_id - Session ID (also Claude session ID)
35
35
  * @param {string} cwd - Current working directory
36
36
  * @param {object} config - Config object with relayApiUrl and relayApiKey
37
+ * @param {object} [preExtractedMetadata] - Pre-extracted metadata to avoid duplicate extraction
37
38
  * @returns {Promise<boolean|object>} - True if registered successfully, or error object if orphan
38
39
  */
39
- export async function ensureSessionRegistered(session_id, cwd, config) {
40
+ export async function ensureSessionRegistered(session_id, cwd, config, preExtractedMetadata = null) {
40
41
  const RELAY_API_URL = config.relayApiUrl || '';
41
42
  const RELAY_API_KEY = config.relayApiKey || '';
42
43
 
@@ -65,36 +66,50 @@ export async function ensureSessionRegistered(session_id, cwd, config) {
65
66
  console.error(`[SessionRegister] Registering session ${session_id}`);
66
67
  }
67
68
 
68
- // Extract enhanced session metadata
69
- let metadata = { cwd, claude_session_id: session_id };
70
- try {
71
- // Try to load metadata extraction module
72
- const possiblePaths = [
73
- join(__dirname, '..', '..', 'lib', 'session', 'metadata.js'),
74
- join(process.env.HOME || process.env.USERPROFILE || '', '.teleportation', 'lib', 'session', 'metadata.js'),
75
- './lib/session/metadata.js'
76
- ];
69
+ // Use pre-extracted metadata if provided (avoids duplicate git subprocess calls)
70
+ let metadata;
71
+ if (preExtractedMetadata && Object.keys(preExtractedMetadata).length > 0) {
72
+ const client = preExtractedMetadata.client || 'claude-code';
73
+ const clientSessionField = client === 'cursor'
74
+ ? { cursor_conversation_id: session_id }
75
+ : { claude_session_id: session_id };
76
+ metadata = { ...preExtractedMetadata, session_id, ...clientSessionField };
77
+ if (env.DEBUG) {
78
+ console.error('[SessionRegister] Using pre-extracted metadata (skipping re-extraction)');
79
+ }
80
+ } else {
81
+ // Fallback: extract metadata (for callers that don't provide it)
82
+ metadata = { cwd, claude_session_id: session_id, client: 'claude-code' };
83
+ try {
84
+ const possiblePaths = [
85
+ join(__dirname, '..', '..', 'lib', 'session', 'metadata.js'),
86
+ join(process.env.HOME || process.env.USERPROFILE || '', '.teleportation', 'lib', 'session', 'metadata.js'),
87
+ './lib/session/metadata.js'
88
+ ];
77
89
 
78
- let metadataModule = null;
79
- for (const path of possiblePaths) {
80
- try {
81
- metadataModule = await import('file://' + path);
82
- break;
83
- } catch (e) {
84
- // Try next path
90
+ let metadataModule = null;
91
+ for (const path of possiblePaths) {
92
+ try {
93
+ metadataModule = await import('file://' + path);
94
+ break;
95
+ } catch (e) {
96
+ // Try next path
97
+ }
85
98
  }
86
- }
87
99
 
88
- if (metadataModule && metadataModule.extractSessionMetadata && cwd) {
89
- const extracted = await metadataModule.extractSessionMetadata(cwd);
90
- extracted.session_id = session_id;
91
- extracted.claude_session_id = session_id; // Include Claude session ID for autonomous task resumption
92
- metadata = extracted;
93
- }
94
- } catch (e) {
95
- // If metadata extraction fails, fall back to basic metadata
96
- if (env.DEBUG) {
97
- console.error('[SessionRegister] Failed to extract metadata:', e.message);
100
+ if (metadataModule && metadataModule.extractSessionMetadata && cwd) {
101
+ const extracted = await metadataModule.extractSessionMetadata(cwd);
102
+ extracted.session_id = session_id;
103
+ extracted.client = extracted.client || 'claude-code';
104
+ const clientSessionField = extracted.client === 'cursor'
105
+ ? { cursor_conversation_id: session_id }
106
+ : { claude_session_id: session_id };
107
+ metadata = { ...extracted, ...clientSessionField };
108
+ }
109
+ } catch (e) {
110
+ if (env.DEBUG) {
111
+ console.error('[SessionRegister] Failed to extract metadata:', e.message);
112
+ }
98
113
  }
99
114
  }
100
115
 
@@ -149,7 +164,7 @@ export async function ensureSessionRegistered(session_id, cwd, config) {
149
164
  'Content-Type': 'application/json',
150
165
  'Authorization': `Bearer ${RELAY_API_KEY}`
151
166
  },
152
- body: JSON.stringify({ session_id, meta: metadata })
167
+ body: JSON.stringify({ session_id, meta: metadata, cwd: metadata.working_directory || cwd })
153
168
  });
154
169
 
155
170
  if (response.ok || response.status === 200) {
@@ -40,9 +40,7 @@ const readStdin = () => new Promise((resolve, reject) => {
40
40
  const DAEMON_PORT = config.daemonPort || env.TELEPORTATION_DAEMON_PORT || '3050';
41
41
  const DAEMON_ENABLED = config.daemonEnabled !== false && env.TELEPORTATION_DAEMON_ENABLED !== 'false';
42
42
 
43
- // Detect message source
44
- const isAutonomousTask = env.TELEPORTATION_TASK_MODE === 'true' || !!env.TELEPORTATION_PARENT_SESSION_ID;
45
- const source = isAutonomousTask ? 'autonomous_task' : 'cli_interactive';
43
+ const source = 'cli_interactive';
46
44
 
47
45
  const updateSessionDaemonState = async (updates) => {
48
46
  if (!session_id || !RELAY_API_URL || !RELAY_API_KEY) return;
@@ -92,6 +90,34 @@ const readStdin = () => new Promise((resolve, reject) => {
92
90
  }
93
91
  }
94
92
 
93
+ // Mark PID-based session file as ended so daemon stops heartbeating immediately
94
+ try {
95
+ const { homedir } = await import('node:os');
96
+ const { fileURLToPath } = await import('node:url');
97
+ const { dirname } = await import('node:path');
98
+ const __hookDir = dirname(fileURLToPath(import.meta.url));
99
+ const registryPaths = [
100
+ join(homedir(), '.teleportation', 'lib', 'daemon', 'session-file-registry.js'),
101
+ // Relative path — consistent with session_start.mjs (__dirname-based, not URL-based)
102
+ join(__hookDir, '..', '..', 'lib', 'daemon', 'session-file-registry.js'),
103
+ ];
104
+ let marked = false;
105
+ for (const p of registryPaths) {
106
+ try {
107
+ const { markSessionEnded } = await import(p);
108
+ await markSessionEnded(session_id);
109
+ if (env.DEBUG) console.error(`[SessionEnd] Marked session file ended: ${session_id}`);
110
+ marked = true;
111
+ break;
112
+ } catch { /* try next */ }
113
+ }
114
+ if (!marked) {
115
+ console.error(`[SessionEnd] WARN: Could not mark session file ended — session-file-registry not found at any path`);
116
+ }
117
+ } catch (e) {
118
+ if (env.DEBUG) console.error(`[SessionEnd] Failed to mark session ended: ${e.message}`);
119
+ }
120
+
95
121
  // Delete registration marker file
96
122
  try {
97
123
  const registrationMarker = join(tmpdir(), `teleportation-session-${session_id}.registered`);
@@ -128,6 +128,17 @@ function updateSessionMarker(sessionId) {
128
128
  } catch {}
129
129
 
130
130
  let { session_id, cwd } = input || {};
131
+ // Cursor's sessionStart provides session_id (same as conversation_id) but cwd
132
+ // comes from workspace_roots, not a top-level cwd field
133
+ cwd = cwd || input?.workspace_roots?.[0];
134
+ // Detect client and capture Cursor-specific session context
135
+ const client = input?.cursor_version ? 'cursor' : 'claude-code';
136
+ const cursorMeta = client === 'cursor' ? {
137
+ client,
138
+ is_background_agent: input?.is_background_agent ?? false,
139
+ composer_mode: input?.composer_mode || null,
140
+ cursor_version: input?.cursor_version || null,
141
+ } : { client };
131
142
  const claude_session_id = session_id;
132
143
 
133
144
  // Check if credentials changed since last session start - user may need to restart
@@ -277,10 +288,55 @@ function updateSessionMarker(sessionId) {
277
288
  console.error(`[SessionStart] Captured model: ${meta.current_model}`);
278
289
  }
279
290
 
291
+ // If this session was spawned by the task executor as a child of a parent session,
292
+ // record the parent_session_id so the UI can hide child sessions from the list.
293
+ const parentSessionId = env.TELEPORTATION_PARENT_SESSION_ID;
294
+ const taskMeta = parentSessionId ? { parent_session_id: parentSessionId } : {};
295
+
296
+ const fullMeta = { ...meta, ...cursorMeta, ...taskMeta };
297
+
298
+ // PRIMARY: Write session file for daemon to discover via PID-based polling.
299
+ // This is fire-and-forget — no timeout, no network, survives daemon restarts.
300
+ // Cursor sessions use conversation_id not a real claude PID, so skip for them.
301
+ if (client === 'claude-code' && process.ppid) {
302
+ try {
303
+ const registryPaths = [
304
+ join(homedir(), '.teleportation', 'lib', 'daemon', 'session-file-registry.js'),
305
+ join(__dirname, '..', '..', 'lib', 'daemon', 'session-file-registry.js'),
306
+ ];
307
+ let writeSessionFile = null;
308
+ for (const p of registryPaths) {
309
+ try {
310
+ const mod = await import(p);
311
+ writeSessionFile = mod.writeSessionFile;
312
+ break;
313
+ } catch { /* try next */ }
314
+ }
315
+ if (writeSessionFile) {
316
+ await writeSessionFile({ session_id, claude_pid: process.ppid, cwd: cwd || process.cwd(), meta: fullMeta });
317
+ if (env.DEBUG) {
318
+ // Log the parent process name so we can verify ppid points to `claude`
319
+ // and not an intermediate shell (sh -c "bun hook") in some environments.
320
+ try {
321
+ const { execFile } = await import('node:child_process');
322
+ const { promisify: pify } = await import('node:util');
323
+ const { stdout } = await pify(execFile)('ps', ['-p', String(process.ppid), '-o', 'comm='], { timeout: 1000 });
324
+ console.error(`[SessionStart] Session file written (PID: ${process.ppid}, comm: ${stdout.trim()})`);
325
+ } catch {
326
+ console.error(`[SessionStart] Session file written (PID: ${process.ppid})`);
327
+ }
328
+ }
329
+ }
330
+ } catch (fileErr) {
331
+ if (env.DEBUG) console.error(`[SessionStart] Session file write failed: ${fileErr.message}`);
332
+ }
333
+ }
334
+
335
+ // SECONDARY: HTTP registration with daemon (kept for backward compat with older daemons).
280
336
  await fetchWithTimeout(`${daemonUrl}/sessions/register`, {
281
337
  method: 'POST',
282
338
  headers: { 'Content-Type': 'application/json' },
283
- body: JSON.stringify({ session_id, claude_session_id, cwd, meta })
339
+ body: JSON.stringify({ session_id, claude_session_id, cwd, meta: fullMeta })
284
340
  }, 2000);
285
341
 
286
342
  if (env.DEBUG) {