teleportation-cli 1.0.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.
- package/.claude/hooks/config-loader.mjs +93 -0
- package/.claude/hooks/heartbeat.mjs +331 -0
- package/.claude/hooks/notification.mjs +35 -0
- package/.claude/hooks/permission_request.mjs +307 -0
- package/.claude/hooks/post_tool_use.mjs +137 -0
- package/.claude/hooks/pre_tool_use.mjs +451 -0
- package/.claude/hooks/session-register.mjs +274 -0
- package/.claude/hooks/session_end.mjs +256 -0
- package/.claude/hooks/session_start.mjs +308 -0
- package/.claude/hooks/stop.mjs +277 -0
- package/.claude/hooks/user_prompt_submit.mjs +91 -0
- package/LICENSE +21 -0
- package/README.md +243 -0
- package/lib/auth/api-key.js +110 -0
- package/lib/auth/credentials.js +341 -0
- package/lib/backup/manager.js +461 -0
- package/lib/cli/daemon-commands.js +299 -0
- package/lib/cli/index.js +303 -0
- package/lib/cli/session-commands.js +294 -0
- package/lib/cli/snapshot-commands.js +223 -0
- package/lib/cli/worktree-commands.js +291 -0
- package/lib/config/manager.js +306 -0
- package/lib/daemon/lifecycle.js +336 -0
- package/lib/daemon/pid-manager.js +160 -0
- package/lib/daemon/teleportation-daemon.js +2009 -0
- package/lib/handoff/config.js +102 -0
- package/lib/handoff/example.js +152 -0
- package/lib/handoff/git-handoff.js +351 -0
- package/lib/handoff/handoff.js +277 -0
- package/lib/handoff/index.js +25 -0
- package/lib/handoff/session-state.js +238 -0
- package/lib/install/installer.js +555 -0
- package/lib/machine-coders/claude-code-adapter.js +329 -0
- package/lib/machine-coders/example.js +239 -0
- package/lib/machine-coders/gemini-cli-adapter.js +406 -0
- package/lib/machine-coders/index.js +103 -0
- package/lib/machine-coders/interface.js +168 -0
- package/lib/router/classifier.js +251 -0
- package/lib/router/example.js +92 -0
- package/lib/router/index.js +69 -0
- package/lib/router/mech-llms-client.js +277 -0
- package/lib/router/models.js +188 -0
- package/lib/router/router.js +382 -0
- package/lib/session/cleanup.js +100 -0
- package/lib/session/metadata.js +258 -0
- package/lib/session/mute-checker.js +114 -0
- package/lib/session-registry/manager.js +302 -0
- package/lib/snapshot/manager.js +390 -0
- package/lib/utils/errors.js +166 -0
- package/lib/utils/logger.js +148 -0
- package/lib/utils/retry.js +155 -0
- package/lib/worktree/manager.js +301 -0
- package/package.json +66 -0
- package/teleportation-cli.cjs +2987 -0
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Session Registration Helper
|
|
4
|
+
* Registers a session with the relay API if not already registered.
|
|
5
|
+
* This is called lazily when an approval is created or a message is sent.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { env } from 'node:process';
|
|
9
|
+
import { fileURLToPath } from 'url';
|
|
10
|
+
import { dirname, join } from 'path';
|
|
11
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
12
|
+
import { homedir, tmpdir } from 'node:os';
|
|
13
|
+
|
|
14
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
15
|
+
const __dirname = dirname(__filename);
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Load version info from ~/.teleportation/version.json
|
|
19
|
+
* Returns null if file doesn't exist (old installation)
|
|
20
|
+
*/
|
|
21
|
+
async function loadVersionInfo() {
|
|
22
|
+
const versionFile = join(homedir(), '.teleportation', 'version.json');
|
|
23
|
+
try {
|
|
24
|
+
const content = await readFile(versionFile, 'utf8');
|
|
25
|
+
return JSON.parse(content);
|
|
26
|
+
} catch (e) {
|
|
27
|
+
// Version file doesn't exist - old installation
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Register a session with the relay API if not already registered
|
|
34
|
+
* @param {string} session_id - Session ID
|
|
35
|
+
* @param {string} cwd - Current working directory
|
|
36
|
+
* @param {object} config - Config object with relayApiUrl and relayApiKey
|
|
37
|
+
* @returns {Promise<boolean>} - True if registered successfully
|
|
38
|
+
*/
|
|
39
|
+
export async function ensureSessionRegistered(session_id, cwd, config) {
|
|
40
|
+
const RELAY_API_URL = config.relayApiUrl || '';
|
|
41
|
+
const RELAY_API_KEY = config.relayApiKey || '';
|
|
42
|
+
|
|
43
|
+
if (!session_id || !RELAY_API_URL || !RELAY_API_KEY) {
|
|
44
|
+
if (env.DEBUG) {
|
|
45
|
+
console.error(`[SessionRegister] Early return: session_id=${!!session_id}, RELAY_API_URL=${!!RELAY_API_URL}, RELAY_API_KEY=${!!RELAY_API_KEY}`);
|
|
46
|
+
}
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Check if session is already registered (marker file exists)
|
|
51
|
+
// If so, skip API call - heartbeat keeps the session alive
|
|
52
|
+
const registrationMarker = join(tmpdir(), `teleportation-session-${session_id}.registered`);
|
|
53
|
+
try {
|
|
54
|
+
await readFile(registrationMarker);
|
|
55
|
+
// Marker exists - session already registered, skip API call
|
|
56
|
+
if (env.DEBUG) {
|
|
57
|
+
console.error(`[SessionRegister] Session ${session_id} already registered, skipping`);
|
|
58
|
+
}
|
|
59
|
+
return true;
|
|
60
|
+
} catch (e) {
|
|
61
|
+
// Marker doesn't exist - proceed with registration
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (env.DEBUG) {
|
|
65
|
+
console.error(`[SessionRegister] Registering session ${session_id}`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Extract enhanced session metadata
|
|
69
|
+
let metadata = { cwd };
|
|
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
|
+
];
|
|
77
|
+
|
|
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
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (metadataModule && metadataModule.extractSessionMetadata && cwd) {
|
|
89
|
+
const extracted = await metadataModule.extractSessionMetadata(cwd);
|
|
90
|
+
extracted.session_id = session_id;
|
|
91
|
+
metadata = extracted;
|
|
92
|
+
}
|
|
93
|
+
} catch (e) {
|
|
94
|
+
// If metadata extraction fails, fall back to basic metadata
|
|
95
|
+
if (env.DEBUG) {
|
|
96
|
+
console.error('[SessionRegister] Failed to extract metadata:', e.message);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Add version info to metadata
|
|
101
|
+
try {
|
|
102
|
+
const versionInfo = await loadVersionInfo();
|
|
103
|
+
if (versionInfo) {
|
|
104
|
+
metadata.teleportation_version = versionInfo.version;
|
|
105
|
+
metadata.protocol_version = versionInfo.protocol_version;
|
|
106
|
+
}
|
|
107
|
+
} catch (e) {
|
|
108
|
+
// Version info not available - old installation
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
if (env.DEBUG) {
|
|
113
|
+
console.error(`[SessionRegister] Calling ${RELAY_API_URL}/api/sessions/register`);
|
|
114
|
+
}
|
|
115
|
+
const response = await fetch(`${RELAY_API_URL}/api/sessions/register`, {
|
|
116
|
+
method: 'POST',
|
|
117
|
+
headers: {
|
|
118
|
+
'Content-Type': 'application/json',
|
|
119
|
+
'Authorization': `Bearer ${RELAY_API_KEY}`
|
|
120
|
+
},
|
|
121
|
+
body: JSON.stringify({ session_id, meta: metadata })
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
if (response.ok || response.status === 200) {
|
|
125
|
+
// Create marker file so we don't re-register on every tool use
|
|
126
|
+
try {
|
|
127
|
+
await writeFile(registrationMarker, JSON.stringify({
|
|
128
|
+
session_id,
|
|
129
|
+
registered_at: Date.now()
|
|
130
|
+
}), { mode: 0o600 });
|
|
131
|
+
} catch (e) {
|
|
132
|
+
// Ignore marker write failures - registration still succeeded
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Start heartbeat if not already running
|
|
136
|
+
try {
|
|
137
|
+
const heartbeatEnabled = config.session?.heartbeat?.enabled !== false;
|
|
138
|
+
const heartbeatInterval = config.session?.heartbeat?.interval || 120000;
|
|
139
|
+
const startDelay = config.session?.heartbeat?.startDelay || 5000;
|
|
140
|
+
const maxFailures = config.session?.heartbeat?.maxFailures || 3;
|
|
141
|
+
|
|
142
|
+
if (heartbeatEnabled && RELAY_API_URL && RELAY_API_KEY) {
|
|
143
|
+
const { spawn } = await import('child_process');
|
|
144
|
+
const heartbeatPath = join(__dirname, 'heartbeat.mjs');
|
|
145
|
+
|
|
146
|
+
// Check if heartbeat is already running for this session
|
|
147
|
+
const { tmpdir } = await import('os');
|
|
148
|
+
const { readFile } = await import('fs/promises');
|
|
149
|
+
const pidFile = join(tmpdir(), `teleportation-heartbeat-${session_id}.pid`);
|
|
150
|
+
|
|
151
|
+
let shouldStartHeartbeat = true;
|
|
152
|
+
try {
|
|
153
|
+
await readFile(pidFile);
|
|
154
|
+
// PID file exists, heartbeat might be running
|
|
155
|
+
shouldStartHeartbeat = false;
|
|
156
|
+
} catch (e) {
|
|
157
|
+
// PID file doesn't exist, start heartbeat
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (shouldStartHeartbeat) {
|
|
161
|
+
const heartbeat = spawn('node', [heartbeatPath, session_id], {
|
|
162
|
+
detached: true,
|
|
163
|
+
stdio: 'ignore',
|
|
164
|
+
env: {
|
|
165
|
+
...process.env,
|
|
166
|
+
SESSION_ID: session_id,
|
|
167
|
+
RELAY_API_URL,
|
|
168
|
+
RELAY_API_KEY,
|
|
169
|
+
HEARTBEAT_INTERVAL: String(heartbeatInterval),
|
|
170
|
+
START_DELAY: String(startDelay),
|
|
171
|
+
MAX_FAILURES: String(maxFailures),
|
|
172
|
+
TELEPORTATION_DEBUG: process.env.TELEPORTATION_DEBUG || 'false'
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
heartbeat.unref();
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
} catch (error) {
|
|
179
|
+
// Don't fail registration if heartbeat spawn fails
|
|
180
|
+
if (env.DEBUG) {
|
|
181
|
+
console.error('[SessionRegister] Failed to spawn heartbeat:', error.message);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
} catch (e) {
|
|
188
|
+
// Registration failed - this is okay, we'll try again next time
|
|
189
|
+
if (env.DEBUG) {
|
|
190
|
+
console.error('[SessionRegister] Failed to register session:', e.message);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Update session metadata without triggering a registration event
|
|
199
|
+
* Use this for updating dynamic metadata like current_branch, last_file_edited, etc.
|
|
200
|
+
* @param {string} session_id - Session ID
|
|
201
|
+
* @param {string} cwd - Current working directory
|
|
202
|
+
* @param {object} config - Config object with relayApiUrl and relayApiKey
|
|
203
|
+
* @returns {Promise<boolean>} - True if updated successfully
|
|
204
|
+
*/
|
|
205
|
+
export async function updateSessionMetadata(session_id, cwd, config) {
|
|
206
|
+
const RELAY_API_URL = config.relayApiUrl || '';
|
|
207
|
+
const RELAY_API_KEY = config.relayApiKey || '';
|
|
208
|
+
|
|
209
|
+
if (!session_id || !RELAY_API_URL || !RELAY_API_KEY) {
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Extract current session metadata (without started_at - that's preserved server-side)
|
|
214
|
+
let metadata = { cwd };
|
|
215
|
+
try {
|
|
216
|
+
const possiblePaths = [
|
|
217
|
+
join(__dirname, '..', '..', 'lib', 'session', 'metadata.js'),
|
|
218
|
+
join(process.env.HOME || process.env.USERPROFILE || '', '.teleportation', 'lib', 'session', 'metadata.js'),
|
|
219
|
+
'./lib/session/metadata.js'
|
|
220
|
+
];
|
|
221
|
+
|
|
222
|
+
let metadataModule = null;
|
|
223
|
+
for (const path of possiblePaths) {
|
|
224
|
+
try {
|
|
225
|
+
metadataModule = await import('file://' + path);
|
|
226
|
+
break;
|
|
227
|
+
} catch (e) {
|
|
228
|
+
// Try next path
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (metadataModule && metadataModule.extractSessionMetadata && cwd) {
|
|
233
|
+
const extracted = await metadataModule.extractSessionMetadata(cwd);
|
|
234
|
+
extracted.session_id = session_id;
|
|
235
|
+
metadata = extracted;
|
|
236
|
+
}
|
|
237
|
+
} catch (e) {
|
|
238
|
+
// If metadata extraction fails, fall back to basic metadata
|
|
239
|
+
if (env.DEBUG) {
|
|
240
|
+
console.error('[SessionUpdate] Failed to extract metadata:', e.message);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
try {
|
|
245
|
+
const response = await fetch(`${RELAY_API_URL}/api/sessions/${session_id}`, {
|
|
246
|
+
method: 'PATCH',
|
|
247
|
+
headers: {
|
|
248
|
+
'Content-Type': 'application/json',
|
|
249
|
+
'Authorization': `Bearer ${RELAY_API_KEY}`
|
|
250
|
+
},
|
|
251
|
+
body: JSON.stringify({ meta: metadata })
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
if (response.ok || response.status === 200) {
|
|
255
|
+
if (env.DEBUG) {
|
|
256
|
+
console.error(`[SessionUpdate] Session metadata updated: ${session_id}`);
|
|
257
|
+
}
|
|
258
|
+
return true;
|
|
259
|
+
} else if (response.status === 404) {
|
|
260
|
+
// Session doesn't exist - need to register first
|
|
261
|
+
if (env.DEBUG) {
|
|
262
|
+
console.error(`[SessionUpdate] Session not found, needs registration: ${session_id}`);
|
|
263
|
+
}
|
|
264
|
+
return false;
|
|
265
|
+
}
|
|
266
|
+
} catch (e) {
|
|
267
|
+
if (env.DEBUG) {
|
|
268
|
+
console.error('[SessionUpdate] Failed to update session:', e.message);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return false;
|
|
273
|
+
}
|
|
274
|
+
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { stdin, exit, env } from 'node:process';
|
|
4
|
+
import { readFile, unlink } from 'fs/promises';
|
|
5
|
+
import { tmpdir } from 'os';
|
|
6
|
+
import { join } from 'path';
|
|
7
|
+
|
|
8
|
+
const readStdin = () => new Promise((resolve, reject) => {
|
|
9
|
+
let data='';
|
|
10
|
+
stdin.setEncoding('utf8');
|
|
11
|
+
stdin.on('data', c => data += c);
|
|
12
|
+
stdin.on('end', () => resolve(data));
|
|
13
|
+
stdin.on('error', reject);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
(async () => {
|
|
17
|
+
let input = {};
|
|
18
|
+
try {
|
|
19
|
+
const raw = await readStdin();
|
|
20
|
+
input = JSON.parse(raw || '{}');
|
|
21
|
+
} catch {}
|
|
22
|
+
|
|
23
|
+
const { session_id } = input || {};
|
|
24
|
+
|
|
25
|
+
// Load config from encrypted credentials, legacy config file, or env vars
|
|
26
|
+
let config;
|
|
27
|
+
try {
|
|
28
|
+
const { loadConfig } = await import('./config-loader.mjs');
|
|
29
|
+
config = await loadConfig();
|
|
30
|
+
} catch (e) {
|
|
31
|
+
// Fallback to environment variables if config loader fails
|
|
32
|
+
config = {
|
|
33
|
+
relayApiUrl: env.RELAY_API_URL || '',
|
|
34
|
+
relayApiKey: env.RELAY_API_KEY || ''
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const RELAY_API_URL = config.relayApiUrl || '';
|
|
39
|
+
const RELAY_API_KEY = config.relayApiKey || '';
|
|
40
|
+
const DAEMON_PORT = config.daemonPort || env.TELEPORTATION_DAEMON_PORT || '3050';
|
|
41
|
+
const DAEMON_ENABLED = config.daemonEnabled !== false && env.TELEPORTATION_DAEMON_ENABLED !== 'false';
|
|
42
|
+
|
|
43
|
+
const updateSessionDaemonState = async (updates) => {
|
|
44
|
+
if (!session_id || !RELAY_API_URL || !RELAY_API_KEY) return;
|
|
45
|
+
try {
|
|
46
|
+
await fetch(`${RELAY_API_URL}/api/sessions/${session_id}/daemon-state`, {
|
|
47
|
+
method: 'PATCH',
|
|
48
|
+
headers: {
|
|
49
|
+
'Content-Type': 'application/json',
|
|
50
|
+
'Authorization': `Bearer ${RELAY_API_KEY}`
|
|
51
|
+
},
|
|
52
|
+
body: JSON.stringify(updates)
|
|
53
|
+
});
|
|
54
|
+
} catch {}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// Kill heartbeat background process if running
|
|
58
|
+
if (session_id) {
|
|
59
|
+
try {
|
|
60
|
+
const pidFile = join(tmpdir(), `teleportation-heartbeat-${session_id}.pid`);
|
|
61
|
+
const pidContent = await readFile(pidFile, 'utf8');
|
|
62
|
+
|
|
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
|
+
}
|
|
71
|
+
|
|
72
|
+
const pid = pidData.pid;
|
|
73
|
+
|
|
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
|
+
}
|
|
97
|
+
|
|
98
|
+
// Delete PID file
|
|
99
|
+
try {
|
|
100
|
+
await unlink(pidFile);
|
|
101
|
+
} catch (unlinkError) {
|
|
102
|
+
// Ignore errors - file might not exist
|
|
103
|
+
}
|
|
104
|
+
} catch (error) {
|
|
105
|
+
// Ignore errors reading PID file - heartbeat might not have been started
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Delete registration marker file
|
|
109
|
+
try {
|
|
110
|
+
const registrationMarker = join(tmpdir(), `teleportation-session-${session_id}.registered`);
|
|
111
|
+
await unlink(registrationMarker);
|
|
112
|
+
} catch (e) {
|
|
113
|
+
// Ignore errors - marker might not exist
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Clean up model tracking files
|
|
117
|
+
try {
|
|
118
|
+
const lastModelFile = join(tmpdir(), `teleportation-last-model-${session_id}.txt`);
|
|
119
|
+
await unlink(lastModelFile);
|
|
120
|
+
} catch (e) {
|
|
121
|
+
// Ignore errors - file may not exist
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Clean up model change marker file (if it exists)
|
|
125
|
+
try {
|
|
126
|
+
const modelChangeMarker = join(tmpdir(), `teleportation-model-changing-${session_id}.txt`);
|
|
127
|
+
await unlink(modelChangeMarker);
|
|
128
|
+
} catch (e) {
|
|
129
|
+
// Ignore errors - file may not exist
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Clean up session resources
|
|
134
|
+
if (session_id) {
|
|
135
|
+
try {
|
|
136
|
+
// Cache module loading to prevent memory leaks from repeated imports
|
|
137
|
+
if (!global.__teleportationCleanup) {
|
|
138
|
+
const { fileURLToPath } = await import('url');
|
|
139
|
+
const { dirname, join } = await import('path');
|
|
140
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
141
|
+
const __dirname = dirname(__filename);
|
|
142
|
+
|
|
143
|
+
// Try to load cleanup utility from multiple possible locations
|
|
144
|
+
const possiblePaths = [
|
|
145
|
+
join(__dirname, '..', '..', 'lib', 'session', 'cleanup.js'),
|
|
146
|
+
join(process.env.HOME || process.env.USERPROFILE || '', '.teleportation', 'lib', 'session', 'cleanup.js'),
|
|
147
|
+
'./lib/session/cleanup.js'
|
|
148
|
+
];
|
|
149
|
+
|
|
150
|
+
for (const path of possiblePaths) {
|
|
151
|
+
try {
|
|
152
|
+
global.__teleportationCleanup = await import('file://' + path);
|
|
153
|
+
break;
|
|
154
|
+
} catch (e) {
|
|
155
|
+
// Try next path
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const cleanupModule = global.__teleportationCleanup;
|
|
161
|
+
// Use cleanup utility if available, otherwise fall back to direct mute cache clearing
|
|
162
|
+
if (cleanupModule && cleanupModule.cleanupSession) {
|
|
163
|
+
await cleanupModule.cleanupSession(session_id);
|
|
164
|
+
} else {
|
|
165
|
+
// Fallback: try to clear mute cache directly
|
|
166
|
+
if (!global.__teleportationMuteChecker) {
|
|
167
|
+
const { fileURLToPath } = await import('url');
|
|
168
|
+
const { dirname, join } = await import('path');
|
|
169
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
170
|
+
const __dirname = dirname(__filename);
|
|
171
|
+
|
|
172
|
+
const mutePaths = [
|
|
173
|
+
join(__dirname, '..', '..', 'lib', 'session', 'mute-checker.js'),
|
|
174
|
+
join(process.env.HOME || process.env.USERPROFILE || '', '.teleportation', 'lib', 'session', 'mute-checker.js'),
|
|
175
|
+
'./lib/session/mute-checker.js'
|
|
176
|
+
];
|
|
177
|
+
|
|
178
|
+
for (const path of mutePaths) {
|
|
179
|
+
try {
|
|
180
|
+
global.__teleportationMuteChecker = await import('file://' + path);
|
|
181
|
+
break;
|
|
182
|
+
} catch (e) {
|
|
183
|
+
// Try next path
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const muteChecker = global.__teleportationMuteChecker;
|
|
189
|
+
if (muteChecker && muteChecker.clearMuteCache) {
|
|
190
|
+
muteChecker.clearMuteCache(session_id);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
} catch (e) {
|
|
194
|
+
// Ignore errors in cleanup - session end should always succeed
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (session_id) {
|
|
199
|
+
await updateSessionDaemonState({
|
|
200
|
+
status: 'stopped',
|
|
201
|
+
started_reason: null,
|
|
202
|
+
is_away: false,
|
|
203
|
+
stopped_reason: 'session_end'
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Deregister session with daemon
|
|
208
|
+
if (DAEMON_ENABLED && session_id) {
|
|
209
|
+
try {
|
|
210
|
+
const daemonUrl = `http://127.0.0.1:${DAEMON_PORT}`;
|
|
211
|
+
|
|
212
|
+
// Add timeout to prevent hanging if daemon is unresponsive
|
|
213
|
+
const controller = new AbortController();
|
|
214
|
+
const timeoutId = setTimeout(() => controller.abort(), 2000); // 2 second timeout
|
|
215
|
+
|
|
216
|
+
try {
|
|
217
|
+
await fetch(`${daemonUrl}/sessions/deregister`, {
|
|
218
|
+
method: 'POST',
|
|
219
|
+
headers: { 'Content-Type': 'application/json' },
|
|
220
|
+
body: JSON.stringify({ session_id }),
|
|
221
|
+
signal: controller.signal
|
|
222
|
+
});
|
|
223
|
+
clearTimeout(timeoutId);
|
|
224
|
+
if (env.DEBUG) {
|
|
225
|
+
console.error(`[SessionEnd] Deregistered session from daemon: ${session_id}`);
|
|
226
|
+
}
|
|
227
|
+
} catch (fetchError) {
|
|
228
|
+
clearTimeout(timeoutId);
|
|
229
|
+
throw fetchError; // Re-throw to be caught by outer try-catch
|
|
230
|
+
}
|
|
231
|
+
} catch (e) {
|
|
232
|
+
// Ignore errors - daemon might not be running
|
|
233
|
+
if (env.DEBUG) {
|
|
234
|
+
console.error(`[SessionEnd] Failed to deregister from daemon:`, e.message);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Deregister session with relay API
|
|
240
|
+
if (session_id && RELAY_API_URL && RELAY_API_KEY) {
|
|
241
|
+
try {
|
|
242
|
+
await fetch(`${RELAY_API_URL}/api/sessions/deregister`, {
|
|
243
|
+
method: 'POST',
|
|
244
|
+
headers: {
|
|
245
|
+
'Content-Type': 'application/json',
|
|
246
|
+
'Authorization': `Bearer ${RELAY_API_KEY}`
|
|
247
|
+
},
|
|
248
|
+
body: JSON.stringify({ session_id })
|
|
249
|
+
});
|
|
250
|
+
} catch (e) {
|
|
251
|
+
// Ignore errors - session end should always succeed even if API is unavailable
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return exit(0);
|
|
256
|
+
})();
|