teleportation-cli 1.1.5 → 1.2.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.
Files changed (42) hide show
  1. package/.claude/hooks/permission_request.mjs +326 -59
  2. package/.claude/hooks/post_tool_use.mjs +90 -0
  3. package/.claude/hooks/pre_tool_use.mjs +212 -293
  4. package/.claude/hooks/session-register.mjs +89 -104
  5. package/.claude/hooks/session_end.mjs +41 -42
  6. package/.claude/hooks/session_start.mjs +45 -60
  7. package/.claude/hooks/stop.mjs +752 -99
  8. package/.claude/hooks/user_prompt_submit.mjs +26 -3
  9. package/lib/cli/daemon-commands.js +1 -1
  10. package/lib/cli/teleport-commands.js +469 -0
  11. package/lib/daemon/daemon-v2.js +104 -0
  12. package/lib/daemon/lifecycle.js +56 -171
  13. package/lib/daemon/services/index.js +3 -0
  14. package/lib/daemon/services/polling-service.js +173 -0
  15. package/lib/daemon/services/queue-service.js +318 -0
  16. package/lib/daemon/services/session-service.js +115 -0
  17. package/lib/daemon/state.js +35 -0
  18. package/lib/daemon/task-executor-v2.js +413 -0
  19. package/lib/daemon/task-executor.js +270 -96
  20. package/lib/daemon/teleportation-daemon.js +709 -126
  21. package/lib/daemon/timeline-analyzer.js +215 -0
  22. package/lib/daemon/transcript-ingestion.js +696 -0
  23. package/lib/daemon/utils.js +91 -0
  24. package/lib/install/installer.js +184 -20
  25. package/lib/install/uhr-installer.js +136 -0
  26. package/lib/remote/providers/base-provider.js +46 -0
  27. package/lib/remote/providers/daytona-provider.js +58 -0
  28. package/lib/remote/providers/provider-factory.js +90 -19
  29. package/lib/remote/providers/sprites-provider.js +711 -0
  30. package/lib/teleport/exporters/claude-exporter.js +302 -0
  31. package/lib/teleport/exporters/gemini-exporter.js +307 -0
  32. package/lib/teleport/exporters/index.js +93 -0
  33. package/lib/teleport/exporters/interface.js +153 -0
  34. package/lib/teleport/fork-tracker.js +415 -0
  35. package/lib/teleport/git-committer.js +337 -0
  36. package/lib/teleport/index.js +48 -0
  37. package/lib/teleport/manager.js +620 -0
  38. package/lib/teleport/session-capture.js +282 -0
  39. package/package.json +6 -2
  40. package/teleportation-cli.cjs +488 -453
  41. package/.claude/hooks/heartbeat.mjs +0 -396
  42. package/lib/daemon/pid-manager.js +0 -183
@@ -31,7 +31,7 @@ async function loadVersionInfo() {
31
31
 
32
32
  /**
33
33
  * Register a session with the relay API if not already registered
34
- * @param {string} session_id - Session ID
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
37
  * @returns {Promise<boolean|object>} - True if registered successfully, or error object if orphan
@@ -66,7 +66,7 @@ export async function ensureSessionRegistered(session_id, cwd, config) {
66
66
  }
67
67
 
68
68
  // Extract enhanced session metadata
69
- let metadata = { cwd };
69
+ let metadata = { cwd, claude_session_id: session_id };
70
70
  try {
71
71
  // Try to load metadata extraction module
72
72
  const possiblePaths = [
@@ -88,6 +88,7 @@ export async function ensureSessionRegistered(session_id, cwd, config) {
88
88
  if (metadataModule && metadataModule.extractSessionMetadata && cwd) {
89
89
  const extracted = await metadataModule.extractSessionMetadata(cwd);
90
90
  extracted.session_id = session_id;
91
+ extracted.claude_session_id = session_id; // Include Claude session ID for autonomous task resumption
91
92
  metadata = extracted;
92
93
  }
93
94
  } catch (e) {
@@ -108,6 +109,36 @@ export async function ensureSessionRegistered(session_id, cwd, config) {
108
109
  // Version info not available - old installation
109
110
  }
110
111
 
112
+ // Mark cloud sessions (set by cloud provider bootstrap scripts)
113
+ if (env.TELEPORTATION_IS_CLOUD === 'true') {
114
+ metadata.is_cloud = true;
115
+ }
116
+
117
+ // PRD-0002: Create session file BEFORE relay registration
118
+ // This prevents race condition where registration succeeds but hook crashes before creating file
119
+ // If registration fails, we cleanup the session file
120
+ const sessionFile = join(tmpdir(), `teleportation-session-${session_id}.json`);
121
+ const claudePid = process.ppid || process.pid; // Use parent (Claude CLI), fallback to current process
122
+ let sessionFileCreated = false;
123
+
124
+ try {
125
+ await writeFile(sessionFile, JSON.stringify({
126
+ session_id,
127
+ claude_pid: claudePid,
128
+ cwd: cwd || process.cwd(),
129
+ registered_at: Date.now(),
130
+ meta: metadata
131
+ }), { mode: 0o600 });
132
+ sessionFileCreated = true;
133
+
134
+ if (env.DEBUG) {
135
+ console.error(`[SessionRegister] Session file created (Claude PID: ${claudePid})`);
136
+ }
137
+ } catch (e) {
138
+ console.error('[SessionRegister] CRITICAL: Failed to create session file:', e.message);
139
+ // Continue with registration anyway - daemon won't track but session will exist
140
+ }
141
+
111
142
  try {
112
143
  if (env.DEBUG) {
113
144
  console.error(`[SessionRegister] Calling ${RELAY_API_URL}/api/sessions/register`);
@@ -132,124 +163,77 @@ export async function ensureSessionRegistered(session_id, cwd, config) {
132
163
  // Ignore marker write failures - registration still succeeded
133
164
  }
134
165
 
135
- // Start heartbeat if not already running
166
+ // Write to shared session log for daemon discovery
136
167
  try {
137
- const heartbeatInterval = config.session?.heartbeat?.interval || 120000;
138
- const startDelay = config.session?.heartbeat?.startDelay || 5000;
139
- const maxFailures = config.session?.heartbeat?.maxFailures || 3;
140
-
141
- // Heartbeats are mandatory for session liveness tracking
142
- if (RELAY_API_URL && RELAY_API_KEY) {
143
- const { spawn } = await import('child_process');
144
- const heartbeatPath = join(__dirname, 'heartbeat.mjs');
168
+ const { appendFile, mkdir } = await import('fs/promises');
169
+ const sessionLogPath = join(dirname(registrationMarker), 'session-events.log');
145
170
 
146
- // Check if heartbeat is already running for this session
147
- const { tmpdir } = await import('os');
148
- const { readFile, unlink } = await import('fs/promises');
149
- const pidFile = join(tmpdir(), `teleportation-heartbeat-${session_id}.pid`);
171
+ // Ensure directory exists
172
+ await mkdir(dirname(sessionLogPath), { recursive: true, mode: 0o700 }).catch(() => {});
150
173
 
151
- let shouldStartHeartbeat = true;
152
- try {
153
- const pidContent = await readFile(pidFile, 'utf8');
154
- // Parse PID file (JSON format with session_id validation)
155
- let pidData;
156
- try {
157
- pidData = JSON.parse(pidContent);
158
- } catch {
159
- // Fallback for old format (plain PID number)
160
- pidData = { pid: parseInt(pidContent.trim(), 10) };
161
- }
162
-
163
- const pid = pidData.pid;
164
- if (pid && !isNaN(pid)) {
165
- // Check if the process is actually running
166
- try {
167
- process.kill(pid, 0); // Signal 0 checks existence without killing
168
- // Process is running - don't start another heartbeat
169
- shouldStartHeartbeat = false;
170
- if (env.DEBUG) {
171
- console.error(`[SessionRegister] Heartbeat already running (PID: ${pid})`);
172
- }
173
- } catch (killError) {
174
- if (killError.code === 'ESRCH') {
175
- // Process is dead - clean up stale PID file
176
- try {
177
- await unlink(pidFile);
178
- if (env.DEBUG) {
179
- console.error(`[SessionRegister] Cleaned up stale heartbeat PID file (PID: ${pid})`);
180
- }
181
- } catch (unlinkError) {
182
- if (env.DEBUG) {
183
- console.error(`[SessionRegister] Failed to unlink stale PID file: ${unlinkError.message}`);
184
- }
185
- }
186
- // Will start new heartbeat
187
- } else {
188
- // Permission error - assume process exists, don't start duplicate
189
- shouldStartHeartbeat = false;
190
- if (env.DEBUG) {
191
- console.error(`[SessionRegister] Cannot check heartbeat (permission denied), assuming running (PID: ${pid})`);
192
- }
193
- }
194
- }
195
- }
196
- } catch (e) {
197
- // PID file doesn't exist, start heartbeat
198
- }
174
+ const logEvent = {
175
+ type: 'register',
176
+ session_id: session_id,
177
+ claude_session_id: session_id,
178
+ pid: process.pid,
179
+ cwd: cwd || process.cwd(),
180
+ meta: metadata,
181
+ timestamp: Date.now()
182
+ };
199
183
 
200
- if (shouldStartHeartbeat) {
201
- // Kill any orphan heartbeat processes for this session before starting a new one
202
- // This handles race conditions where multiple processes pass the check simultaneously
203
- try {
204
- const { execSync } = await import('child_process');
205
- // Use pkill with -f flag to match full command line (session_id argument)
206
- // Returns 1 if no processes matched, so we ignore the exit code
207
- execSync(`pkill -f "heartbeat.mjs ${session_id}" 2>/dev/null || true`, { stdio: 'ignore' });
208
- if (env.DEBUG) {
209
- console.error(`[SessionRegister] Cleaned up any orphan heartbeats for ${session_id}`);
210
- }
211
- // Small delay to let orphan processes terminate
212
- await new Promise(r => setTimeout(r, 100));
213
- } catch (pkillError) {
214
- // Ignore pkill errors - process might not exist
215
- if (env.DEBUG) {
216
- console.error(`[SessionRegister] pkill cleanup failed: ${pkillError.message}`);
217
- }
218
- }
184
+ await appendFile(sessionLogPath, JSON.stringify(logEvent) + '\n', 'utf8');
185
+ } catch (e) {
186
+ // Log all failures - session recovery depends on this
187
+ console.error('[SessionRegister] WARN: Failed to write session log:', e.message);
219
188
 
220
- const heartbeat = spawn('node', [heartbeatPath, session_id], {
221
- detached: true,
222
- stdio: 'ignore',
223
- env: {
224
- ...process.env,
225
- SESSION_ID: session_id,
226
- RELAY_API_URL,
227
- RELAY_API_KEY,
228
- HEARTBEAT_INTERVAL: String(heartbeatInterval),
229
- START_DELAY: String(startDelay),
230
- MAX_FAILURES: String(maxFailures),
231
- TELEPORTATION_DEBUG: process.env.TELEPORTATION_DEBUG || 'false'
232
- }
233
- });
234
- heartbeat.unref();
235
- }
236
- }
237
- } catch (error) {
238
- // Don't fail registration if heartbeat spawn fails
239
- if (env.DEBUG) {
240
- console.error('[SessionRegister] Failed to spawn heartbeat:', error.message);
189
+ // Critical errors should be visible
190
+ if (e.code === 'EACCES' || e.code === 'ENOSPC') {
191
+ console.error(`[SessionRegister] CRITICAL: ${e.code} - Session recovery may fail after daemon restart`);
241
192
  }
242
193
  }
243
194
 
195
+ // Session file already created above (before registration)
196
+ // This ensures daemon can start tracking immediately
244
197
  return true;
245
198
  } else if (response.status === 403) {
246
199
  const data = await response.json().catch(() => ({}));
247
200
  if (data.error === 'orphan_api_key') {
201
+ // Cleanup session file on orphan key error
202
+ if (sessionFileCreated) {
203
+ try {
204
+ const { unlink } = await import('fs/promises');
205
+ await unlink(sessionFile);
206
+ if (env.DEBUG) {
207
+ console.error('[SessionRegister] Cleaned up session file (orphan API key)');
208
+ }
209
+ } catch {}
210
+ }
248
211
  return { success: false, error: 'orphan_api_key', message: data.message };
249
212
  }
250
213
  }
214
+
215
+ // Registration failed - cleanup session file to prevent orphan
216
+ if (sessionFileCreated) {
217
+ try {
218
+ const { unlink } = await import('fs/promises');
219
+ await unlink(sessionFile);
220
+ if (env.DEBUG) {
221
+ console.error('[SessionRegister] Cleaned up session file (registration failed)');
222
+ }
223
+ } catch {}
224
+ }
251
225
  } catch (e) {
252
- // Registration failed - this is okay, we'll try again next time
226
+ // Registration failed - cleanup session file
227
+ if (sessionFileCreated) {
228
+ try {
229
+ const { unlink } = await import('fs/promises');
230
+ await unlink(sessionFile);
231
+ if (env.DEBUG) {
232
+ console.error('[SessionRegister] Cleaned up session file (exception)');
233
+ }
234
+ } catch {}
235
+ }
236
+
253
237
  if (env.DEBUG) {
254
238
  console.error('[SessionRegister] Failed to register session:', e.message);
255
239
  }
@@ -296,6 +280,7 @@ export async function updateSessionMetadata(session_id, cwd, config) {
296
280
  if (metadataModule && metadataModule.extractSessionMetadata && cwd) {
297
281
  const extracted = await metadataModule.extractSessionMetadata(cwd);
298
282
  extracted.session_id = session_id;
283
+ extracted.claude_session_id = session_id; // Critical: preserve claude_session_id for task resumption
299
284
  metadata = extracted;
300
285
  }
301
286
  } catch (e) {
@@ -40,6 +40,10 @@ 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';
46
+
43
47
  const updateSessionDaemonState = async (updates) => {
44
48
  if (!session_id || !RELAY_API_URL || !RELAY_API_KEY) return;
45
49
  try {
@@ -54,55 +58,38 @@ const readStdin = () => new Promise((resolve, reject) => {
54
58
  } catch {}
55
59
  };
56
60
 
57
- // Kill heartbeat background process if running
61
+ // NOTE: Heartbeat processes are no longer spawned per-session.
62
+ // The daemon handles heartbeats inline (PRD-0025 migration).
63
+
58
64
  if (session_id) {
65
+ // Write unregister event to shared session log
59
66
  try {
60
- const pidFile = join(tmpdir(), `teleportation-heartbeat-${session_id}.pid`);
61
- const pidContent = await readFile(pidFile, 'utf8');
67
+ const { appendFile, mkdir } = await import('fs/promises');
68
+ const { homedir } = await import('os');
69
+ const { dirname } = await import('path');
70
+ const sessionLogPath = join(homedir(), '.teleportation', 'session-events.log');
62
71
 
63
- // Parse PID file (now JSON format with session_id validation)
64
- let pidData;
65
- try {
66
- pidData = JSON.parse(pidContent);
67
- } catch {
68
- // Fallback for old format (plain PID number)
69
- pidData = { pid: parseInt(pidContent.trim(), 10) };
70
- }
72
+ // Ensure directory exists
73
+ await mkdir(dirname(sessionLogPath), { recursive: true, mode: 0o700 }).catch(() => {});
71
74
 
72
- const pid = pidData.pid;
75
+ const logEvent = {
76
+ type: 'unregister',
77
+ session_id: session_id,
78
+ claude_session_id: session_id,
79
+ pid: process.pid,
80
+ cwd: process.cwd(),
81
+ timestamp: Date.now()
82
+ };
73
83
 
74
- // Validate session_id matches (prevents killing wrong process)
75
- if (pid && !isNaN(pid)) {
76
- if (pidData.session_id && pidData.session_id !== session_id) {
77
- console.error(`[SessionEnd] PID file session_id mismatch: expected ${session_id}, got ${pidData.session_id}`);
78
- } else {
79
- try {
80
- // Verify process exists before killing
81
- process.kill(pid, 0); // Signal 0 checks existence without killing
82
-
83
- // Process exists, safe to kill
84
- process.kill(pid, 'SIGTERM');
85
- console.log(`[SessionEnd] Killed heartbeat process (PID: ${pid})`);
86
- } catch (killError) {
87
- if (killError.code === 'ESRCH') {
88
- // Process already dead, that's okay
89
- console.log(`[SessionEnd] Heartbeat process already terminated (PID: ${pid})`);
90
- } else {
91
- // Permission error or other issue
92
- console.error(`[SessionEnd] Failed to kill heartbeat:`, killError.message);
93
- }
94
- }
95
- }
96
- }
84
+ await appendFile(sessionLogPath, JSON.stringify(logEvent) + '\n', 'utf8');
85
+ } catch (e) {
86
+ // Log all failures - session recovery depends on this
87
+ console.error('[SessionEnd] WARN: Failed to write session log:', e.message);
97
88
 
98
- // Delete PID file
99
- try {
100
- await unlink(pidFile);
101
- } catch (unlinkError) {
102
- // Ignore errors - file might not exist
89
+ // Critical errors should be visible
90
+ if (e.code === 'EACCES' || e.code === 'ENOSPC') {
91
+ console.error(`[SessionEnd] CRITICAL: ${e.code} - Session recovery may fail after daemon restart`);
103
92
  }
104
- } catch (error) {
105
- // Ignore errors reading PID file - heartbeat might not have been started
106
93
  }
107
94
 
108
95
  // Delete registration marker file
@@ -113,6 +100,17 @@ const readStdin = () => new Promise((resolve, reject) => {
113
100
  // Ignore errors - marker might not exist
114
101
  }
115
102
 
103
+ // Delete daemon session file (prevents /tmp accumulation)
104
+ try {
105
+ const daemonSessionFile = join(tmpdir(), `teleportation-session-${session_id}.json`);
106
+ await unlink(daemonSessionFile);
107
+ if (env.DEBUG) {
108
+ console.error(`[SessionEnd] Deleted daemon session file: ${daemonSessionFile}`);
109
+ }
110
+ } catch (e) {
111
+ // Ignore errors - file might not exist
112
+ }
113
+
116
114
  // Clean up model tracking files
117
115
  try {
118
116
  const lastModelFile = join(tmpdir(), `teleportation-last-model-${session_id}.txt`);
@@ -215,6 +213,7 @@ const readStdin = () => new Promise((resolve, reject) => {
215
213
  body: JSON.stringify({
216
214
  session_id,
217
215
  type: 'session_end',
216
+ source,
218
217
  data: {
219
218
  reason: 'normal_exit',
220
219
  timestamp: Date.now()
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env bun
2
2
 
3
3
  import { stdin, exit, env } from 'node:process';
4
- import { spawn } from 'child_process';
5
4
  import { fileURLToPath } from 'url';
6
5
  import { dirname, join } from 'path';
7
6
  import { homedir } from 'os';
8
7
  import { existsSync, statSync, writeFileSync, mkdirSync } from 'fs';
8
+ import { agentStart, agentStatus } from '@derivativelabs/agent-process';
9
9
 
10
10
  const __filename = fileURLToPath(import.meta.url);
11
11
  const __dirname = dirname(__filename);
@@ -166,14 +166,22 @@ function updateSessionMarker(sessionId) {
166
166
  // Auto-start daemon if enabled
167
167
  if (DAEMON_ENABLED && session_id && RELAY_API_URL && RELAY_API_KEY) {
168
168
  try {
169
- // Check if daemon is already running
169
+ // Check if daemon is already running using agent-runtime
170
170
  const daemonUrl = `http://127.0.0.1:${DAEMON_PORT}`;
171
171
  let daemonRunning = false;
172
172
 
173
173
  try {
174
- const healthResponse = await fetchWithTimeout(`${daemonUrl}/health`, {}, 1000);
175
- daemonRunning = healthResponse.ok;
176
- } catch {}
174
+ const status = await agentStatus('teleportation-daemon');
175
+ daemonRunning = status.state === 'online' || status.running === true;
176
+ if (env.DEBUG && daemonRunning) {
177
+ console.error(`[SessionStart] Daemon already running (PID: ${status.pid})`);
178
+ }
179
+ } catch (statusError) {
180
+ // Agent not installed or not running - need to start it
181
+ if (env.DEBUG) {
182
+ console.error(`[SessionStart] Daemon status check: ${statusError.message}`);
183
+ }
184
+ }
177
185
 
178
186
  // Start daemon if not running
179
187
  if (!daemonRunning) {
@@ -209,63 +217,40 @@ function updateSessionMarker(sessionId) {
209
217
  return exit(0);
210
218
  }
211
219
 
212
- // Retry daemon start with exponential backoff
213
- let retries = 3;
214
- let daemonStarted = false;
215
-
216
- while (retries > 0 && !daemonStarted) {
217
- try {
218
- // Spawn daemon process
219
- spawn(process.execPath, [daemonScript], {
220
- detached: true,
221
- stdio: 'ignore',
222
- env: {
223
- ...process.env,
224
- TELEPORTATION_DAEMON: 'true',
225
- RELAY_API_URL,
226
- RELAY_API_KEY,
227
- TELEPORTATION_DAEMON_PORT: DAEMON_PORT
228
- }
229
- }).unref();
220
+ // Start daemon using agent-runtime
221
+ // This handles retries, health checks, and platform-native service installation
222
+ try {
223
+ const handle = await agentStart({
224
+ name: 'teleportation-daemon',
225
+ script: daemonScript,
226
+ port: parseInt(DAEMON_PORT),
227
+ interpreter: process.execPath, // Use bun
228
+ env: {
229
+ ...process.env,
230
+ TELEPORTATION_DAEMON: 'true',
231
+ RELAY_API_URL,
232
+ RELAY_API_KEY,
233
+ TELEPORTATION_DAEMON_PORT: DAEMON_PORT,
234
+ // Disable idle timeout - daemon should stay running indefinitely
235
+ DAEMON_IDLE_TIMEOUT_MS: process.env.DAEMON_IDLE_TIMEOUT_MS || '0'
236
+ },
237
+ restart: true, // Auto-restart on crash
238
+ maxRestarts: 10,
239
+ restartBackoff: 1000,
240
+ maxMemory: '500M',
241
+ });
230
242
 
231
- // Wait for daemon to start with increasing delays
232
- const waitTime = 500 * (4 - retries); // 500ms, 1000ms, 1500ms
233
- await new Promise(r => setTimeout(r, waitTime));
234
-
235
- // Verify daemon is actually running
236
- try {
237
- const healthCheck = await fetchWithTimeout(`${daemonUrl}/health`, {}, 2000);
238
- if (healthCheck.ok) {
239
- daemonStarted = true;
240
- if (env.DEBUG) {
241
- console.error('[SessionStart] Daemon started successfully');
242
- }
243
- break;
244
- }
245
- } catch (healthError) {
246
- if (env.DEBUG) {
247
- console.error(`[SessionStart] Health check failed: ${healthError.message}`);
248
- }
249
- }
250
-
251
- retries--;
252
- if (retries === 0 && !daemonStarted) {
253
- if (env.DEBUG) {
254
- console.error('[SessionStart] Failed to start daemon after 3 attempts');
255
- }
256
- // Set flag to disable daemon for this session
257
- process.env.TELEPORTATION_DAEMON_DISABLED = 'true';
258
- }
259
- } catch (spawnError) {
260
- retries--;
261
- if (env.DEBUG) {
262
- console.error(`[SessionStart] Daemon spawn error (${3 - retries}/3):`, spawnError.message);
263
- }
264
- if (retries > 0) {
265
- // Exponential backoff between retries
266
- await new Promise(r => setTimeout(r, 1000 * (4 - retries)));
267
- }
243
+ if (env.DEBUG) {
244
+ console.error(`[SessionStart] Daemon started successfully (PID: ${handle.pid}, Platform: ${handle.platform})`);
245
+ }
246
+ } catch (startError) {
247
+ console.error(`\n⚠️ Teleportation daemon failed to start: ${startError.message}`);
248
+ console.error(' Sessions may appear INACTIVE. Run: teleportation doctor\n');
249
+ if (env.DEBUG) {
250
+ console.error(`[SessionStart] Stack: ${startError.stack}`);
268
251
  }
252
+ // Set flag to disable daemon for this session
253
+ process.env.TELEPORTATION_DAEMON_DISABLED = 'true';
269
254
  }
270
255
  }
271
256