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
@@ -0,0 +1,302 @@
1
+ /**
2
+ * Claude Code Transcript Exporter
3
+ *
4
+ * Converts Timeline API events to Claude Code's native JSONL format.
5
+ * Claude Code stores transcripts in ~/.claude/projects/{slug}/{sessionId}.jsonl
6
+ *
7
+ * Claude JSONL Format:
8
+ * - Each line is a valid JSON object
9
+ * - Human messages: {type: "human", content: [{type: "text", text: "..."}]}
10
+ * - Assistant messages: {type: "assistant", content: [{type: "text", text: "..."}, {type: "tool_use", ...}]}
11
+ * - Tool results are embedded in subsequent human messages
12
+ *
13
+ * @module lib/teleport/exporters/claude-exporter
14
+ */
15
+
16
+ import { homedir } from 'os';
17
+ import { join } from 'path';
18
+ import { TranscriptExporter } from './interface.js';
19
+
20
+ /**
21
+ * Maps Timeline event types to Claude message types
22
+ * @type {Object.<string, string>}
23
+ */
24
+ const EVENT_TYPE_MAP = {
25
+ user_message: 'human',
26
+ assistant_response: 'assistant',
27
+ tool_use: 'assistant',
28
+ tool_result: 'human',
29
+ thinking: 'assistant',
30
+ compact_summary: 'assistant',
31
+ };
32
+
33
+ /**
34
+ * Exporter for Claude Code's native JSONL transcript format.
35
+ *
36
+ * @extends TranscriptExporter
37
+ */
38
+ export class ClaudeTranscriptExporter extends TranscriptExporter {
39
+ name = 'claude-code';
40
+ displayName = 'Claude Code';
41
+
42
+ /**
43
+ * Export timeline events to Claude Code JSONL format.
44
+ *
45
+ * @param {import('./interface.js').TimelineEvent[]} events - Timeline events
46
+ * @param {import('./interface.js').ExportOptions} [options={}] - Export options
47
+ * @returns {Promise<string>} JSONL content (newline-separated JSON objects)
48
+ */
49
+ async export(events, options = {}) {
50
+ const { includeThinking = true } = options;
51
+
52
+ // Validate and sort events
53
+ this.validateEvents(events);
54
+
55
+ if (events.length === 0) {
56
+ return '';
57
+ }
58
+
59
+ const sorted = this.sortEvents(events);
60
+ const messages = [];
61
+
62
+ // Group events into messages
63
+ // Claude expects alternating human/assistant messages
64
+ let pendingToolResults = [];
65
+
66
+ for (const event of sorted) {
67
+ const claudeMessage = this._convertEvent(event, { includeThinking });
68
+
69
+ if (!claudeMessage) {
70
+ continue;
71
+ }
72
+
73
+ // Tool results get batched and added to the next human message
74
+ if (event.event_type === 'tool_result') {
75
+ pendingToolResults.push(claudeMessage.content[0]);
76
+ continue;
77
+ }
78
+
79
+ // If we have pending tool results and this is a human message, add them
80
+ if (claudeMessage.type === 'human' && pendingToolResults.length > 0) {
81
+ claudeMessage.content = [...pendingToolResults, ...claudeMessage.content];
82
+ pendingToolResults = [];
83
+ }
84
+
85
+ // If we have pending tool results and this is an assistant message,
86
+ // create a synthetic human message with just the tool results first
87
+ if (claudeMessage.type === 'assistant' && pendingToolResults.length > 0) {
88
+ messages.push({
89
+ type: 'human',
90
+ content: pendingToolResults,
91
+ });
92
+ pendingToolResults = [];
93
+ }
94
+
95
+ messages.push(claudeMessage);
96
+ }
97
+
98
+ // If we still have pending tool results, add them as a final human message
99
+ if (pendingToolResults.length > 0) {
100
+ messages.push({
101
+ type: 'human',
102
+ content: pendingToolResults,
103
+ });
104
+ }
105
+
106
+ // Convert to JSONL (each message on its own line)
107
+ return messages.map((msg) => JSON.stringify(msg)).join('\n') + '\n';
108
+ }
109
+
110
+ /**
111
+ * Get the Claude Code transcript path.
112
+ *
113
+ * @param {string} sessionId - Session identifier (UUID)
114
+ * @param {string} projectSlug - Project slug from Claude Code
115
+ * @returns {string} Path like ~/.claude/projects/{slug}/{sessionId}.jsonl
116
+ */
117
+ getTranscriptPath(sessionId, projectSlug) {
118
+ const home = homedir();
119
+ const slug = this._normalizeSlug(projectSlug);
120
+ return join(home, '.claude', 'projects', slug, `${sessionId}.jsonl`);
121
+ }
122
+
123
+ /**
124
+ * Convert a timeline event to Claude message format.
125
+ *
126
+ * @private
127
+ * @param {import('./interface.js').TimelineEvent} event - Timeline event
128
+ * @param {Object} options - Conversion options
129
+ * @param {boolean} options.includeThinking - Include thinking blocks
130
+ * @returns {Object|null} Claude message object or null if skipped
131
+ */
132
+ _convertEvent(event, { includeThinking }) {
133
+ const { event_type, data } = event;
134
+
135
+ switch (event_type) {
136
+ case 'user_message':
137
+ return this._convertUserMessage(data);
138
+
139
+ case 'assistant_response':
140
+ return this._convertAssistantResponse(data);
141
+
142
+ case 'tool_use':
143
+ return this._convertToolUse(data);
144
+
145
+ case 'tool_result':
146
+ return this._convertToolResult(data);
147
+
148
+ case 'thinking':
149
+ if (!includeThinking) return null;
150
+ return this._convertThinking(data);
151
+
152
+ case 'compact_summary':
153
+ return this._convertCompactSummary(data);
154
+
155
+ default:
156
+ // Unknown event type - skip silently
157
+ return null;
158
+ }
159
+ }
160
+
161
+ /**
162
+ * Convert user_message event to Claude human message.
163
+ *
164
+ * @private
165
+ * @param {Object} data - Event data
166
+ * @returns {Object} Claude human message
167
+ */
168
+ _convertUserMessage(data) {
169
+ const content = data.content || data.text || data.message || '';
170
+ return {
171
+ type: 'human',
172
+ content: [{ type: 'text', text: String(content) }],
173
+ };
174
+ }
175
+
176
+ /**
177
+ * Convert assistant_response event to Claude assistant message.
178
+ *
179
+ * @private
180
+ * @param {Object} data - Event data
181
+ * @returns {Object} Claude assistant message
182
+ */
183
+ _convertAssistantResponse(data) {
184
+ const content = data.content || data.message || data.text || data.response || '';
185
+ return {
186
+ type: 'assistant',
187
+ content: [{ type: 'text', text: String(content) }],
188
+ };
189
+ }
190
+
191
+ /**
192
+ * Convert tool_use event to Claude assistant message with tool_use content.
193
+ *
194
+ * @private
195
+ * @param {Object} data - Event data
196
+ * @returns {Object} Claude assistant message with tool_use
197
+ */
198
+ _convertToolUse(data) {
199
+ const toolUse = {
200
+ type: 'tool_use',
201
+ id: data.tool_id || data.id || `tool_${Date.now()}`,
202
+ name: data.tool || data.tool_name || data.name || 'unknown',
203
+ input: data.input || data.parameters || data.args || {},
204
+ };
205
+
206
+ return {
207
+ type: 'assistant',
208
+ content: [toolUse],
209
+ };
210
+ }
211
+
212
+ /**
213
+ * Convert tool_result event to content block for human message.
214
+ *
215
+ * @private
216
+ * @param {Object} data - Event data
217
+ * @returns {Object} Claude human message with tool_result content
218
+ */
219
+ _convertToolResult(data) {
220
+ const toolResult = {
221
+ type: 'tool_result',
222
+ tool_use_id: data.tool_id || data.tool_use_id || data.id || 'unknown',
223
+ content: data.output || data.result || data.content || '',
224
+ is_error: data.is_error || data.error || false,
225
+ };
226
+
227
+ // If is_error is truthy but content is empty, use error message
228
+ if (toolResult.is_error && !toolResult.content && data.error_message) {
229
+ toolResult.content = data.error_message;
230
+ }
231
+
232
+ return {
233
+ type: 'human',
234
+ content: [toolResult],
235
+ };
236
+ }
237
+
238
+ /**
239
+ * Convert thinking event to Claude assistant message with thinking content.
240
+ *
241
+ * @private
242
+ * @param {Object} data - Event data
243
+ * @returns {Object} Claude assistant message with thinking
244
+ */
245
+ _convertThinking(data) {
246
+ const thinking = data.thinking || data.content || data.text || '';
247
+ return {
248
+ type: 'assistant',
249
+ content: [
250
+ {
251
+ type: 'thinking',
252
+ thinking: String(thinking),
253
+ },
254
+ ],
255
+ };
256
+ }
257
+
258
+ /**
259
+ * Convert compact_summary event to Claude assistant message.
260
+ *
261
+ * @private
262
+ * @param {Object} data - Event data
263
+ * @returns {Object} Claude assistant message with summary
264
+ */
265
+ _convertCompactSummary(data) {
266
+ const summary = data.summary || data.content || data.text || '';
267
+ return {
268
+ type: 'assistant',
269
+ content: [
270
+ {
271
+ type: 'text',
272
+ text: `[Session Summary]\n${String(summary)}`,
273
+ },
274
+ ],
275
+ };
276
+ }
277
+
278
+ /**
279
+ * Normalize project slug for use in file paths.
280
+ * Claude Code uses hyphenated paths like "-Users-kefentse-dev-teleporter".
281
+ *
282
+ * @private
283
+ * @param {string} slug - Project slug or path
284
+ * @returns {string} Normalized slug
285
+ */
286
+ _normalizeSlug(slug) {
287
+ if (!slug) {
288
+ return 'default';
289
+ }
290
+
291
+ // If it's already a Claude-style slug, use it as-is
292
+ if (slug.startsWith('-')) {
293
+ return slug;
294
+ }
295
+
296
+ // Convert path-like slug to Claude format
297
+ // e.g., "/Users/kefentse/dev/project" -> "-Users-kefentse-dev-project"
298
+ return slug.replace(/\//g, '-').replace(/^-/, '');
299
+ }
300
+ }
301
+
302
+ export default ClaudeTranscriptExporter;
@@ -0,0 +1,307 @@
1
+ /**
2
+ * Gemini CLI Transcript Exporter
3
+ *
4
+ * Converts Timeline API events to Gemini CLI's native JSON format.
5
+ * Gemini CLI stores transcripts in ~/.gemini/tmp/{hash}/logs.json
6
+ *
7
+ * Gemini JSON Format:
8
+ * - Array of message objects
9
+ * - User messages: {role: "user", parts: [{text: "..."}]}
10
+ * - Model messages: {role: "model", parts: [{text: "..."}, {functionCall: {...}}]}
11
+ * - Function results: {role: "user", parts: [{functionResponse: {...}}]}
12
+ *
13
+ * @module lib/teleport/exporters/gemini-exporter
14
+ */
15
+
16
+ import { createHash } from 'crypto';
17
+ import { homedir } from 'os';
18
+ import { join } from 'path';
19
+ import { TranscriptExporter } from './interface.js';
20
+
21
+ /**
22
+ * Tool name mapping from Claude/Timeline format to Gemini format.
23
+ * Gemini CLI uses snake_case tool names.
24
+ *
25
+ * @type {Object.<string, string>}
26
+ */
27
+ const TOOL_NAME_MAP = {
28
+ Read: 'read_file',
29
+ Write: 'write_file',
30
+ Edit: 'edit_file',
31
+ Bash: 'run_terminal_command',
32
+ Glob: 'glob_files',
33
+ Grep: 'grep_search',
34
+ LS: 'list_directory',
35
+ WebSearch: 'web_search',
36
+ WebFetch: 'web_fetch',
37
+ Task: 'spawn_agent',
38
+ AskUser: 'ask_user',
39
+ AskUserQuestion: 'ask_user',
40
+ // Add more mappings as needed
41
+ };
42
+
43
+ /**
44
+ * Exporter for Gemini CLI's native JSON transcript format.
45
+ *
46
+ * @extends TranscriptExporter
47
+ */
48
+ export class GeminiTranscriptExporter extends TranscriptExporter {
49
+ name = 'gemini-cli';
50
+ displayName = 'Gemini CLI';
51
+
52
+ /**
53
+ * Export timeline events to Gemini CLI JSON format.
54
+ *
55
+ * Note: Gemini CLI does not support thinking blocks, so they are omitted.
56
+ *
57
+ * @param {import('./interface.js').TimelineEvent[]} events - Timeline events
58
+ * @param {import('./interface.js').ExportOptions} [options={}] - Export options
59
+ * @returns {Promise<string>} JSON content (pretty-printed with 2-space indent)
60
+ */
61
+ async export(events, options = {}) {
62
+ const { prettyPrint = true } = options;
63
+
64
+ // Validate and sort events
65
+ this.validateEvents(events);
66
+
67
+ if (events.length === 0) {
68
+ return prettyPrint ? '[]' : '[]';
69
+ }
70
+
71
+ const sorted = this.sortEvents(events);
72
+ const messages = [];
73
+
74
+ for (const event of sorted) {
75
+ const geminiMessage = this._convertEvent(event);
76
+
77
+ if (!geminiMessage) {
78
+ continue;
79
+ }
80
+
81
+ // Try to merge consecutive messages of the same role
82
+ const lastMessage = messages[messages.length - 1];
83
+ if (lastMessage && lastMessage.role === geminiMessage.role) {
84
+ // Merge parts
85
+ lastMessage.parts = [...lastMessage.parts, ...geminiMessage.parts];
86
+ } else {
87
+ messages.push(geminiMessage);
88
+ }
89
+ }
90
+
91
+ // Format output
92
+ return prettyPrint ? JSON.stringify(messages, null, 2) : JSON.stringify(messages);
93
+ }
94
+
95
+ /**
96
+ * Get the Gemini CLI transcript path.
97
+ *
98
+ * Gemini uses MD5 hash of project slug for directory name.
99
+ *
100
+ * @param {string} sessionId - Session identifier
101
+ * @param {string} projectSlug - Project slug/path
102
+ * @returns {string} Path like ~/.gemini/tmp/{hash}/logs.json
103
+ */
104
+ getTranscriptPath(sessionId, projectSlug) {
105
+ const home = homedir();
106
+ const hash = this._hashSlug(projectSlug);
107
+ return join(home, '.gemini', 'tmp', hash, 'logs.json');
108
+ }
109
+
110
+ /**
111
+ * Convert a timeline event to Gemini message format.
112
+ *
113
+ * @private
114
+ * @param {import('./interface.js').TimelineEvent} event - Timeline event
115
+ * @returns {Object|null} Gemini message object or null if skipped
116
+ */
117
+ _convertEvent(event) {
118
+ const { event_type, data } = event;
119
+
120
+ switch (event_type) {
121
+ case 'user_message':
122
+ return this._convertUserMessage(data);
123
+
124
+ case 'assistant_response':
125
+ return this._convertAssistantResponse(data);
126
+
127
+ case 'tool_use':
128
+ return this._convertToolUse(data);
129
+
130
+ case 'tool_result':
131
+ return this._convertToolResult(data);
132
+
133
+ case 'compact_summary':
134
+ return this._convertCompactSummary(data);
135
+
136
+ case 'thinking':
137
+ // Gemini does not support thinking blocks - skip
138
+ return null;
139
+
140
+ default:
141
+ // Unknown event type - skip silently
142
+ return null;
143
+ }
144
+ }
145
+
146
+ /**
147
+ * Convert user_message event to Gemini user message.
148
+ *
149
+ * @private
150
+ * @param {Object} data - Event data
151
+ * @returns {Object} Gemini user message
152
+ */
153
+ _convertUserMessage(data) {
154
+ const text = data.content || data.text || data.message || '';
155
+ return {
156
+ role: 'user',
157
+ parts: [{ text: String(text) }],
158
+ };
159
+ }
160
+
161
+ /**
162
+ * Convert assistant_response event to Gemini model message.
163
+ *
164
+ * @private
165
+ * @param {Object} data - Event data
166
+ * @returns {Object} Gemini model message
167
+ */
168
+ _convertAssistantResponse(data) {
169
+ const text = data.content || data.message || data.text || data.response || '';
170
+ return {
171
+ role: 'model',
172
+ parts: [{ text: String(text) }],
173
+ };
174
+ }
175
+
176
+ /**
177
+ * Convert tool_use event to Gemini model message with functionCall.
178
+ *
179
+ * @private
180
+ * @param {Object} data - Event data
181
+ * @returns {Object} Gemini model message with functionCall
182
+ */
183
+ _convertToolUse(data) {
184
+ const toolName = data.tool || data.tool_name || data.name || 'unknown';
185
+ const geminiName = this._mapToolName(toolName);
186
+ const args = data.input || data.parameters || data.args || {};
187
+
188
+ return {
189
+ role: 'model',
190
+ parts: [
191
+ {
192
+ functionCall: {
193
+ name: geminiName,
194
+ args: this._convertToolArgs(toolName, args),
195
+ },
196
+ },
197
+ ],
198
+ };
199
+ }
200
+
201
+ /**
202
+ * Convert tool_result event to Gemini user message with functionResponse.
203
+ *
204
+ * @private
205
+ * @param {Object} data - Event data
206
+ * @returns {Object} Gemini user message with functionResponse
207
+ */
208
+ _convertToolResult(data) {
209
+ const toolName = data.tool || data.tool_name || data.name || 'unknown';
210
+ const geminiName = this._mapToolName(toolName);
211
+ const output = data.output || data.result || data.content || '';
212
+ const isError = data.is_error || data.error || false;
213
+
214
+ return {
215
+ role: 'user',
216
+ parts: [
217
+ {
218
+ functionResponse: {
219
+ name: geminiName,
220
+ response: {
221
+ success: !isError,
222
+ output: String(output),
223
+ },
224
+ },
225
+ },
226
+ ],
227
+ };
228
+ }
229
+
230
+ /**
231
+ * Convert compact_summary event to Gemini model message.
232
+ *
233
+ * @private
234
+ * @param {Object} data - Event data
235
+ * @returns {Object} Gemini model message
236
+ */
237
+ _convertCompactSummary(data) {
238
+ const summary = data.summary || data.content || data.text || '';
239
+ return {
240
+ role: 'model',
241
+ parts: [{ text: `[Session Summary]\n${String(summary)}` }],
242
+ };
243
+ }
244
+
245
+ /**
246
+ * Map Claude/Timeline tool name to Gemini tool name.
247
+ *
248
+ * @private
249
+ * @param {string} toolName - Original tool name
250
+ * @returns {string} Gemini-compatible tool name
251
+ */
252
+ _mapToolName(toolName) {
253
+ // Check direct mapping
254
+ if (TOOL_NAME_MAP[toolName]) {
255
+ return TOOL_NAME_MAP[toolName];
256
+ }
257
+
258
+ // Convert to snake_case if not in map
259
+ return toolName
260
+ .replace(/([A-Z])/g, '_$1')
261
+ .toLowerCase()
262
+ .replace(/^_/, '');
263
+ }
264
+
265
+ /**
266
+ * Convert tool arguments to Gemini format.
267
+ * Some tools have different argument names.
268
+ *
269
+ * @private
270
+ * @param {string} toolName - Original tool name
271
+ * @param {Object} args - Original arguments
272
+ * @returns {Object} Gemini-compatible arguments
273
+ */
274
+ _convertToolArgs(toolName, args) {
275
+ // Map common argument names
276
+ const argMap = {
277
+ file_path: 'path',
278
+ command: 'cmd',
279
+ pattern: 'query',
280
+ };
281
+
282
+ const converted = {};
283
+ for (const [key, value] of Object.entries(args)) {
284
+ const newKey = argMap[key] || key;
285
+ converted[newKey] = value;
286
+ }
287
+
288
+ return converted;
289
+ }
290
+
291
+ /**
292
+ * Generate MD5 hash of project slug for Gemini's directory naming.
293
+ *
294
+ * @private
295
+ * @param {string} slug - Project slug/path
296
+ * @returns {string} MD5 hash (16 characters)
297
+ */
298
+ _hashSlug(slug) {
299
+ if (!slug) {
300
+ return createHash('md5').update('default').digest('hex').substring(0, 16);
301
+ }
302
+
303
+ return createHash('md5').update(slug).digest('hex').substring(0, 16);
304
+ }
305
+ }
306
+
307
+ export default GeminiTranscriptExporter;
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Transcript Exporters
3
+ *
4
+ * CLI-agnostic exporters that convert Timeline API events to native CLI formats.
5
+ *
6
+ * @module lib/teleport/exporters
7
+ */
8
+
9
+ export { TranscriptExporter } from './interface.js';
10
+ export { ClaudeTranscriptExporter } from './claude-exporter.js';
11
+ export { GeminiTranscriptExporter } from './gemini-exporter.js';
12
+
13
+ /**
14
+ * Coder name constants for exporter selection
15
+ * @type {Object.<string, string>}
16
+ */
17
+ export const CODER_NAMES = {
18
+ CLAUDE_CODE: 'claude-code',
19
+ GEMINI_CLI: 'gemini-cli',
20
+ };
21
+
22
+ /**
23
+ * Registry of available exporters
24
+ * @type {Object.<string, typeof TranscriptExporter>}
25
+ */
26
+ const EXPORTERS = {
27
+ [CODER_NAMES.CLAUDE_CODE]: () => import('./claude-exporter.js').then(m => m.ClaudeTranscriptExporter),
28
+ [CODER_NAMES.GEMINI_CLI]: () => import('./gemini-exporter.js').then(m => m.GeminiTranscriptExporter),
29
+ };
30
+
31
+ /**
32
+ * Get the appropriate exporter for a given coder name.
33
+ *
34
+ * @param {string} coderName - The coder name (e.g., 'claude-code', 'gemini-cli')
35
+ * @returns {Promise<import('./interface.js').TranscriptExporter>} Exporter instance
36
+ * @throws {Error} If coder name is not supported
37
+ *
38
+ * @example
39
+ * ```js
40
+ * const exporter = await getExporterForCoder('claude-code');
41
+ * const transcript = await exporter.export(events);
42
+ * ```
43
+ */
44
+ export async function getExporterForCoder(coderName) {
45
+ // Normalize coder name
46
+ const normalized = coderName?.toLowerCase()?.trim();
47
+
48
+ // Direct match
49
+ if (EXPORTERS[normalized]) {
50
+ const ExporterClass = await EXPORTERS[normalized]();
51
+ return new ExporterClass();
52
+ }
53
+
54
+ // Alias matching
55
+ const aliases = {
56
+ 'claude': CODER_NAMES.CLAUDE_CODE,
57
+ 'claude-code': CODER_NAMES.CLAUDE_CODE,
58
+ 'claudecode': CODER_NAMES.CLAUDE_CODE,
59
+ 'gemini': CODER_NAMES.GEMINI_CLI,
60
+ 'gemini-cli': CODER_NAMES.GEMINI_CLI,
61
+ 'geminicli': CODER_NAMES.GEMINI_CLI,
62
+ };
63
+
64
+ const mappedName = aliases[normalized];
65
+ if (mappedName && EXPORTERS[mappedName]) {
66
+ const ExporterClass = await EXPORTERS[mappedName]();
67
+ return new ExporterClass();
68
+ }
69
+
70
+ throw new Error(
71
+ `Unsupported coder: '${coderName}'. Supported coders: ${Object.keys(EXPORTERS).join(', ')}`
72
+ );
73
+ }
74
+
75
+ /**
76
+ * Get list of supported coder names.
77
+ *
78
+ * @returns {string[]} Array of supported coder names
79
+ */
80
+ export function getSupportedCoders() {
81
+ return Object.keys(EXPORTERS);
82
+ }
83
+
84
+ /**
85
+ * Check if a coder is supported.
86
+ *
87
+ * @param {string} coderName - The coder name to check
88
+ * @returns {boolean} True if supported
89
+ */
90
+ export function isCoderSupported(coderName) {
91
+ const normalized = coderName?.toLowerCase()?.trim();
92
+ return Boolean(EXPORTERS[normalized]);
93
+ }