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.
- package/.claude/hooks/permission_request.mjs +11 -4
- package/.claude/hooks/post_tool_use.mjs +1 -3
- package/.claude/hooks/pre_tool_use.mjs +255 -289
- package/.claude/hooks/session-register.mjs +44 -29
- package/.claude/hooks/session_end.mjs +29 -3
- package/.claude/hooks/session_start.mjs +57 -1
- package/.claude/hooks/stop.mjs +245 -242
- package/.claude/hooks/user_prompt_submit.mjs +1 -3
- package/lib/config/manager.js +45 -1
- package/lib/daemon/session-file-registry.js +207 -0
- package/lib/daemon/task-executor-v2.js +239 -29
- package/lib/daemon/teleportation-daemon.js +469 -29
- package/lib/daemon/timeline-analyzer.js +19 -13
- package/lib/daemon/transcript-ingestion.js +310 -51
- package/lib/daemon/utils.js +0 -9
- package/lib/install/installer.js +126 -3
- package/lib/install/uhr-installer.js +32 -18
- package/lib/intelligence/benchmark.js +240 -0
- package/lib/intelligence/index.js +29 -0
- package/lib/intelligence/rebuild-policies.js +169 -0
- package/lib/intelligence/schema.js +259 -0
- package/lib/intelligence/transcript-mine.js +339 -0
- package/lib/session/metadata.js +23 -5
- package/lib/transcript-sync/lifecycle.js +88 -0
- package/lib/transcript-sync/repo-context.js +45 -0
- package/lib/transcript-sync/worker.js +233 -0
- package/lib/utils/log-sanitizer.js +65 -0
- package/package.json +2 -1
- package/scripts/sync-transcripts.sh +272 -0
- package/teleportation-cli.cjs +295 -4
|
@@ -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
|
-
//
|
|
69
|
-
let metadata
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
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) {
|