claude-code-templates 1.24.17 → 1.25.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.
@@ -10,6 +10,7 @@ const StateCalculator = require('./analytics/core/StateCalculator');
10
10
  const FileWatcher = require('./analytics/core/FileWatcher');
11
11
  const DataCache = require('./analytics/data/DataCache');
12
12
  const WebSocketServer = require('./analytics/notifications/WebSocketServer');
13
+ const SessionSharing = require('./session-sharing');
13
14
 
14
15
  class ChatsMobile {
15
16
  constructor(options = {}) {
@@ -28,7 +29,10 @@ class ChatsMobile {
28
29
  const homeDir = os.homedir();
29
30
  const claudeDir = path.join(homeDir, '.claude');
30
31
  this.conversationAnalyzer = new ConversationAnalyzer(claudeDir, this.dataCache);
31
-
32
+
33
+ // Initialize SessionSharing for export/import functionality
34
+ this.sessionSharing = new SessionSharing(this.conversationAnalyzer);
35
+
32
36
  this.data = {
33
37
  conversations: [],
34
38
  conversationStates: {},
@@ -474,6 +478,39 @@ class ChatsMobile {
474
478
  }
475
479
  });
476
480
 
481
+ // API to share a conversation session
482
+ this.app.post('/api/conversations/:id/share', async (req, res) => {
483
+ try {
484
+ const conversationId = req.params.id;
485
+ const conversation = this.data.conversations.find(conv => conv.id === conversationId);
486
+
487
+ if (!conversation) {
488
+ return res.status(404).json({ error: 'Conversation not found' });
489
+ }
490
+
491
+ console.log(chalk.cyan(`📤 Sharing conversation ${conversationId}...`));
492
+
493
+ // Share the session using SessionSharing module
494
+ const shareResult = await this.sessionSharing.shareSession(conversationId, conversation);
495
+
496
+ res.json({
497
+ success: true,
498
+ conversationId: conversationId,
499
+ uploadUrl: shareResult.uploadUrl,
500
+ shareCommand: shareResult.shareCommand,
501
+ expiresIn: shareResult.expiresIn,
502
+ qrCode: shareResult.qrCode,
503
+ timestamp: new Date().toISOString()
504
+ });
505
+ } catch (error) {
506
+ console.error('Error sharing conversation:', error);
507
+ res.status(500).json({
508
+ error: 'Failed to share session',
509
+ message: error.message
510
+ });
511
+ }
512
+ });
513
+
477
514
  // Serve the mobile chats page as default
478
515
  this.app.get('/', (req, res) => {
479
516
  res.sendFile(path.join(__dirname, 'analytics-web', 'chats_mobile.html'));
package/src/index.js CHANGED
@@ -18,6 +18,8 @@ const { runHealthCheck } = require('./health-check');
18
18
  const { runPluginDashboard } = require('./plugin-dashboard');
19
19
  const { trackingService } = require('./tracking-service');
20
20
  const { createGlobalAgent, listGlobalAgents, removeGlobalAgent, updateGlobalAgent } = require('./sdk/global-agent-manager');
21
+ const SessionSharing = require('./session-sharing');
22
+ const ConversationAnalyzer = require('./analytics/core/ConversationAnalyzer');
21
23
 
22
24
  async function showMainMenu() {
23
25
  console.log('');
@@ -222,7 +224,48 @@ async function createClaudeConfig(options = {}) {
222
224
  await startChatsMobile(options);
223
225
  return;
224
226
  }
225
-
227
+
228
+ // Handle session clone (download and import shared session)
229
+ if (options.cloneSession) {
230
+ console.log(chalk.blue('📥 Cloning shared Claude Code session...'));
231
+
232
+ try {
233
+ const os = require('os');
234
+ const homeDir = os.homedir();
235
+ const claudeDir = path.join(homeDir, '.claude');
236
+
237
+ // Initialize ConversationAnalyzer and SessionSharing
238
+ const conversationAnalyzer = new ConversationAnalyzer(claudeDir);
239
+ const sessionSharing = new SessionSharing(conversationAnalyzer);
240
+
241
+ // Clone the session (cloneSession method handles all console output)
242
+ const result = await sessionSharing.cloneSession(options.cloneSession, {
243
+ projectPath: options.directory || process.cwd()
244
+ });
245
+
246
+ // Track session clone
247
+ trackingService.trackAnalyticsDashboard({
248
+ page: 'session-clone',
249
+ source: 'command_line',
250
+ success: true
251
+ });
252
+ } catch (error) {
253
+ console.error(chalk.red('❌ Failed to clone session:'), error.message);
254
+
255
+ // Track failed clone
256
+ trackingService.trackAnalyticsDashboard({
257
+ page: 'session-clone',
258
+ source: 'command_line',
259
+ success: false,
260
+ error: error.message
261
+ });
262
+
263
+ process.exit(1);
264
+ }
265
+
266
+ return;
267
+ }
268
+
226
269
  // Handle health check
227
270
  let shouldRunSetup = false;
228
271
  if (options.healthCheck || options.health || options.check || options.verify) {
@@ -0,0 +1,396 @@
1
+ const chalk = require('chalk');
2
+ const fs = require('fs-extra');
3
+ const path = require('path');
4
+ const os = require('os');
5
+ const { exec } = require('child_process');
6
+ const { promisify } = require('util');
7
+ const execAsync = promisify(exec);
8
+ const QRCode = require('qrcode');
9
+
10
+ /**
11
+ * SessionSharing - Handles exporting and importing Claude Code sessions
12
+ * Uses x0.at - a simple, reliable file hosting service
13
+ */
14
+ class SessionSharing {
15
+ constructor(conversationAnalyzer) {
16
+ this.conversationAnalyzer = conversationAnalyzer;
17
+ this.uploadService = 'x0.at';
18
+ this.uploadUrl = 'https://x0.at';
19
+ }
20
+
21
+ /**
22
+ * Export and share a conversation session
23
+ * @param {string} conversationId - Conversation ID to share
24
+ * @param {Object} conversationData - Full conversation data object
25
+ * @param {Object} options - Share options (messageLimit, etc.)
26
+ * @returns {Promise<Object>} Share result with URL, command, and QR code
27
+ */
28
+ async shareSession(conversationId, conversationData, options = {}) {
29
+ console.log(chalk.blue(`📤 Preparing session ${conversationId} for sharing...`));
30
+
31
+ try {
32
+ // 1. Export session to structured JSON format
33
+ const sessionExport = await this.exportSessionData(conversationId, conversationData, options);
34
+
35
+ // 2. Upload to x0.at
36
+ const uploadUrl = await this.uploadToX0(sessionExport, conversationId);
37
+
38
+ // 3. Generate share command
39
+ const shareCommand = `npx claude-code-templates@latest --clone-session "${uploadUrl}"`;
40
+
41
+ // 4. Generate QR code
42
+ const qrCode = await this.generateQRCode(shareCommand);
43
+
44
+ console.log(chalk.green(`✅ Session shared successfully!`));
45
+ console.log(chalk.cyan(`📋 Share command: ${shareCommand}`));
46
+ console.log(chalk.gray(`🔗 Direct URL: ${uploadUrl}`));
47
+ console.log(chalk.yellow(`⚠️ Files kept for 3-100 days (based on size)`));
48
+ console.log(chalk.gray(`🔓 Note: Files are not encrypted by default`));
49
+
50
+ return {
51
+ success: true,
52
+ uploadUrl,
53
+ shareCommand,
54
+ expiresIn: 'After first download',
55
+ conversationId,
56
+ qrCode,
57
+ messageCount: sessionExport.conversation.messageCount,
58
+ totalMessageCount: sessionExport.conversation.totalMessageCount,
59
+ wasLimited: sessionExport.conversation.wasLimited
60
+ };
61
+ } catch (error) {
62
+ console.error(chalk.red('❌ Failed to share session:'), error.message);
63
+ throw error;
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Export session data to standardized format
69
+ * @param {string} conversationId - Conversation ID
70
+ * @param {Object} conversationData - Conversation metadata
71
+ * @param {Object} options - Export options
72
+ * @returns {Promise<Object>} Exported session object
73
+ */
74
+ async exportSessionData(conversationId, conversationData, options = {}) {
75
+ // Get all messages from the conversation
76
+ const allMessages = await this.conversationAnalyzer.getParsedConversation(conversationData.filePath);
77
+
78
+ // Limit messages to avoid large file sizes (default: last 100 messages)
79
+ const messageLimit = options.messageLimit || 100;
80
+ const messages = allMessages.slice(-messageLimit);
81
+
82
+ // Convert parsed messages back to JSONL format (original Claude Code format)
83
+ const jsonlMessages = messages.map(msg => {
84
+ // Reconstruct original JSONL entry format
85
+ const entry = {
86
+ uuid: msg.uuid || msg.id,
87
+ type: msg.role === 'assistant' ? 'assistant' : 'user',
88
+ timestamp: msg.timestamp.toISOString(),
89
+ message: {
90
+ id: msg.id,
91
+ role: msg.role,
92
+ content: msg.content
93
+ }
94
+ };
95
+
96
+ // Add model info for assistant messages
97
+ if (msg.model) {
98
+ entry.message.model = msg.model;
99
+ }
100
+
101
+ // Add usage info
102
+ if (msg.usage) {
103
+ entry.message.usage = msg.usage;
104
+ }
105
+
106
+ // Add compact summary flag if present
107
+ if (msg.isCompactSummary) {
108
+ entry.isCompactSummary = true;
109
+ }
110
+
111
+ return entry;
112
+ });
113
+
114
+ // Create export package
115
+ const exportData = {
116
+ version: '1.0.0',
117
+ exported_at: new Date().toISOString(),
118
+ conversation: {
119
+ id: conversationId,
120
+ project: conversationData.project || 'Unknown',
121
+ created: conversationData.created,
122
+ lastModified: conversationData.lastModified,
123
+ messageCount: messages.length,
124
+ totalMessageCount: allMessages.length,
125
+ wasLimited: allMessages.length > messageLimit,
126
+ tokens: conversationData.tokens,
127
+ model: conversationData.modelInfo?.primaryModel || 'Unknown'
128
+ },
129
+ messages: jsonlMessages,
130
+ metadata: {
131
+ exportTool: 'claude-code-templates',
132
+ exportVersion: require('../package.json').version || '1.0.0',
133
+ messageLimit: messageLimit,
134
+ description: 'Claude Code session export - can be cloned with: npx claude-code-templates@latest --clone-session <url>'
135
+ }
136
+ };
137
+
138
+ // Log information about exported messages
139
+ if (allMessages.length > messageLimit) {
140
+ console.log(chalk.yellow(`⚠️ Session has ${allMessages.length} messages, exporting last ${messageLimit} messages`));
141
+ } else {
142
+ console.log(chalk.gray(`📊 Exporting ${messages.length} messages`));
143
+ }
144
+
145
+ return exportData;
146
+ }
147
+
148
+ /**
149
+ * Upload session to x0.at
150
+ * @param {Object} sessionData - Session export data
151
+ * @param {string} conversationId - Conversation ID for filename
152
+ * @returns {Promise<string>} Upload URL
153
+ */
154
+ async uploadToX0(sessionData, conversationId) {
155
+ const tmpDir = path.join(os.tmpdir(), 'claude-code-sessions');
156
+ await fs.ensureDir(tmpDir);
157
+
158
+ const tmpFile = path.join(tmpDir, `session-${conversationId}.json`);
159
+
160
+ try {
161
+ // Write session data to temp file
162
+ await fs.writeFile(tmpFile, JSON.stringify(sessionData, null, 2), 'utf8');
163
+
164
+ console.log(chalk.gray(`📁 Created temp file: ${tmpFile}`));
165
+ console.log(chalk.gray(`📤 Uploading to x0.at...`));
166
+
167
+ // Upload to x0.at using curl with form data
168
+ // x0.at API: curl -F'file=@yourfile.png' https://x0.at
169
+ // Response: Direct URL in plain text
170
+ const { stdout, stderr } = await execAsync(
171
+ `curl -s -F "file=@${tmpFile}" ${this.uploadUrl}`,
172
+ { maxBuffer: 10 * 1024 * 1024 } // 10MB buffer
173
+ );
174
+
175
+ // x0.at returns URL directly in plain text
176
+ const uploadUrl = stdout.trim();
177
+
178
+ // Validate response
179
+ if (!uploadUrl || !uploadUrl.startsWith('http')) {
180
+ throw new Error(`Invalid response from x0.at: ${uploadUrl || stderr}`);
181
+ }
182
+
183
+ console.log(chalk.green(`✅ Uploaded to x0.at successfully`));
184
+ console.log(chalk.yellow(`⚠️ Files kept for 3-100 days (based on size)`));
185
+ console.log(chalk.gray(`🔓 Note: Files are not encrypted by default`));
186
+
187
+ // Clean up temp file
188
+ await fs.remove(tmpFile);
189
+
190
+ return uploadUrl;
191
+ } catch (error) {
192
+ // Clean up temp file on error
193
+ await fs.remove(tmpFile).catch(() => {});
194
+ throw error;
195
+ }
196
+ }
197
+
198
+ /**
199
+ * Clone a session from a shared URL
200
+ * Downloads the session and places it in the correct Claude Code location
201
+ * @param {string} url - URL to download session from
202
+ * @param {Object} options - Clone options
203
+ * @returns {Promise<Object>} Result with session path
204
+ */
205
+ async cloneSession(url, options = {}) {
206
+ console.log(chalk.blue(`📥 Downloading session from ${url}...`));
207
+
208
+ try {
209
+ // 1. Download session data
210
+ const sessionData = await this.downloadSession(url);
211
+
212
+ // 2. Validate session data
213
+ this.validateSessionData(sessionData);
214
+
215
+ console.log(chalk.green(`✅ Session downloaded successfully`));
216
+ console.log(chalk.gray(`📊 Project: ${sessionData.conversation.project}`));
217
+ console.log(chalk.gray(`💬 Messages: ${sessionData.conversation.messageCount}`));
218
+ console.log(chalk.gray(`🤖 Model: ${sessionData.conversation.model}`));
219
+
220
+ // 3. Install session in Claude Code directory
221
+ const installResult = await this.installSession(sessionData, options);
222
+
223
+ console.log(chalk.green(`\n✅ Session installed successfully!`));
224
+ console.log(chalk.cyan(`📂 Location: ${installResult.sessionPath}`));
225
+
226
+ // Show resume command
227
+ const resumeCommand = `claude --resume ${installResult.projectPath} ${installResult.conversationId}`;
228
+ console.log(chalk.yellow(`\n💡 To continue this conversation, run:`));
229
+ console.log(chalk.white(`\n ${resumeCommand}\n`));
230
+ console.log(chalk.gray(` Or open Claude Code to see it in your sessions list`));
231
+
232
+ return installResult;
233
+ } catch (error) {
234
+ console.error(chalk.red('❌ Failed to clone session:'), error.message);
235
+ throw error;
236
+ }
237
+ }
238
+
239
+ /**
240
+ * Download session data from URL
241
+ * @param {string} url - URL to download from
242
+ * @returns {Promise<Object>} Session data
243
+ */
244
+ async downloadSession(url) {
245
+ try {
246
+ // Use curl to download (works with x0.at and other services)
247
+ const { stdout, stderr } = await execAsync(`curl -L "${url}"`, {
248
+ maxBuffer: 50 * 1024 * 1024 // 50MB buffer for large sessions
249
+ });
250
+
251
+ if (stderr && !stdout) {
252
+ throw new Error(`Download failed: ${stderr}`);
253
+ }
254
+
255
+ // Parse JSON response
256
+ const sessionData = JSON.parse(stdout);
257
+ return sessionData;
258
+ } catch (error) {
259
+ if (error.message.includes('Unexpected token')) {
260
+ throw new Error('Invalid session file - corrupted or not a Claude Code session');
261
+ }
262
+ throw error;
263
+ }
264
+ }
265
+
266
+ /**
267
+ * Validate session data structure
268
+ * @param {Object} sessionData - Session data to validate
269
+ * @throws {Error} If validation fails
270
+ */
271
+ validateSessionData(sessionData) {
272
+ if (!sessionData.version) {
273
+ throw new Error('Invalid session file - missing version');
274
+ }
275
+
276
+ if (!sessionData.conversation || !sessionData.conversation.id) {
277
+ throw new Error('Invalid session file - missing conversation data');
278
+ }
279
+
280
+ if (!sessionData.messages || !Array.isArray(sessionData.messages)) {
281
+ throw new Error('Invalid session file - missing or invalid messages');
282
+ }
283
+
284
+ if (sessionData.messages.length === 0) {
285
+ throw new Error('Invalid session file - no messages found');
286
+ }
287
+ }
288
+
289
+ /**
290
+ * Install session in Claude Code directory structure
291
+ * @param {Object} sessionData - Session data to install
292
+ * @param {Object} options - Installation options
293
+ * @returns {Promise<Object>} Installation result
294
+ */
295
+ async installSession(sessionData, options = {}) {
296
+ const homeDir = os.homedir();
297
+ const claudeDir = path.join(homeDir, '.claude');
298
+
299
+ // Determine project directory
300
+ const projectName = sessionData.conversation.project || 'shared-session';
301
+ const projectDirName = this.sanitizeProjectName(projectName);
302
+
303
+ // Create project directory structure
304
+ // Format: ~/.claude/projects/-path-to-project/
305
+ const projectDir = path.join(claudeDir, 'projects', projectDirName);
306
+ await fs.ensureDir(projectDir);
307
+
308
+ // Generate conversation filename with original ID
309
+ const conversationId = sessionData.conversation.id;
310
+ const conversationFile = path.join(projectDir, `${conversationId}.jsonl`);
311
+
312
+ // Convert messages back to JSONL format (one JSON object per line)
313
+ const jsonlContent = sessionData.messages
314
+ .map(msg => JSON.stringify(msg))
315
+ .join('\n');
316
+
317
+ // Write conversation file
318
+ await fs.writeFile(conversationFile, jsonlContent, 'utf8');
319
+
320
+ console.log(chalk.gray(`📝 Created conversation file: ${conversationFile}`));
321
+
322
+ // Create or update settings.json
323
+ const settingsFile = path.join(projectDir, 'settings.json');
324
+ const settings = {
325
+ projectName: sessionData.conversation.project,
326
+ projectPath: options.projectPath || process.cwd(),
327
+ sharedSession: true,
328
+ originalExport: {
329
+ exportedAt: sessionData.exported_at,
330
+ exportTool: sessionData.metadata?.exportTool,
331
+ exportVersion: sessionData.metadata?.exportVersion
332
+ },
333
+ importedAt: new Date().toISOString()
334
+ };
335
+
336
+ await fs.writeFile(settingsFile, JSON.stringify(settings, null, 2), 'utf8');
337
+
338
+ console.log(chalk.gray(`⚙️ Created settings file: ${settingsFile}`));
339
+
340
+ return {
341
+ success: true,
342
+ sessionPath: conversationFile,
343
+ projectDir,
344
+ projectPath: settings.projectPath,
345
+ conversationId,
346
+ messageCount: sessionData.messages.length
347
+ };
348
+ }
349
+
350
+ /**
351
+ * Generate QR code for share command
352
+ * @param {string} command - Command to encode in QR
353
+ * @returns {Promise<Object>} QR code data (Data URL for web display)
354
+ */
355
+ async generateQRCode(command) {
356
+ try {
357
+ // Generate QR code as Data URL (for web display)
358
+ const qrDataUrl = await QRCode.toDataURL(command, {
359
+ errorCorrectionLevel: 'M',
360
+ type: 'image/png',
361
+ width: 300,
362
+ margin: 2,
363
+ color: {
364
+ dark: '#000000',
365
+ light: '#FFFFFF'
366
+ }
367
+ });
368
+
369
+ return {
370
+ dataUrl: qrDataUrl,
371
+ command: command
372
+ };
373
+ } catch (error) {
374
+ console.warn(chalk.yellow('⚠️ Could not generate QR code:'), error.message);
375
+ return {
376
+ dataUrl: null,
377
+ command: command
378
+ };
379
+ }
380
+ }
381
+
382
+ /**
383
+ * Sanitize project name for directory usage
384
+ * @param {string} projectName - Original project name
385
+ * @returns {string} Sanitized name
386
+ */
387
+ sanitizeProjectName(projectName) {
388
+ // Replace spaces and special chars with hyphens
389
+ return projectName
390
+ .replace(/[^a-zA-Z0-9-_]/g, '-')
391
+ .replace(/-+/g, '-')
392
+ .toLowerCase();
393
+ }
394
+ }
395
+
396
+ module.exports = SessionSharing;