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.
- package/.claude/hooks/permission_request.mjs +326 -59
- package/.claude/hooks/post_tool_use.mjs +90 -0
- package/.claude/hooks/pre_tool_use.mjs +212 -293
- package/.claude/hooks/session-register.mjs +89 -104
- package/.claude/hooks/session_end.mjs +41 -42
- package/.claude/hooks/session_start.mjs +45 -60
- package/.claude/hooks/stop.mjs +752 -99
- package/.claude/hooks/user_prompt_submit.mjs +26 -3
- package/lib/cli/daemon-commands.js +1 -1
- package/lib/cli/teleport-commands.js +469 -0
- package/lib/daemon/daemon-v2.js +104 -0
- package/lib/daemon/lifecycle.js +56 -171
- package/lib/daemon/services/index.js +3 -0
- package/lib/daemon/services/polling-service.js +173 -0
- package/lib/daemon/services/queue-service.js +318 -0
- package/lib/daemon/services/session-service.js +115 -0
- package/lib/daemon/state.js +35 -0
- package/lib/daemon/task-executor-v2.js +413 -0
- package/lib/daemon/task-executor.js +270 -96
- package/lib/daemon/teleportation-daemon.js +709 -126
- package/lib/daemon/timeline-analyzer.js +215 -0
- package/lib/daemon/transcript-ingestion.js +696 -0
- package/lib/daemon/utils.js +91 -0
- package/lib/install/installer.js +184 -20
- package/lib/install/uhr-installer.js +136 -0
- package/lib/remote/providers/base-provider.js +46 -0
- package/lib/remote/providers/daytona-provider.js +58 -0
- package/lib/remote/providers/provider-factory.js +90 -19
- package/lib/remote/providers/sprites-provider.js +711 -0
- package/lib/teleport/exporters/claude-exporter.js +302 -0
- package/lib/teleport/exporters/gemini-exporter.js +307 -0
- package/lib/teleport/exporters/index.js +93 -0
- package/lib/teleport/exporters/interface.js +153 -0
- package/lib/teleport/fork-tracker.js +415 -0
- package/lib/teleport/git-committer.js +337 -0
- package/lib/teleport/index.js +48 -0
- package/lib/teleport/manager.js +620 -0
- package/lib/teleport/session-capture.js +282 -0
- package/package.json +6 -2
- package/teleportation-cli.cjs +488 -453
- package/.claude/hooks/heartbeat.mjs +0 -396
- 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
|
+
}
|