claude-code-templates 1.24.16 → 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.
- package/bin/create-claude-config.js +1 -0
- package/package.json +2 -1
- package/src/analytics-web/chats_mobile.html +2147 -138
- package/src/chats-mobile.js +296 -1
- package/src/index.js +44 -1
- package/src/session-sharing.js +396 -0
package/src/chats-mobile.js
CHANGED
|
@@ -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: {},
|
|
@@ -173,6 +177,244 @@ class ChatsMobile {
|
|
|
173
177
|
}
|
|
174
178
|
});
|
|
175
179
|
|
|
180
|
+
// API to get unique working directories from conversations
|
|
181
|
+
this.app.get('/api/directories', (req, res) => {
|
|
182
|
+
try {
|
|
183
|
+
// Extract unique directories from conversations
|
|
184
|
+
const directories = new Set();
|
|
185
|
+
|
|
186
|
+
this.data.conversations.forEach(conv => {
|
|
187
|
+
if (conv.project && conv.project.trim()) {
|
|
188
|
+
directories.add(conv.project);
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// Convert to array and sort alphabetically
|
|
193
|
+
const sortedDirectories = Array.from(directories).sort((a, b) =>
|
|
194
|
+
a.toLowerCase().localeCompare(b.toLowerCase())
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
res.json({
|
|
198
|
+
directories: sortedDirectories,
|
|
199
|
+
count: sortedDirectories.length,
|
|
200
|
+
timestamp: new Date().toISOString()
|
|
201
|
+
});
|
|
202
|
+
} catch (error) {
|
|
203
|
+
console.error('Error getting directories:', error);
|
|
204
|
+
res.status(500).json({ error: 'Internal server error' });
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
// API to search conversations with advanced filters
|
|
209
|
+
this.app.post('/api/search', async (req, res) => {
|
|
210
|
+
try {
|
|
211
|
+
const { query, workingDirectory, dateFrom, dateTo, contentSearch } = req.body;
|
|
212
|
+
|
|
213
|
+
let results = [...this.data.conversations];
|
|
214
|
+
|
|
215
|
+
// Filter by working directory (project)
|
|
216
|
+
if (workingDirectory && workingDirectory.trim()) {
|
|
217
|
+
results = results.filter(conv => {
|
|
218
|
+
if (!conv.project) return false;
|
|
219
|
+
return conv.project.toLowerCase().includes(workingDirectory.toLowerCase());
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Filter by date range
|
|
224
|
+
if (dateFrom) {
|
|
225
|
+
const fromDate = new Date(dateFrom);
|
|
226
|
+
results = results.filter(conv => new Date(conv.created) >= fromDate);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (dateTo) {
|
|
230
|
+
const toDate = new Date(dateTo);
|
|
231
|
+
toDate.setHours(23, 59, 59, 999); // Include entire day
|
|
232
|
+
results = results.filter(conv => new Date(conv.created) <= toDate);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Filter by conversation metadata (filename, id)
|
|
236
|
+
if (query && query.trim()) {
|
|
237
|
+
const searchTerm = query.toLowerCase();
|
|
238
|
+
results = results.filter(conv =>
|
|
239
|
+
conv.filename.toLowerCase().includes(searchTerm) ||
|
|
240
|
+
conv.id.toLowerCase().includes(searchTerm) ||
|
|
241
|
+
(conv.project && conv.project.toLowerCase().includes(searchTerm))
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Search within message content
|
|
246
|
+
if (contentSearch && contentSearch.trim()) {
|
|
247
|
+
const contentTerm = contentSearch.toLowerCase();
|
|
248
|
+
const matchingConversations = [];
|
|
249
|
+
|
|
250
|
+
for (const conv of results) {
|
|
251
|
+
try {
|
|
252
|
+
const messages = await this.conversationAnalyzer.getParsedConversation(conv.filePath);
|
|
253
|
+
|
|
254
|
+
// Search in message content
|
|
255
|
+
const hasMatch = messages.some(msg => {
|
|
256
|
+
// Search in text content
|
|
257
|
+
if (typeof msg.content === 'string') {
|
|
258
|
+
return msg.content.toLowerCase().includes(contentTerm);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Search in array content (tool use, text blocks)
|
|
262
|
+
if (Array.isArray(msg.content)) {
|
|
263
|
+
return msg.content.some(block => {
|
|
264
|
+
if (block.type === 'text' && block.text) {
|
|
265
|
+
return block.text.toLowerCase().includes(contentTerm);
|
|
266
|
+
}
|
|
267
|
+
if (block.type === 'tool_use' && block.name) {
|
|
268
|
+
return block.name.toLowerCase().includes(contentTerm);
|
|
269
|
+
}
|
|
270
|
+
return false;
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return false;
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
if (hasMatch) {
|
|
278
|
+
matchingConversations.push(conv);
|
|
279
|
+
}
|
|
280
|
+
} catch (error) {
|
|
281
|
+
// Skip conversations that can't be parsed
|
|
282
|
+
this.log('warn', `Error searching in conversation ${conv.id}:`, error.message);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
results = matchingConversations;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Sort by last modified (most recent first)
|
|
290
|
+
results.sort((a, b) => new Date(b.lastModified) - new Date(a.lastModified));
|
|
291
|
+
|
|
292
|
+
res.json({
|
|
293
|
+
results: results,
|
|
294
|
+
count: results.length,
|
|
295
|
+
filters: {
|
|
296
|
+
query,
|
|
297
|
+
workingDirectory,
|
|
298
|
+
dateFrom,
|
|
299
|
+
dateTo,
|
|
300
|
+
contentSearch
|
|
301
|
+
},
|
|
302
|
+
timestamp: new Date().toISOString()
|
|
303
|
+
});
|
|
304
|
+
} catch (error) {
|
|
305
|
+
console.error('Error searching conversations:', error);
|
|
306
|
+
res.status(500).json({ error: 'Internal server error', message: error.message });
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
// API to search within a specific conversation
|
|
311
|
+
this.app.post('/api/conversations/:id/search', async (req, res) => {
|
|
312
|
+
try {
|
|
313
|
+
const conversationId = req.params.id;
|
|
314
|
+
const { query } = req.body;
|
|
315
|
+
const conversation = this.data.conversations.find(conv => conv.id === conversationId);
|
|
316
|
+
|
|
317
|
+
if (!conversation) {
|
|
318
|
+
return res.status(404).json({ error: 'Conversation not found' });
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (!query || !query.trim()) {
|
|
322
|
+
return res.json({
|
|
323
|
+
matches: [],
|
|
324
|
+
totalMatches: 0,
|
|
325
|
+
conversationId: conversationId
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Get all messages from the conversation
|
|
330
|
+
const allMessages = await this.conversationAnalyzer.getParsedConversation(conversation.filePath);
|
|
331
|
+
const searchTerm = query.toLowerCase();
|
|
332
|
+
const matches = [];
|
|
333
|
+
|
|
334
|
+
// Search through all messages
|
|
335
|
+
allMessages.forEach((msg, index) => {
|
|
336
|
+
let messageText = '';
|
|
337
|
+
let allText = [];
|
|
338
|
+
|
|
339
|
+
// Extract text from message content
|
|
340
|
+
if (typeof msg.content === 'string') {
|
|
341
|
+
allText.push(msg.content);
|
|
342
|
+
} else if (Array.isArray(msg.content)) {
|
|
343
|
+
msg.content.forEach(block => {
|
|
344
|
+
if (block.type === 'text' && block.text) {
|
|
345
|
+
allText.push(block.text);
|
|
346
|
+
}
|
|
347
|
+
// Also search in tool_use content
|
|
348
|
+
if (block.type === 'tool_use') {
|
|
349
|
+
if (block.name) allText.push(block.name);
|
|
350
|
+
if (block.input) {
|
|
351
|
+
allText.push(JSON.stringify(block.input));
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// IMPORTANT: Also search in tool results (this is where code blocks appear!)
|
|
358
|
+
if (msg.toolResults && Array.isArray(msg.toolResults)) {
|
|
359
|
+
msg.toolResults.forEach(toolResult => {
|
|
360
|
+
if (toolResult.content) {
|
|
361
|
+
if (typeof toolResult.content === 'string') {
|
|
362
|
+
allText.push(toolResult.content);
|
|
363
|
+
} else if (Array.isArray(toolResult.content)) {
|
|
364
|
+
toolResult.content.forEach(block => {
|
|
365
|
+
if (block.type === 'text' && block.text) {
|
|
366
|
+
allText.push(block.text);
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Combine all text
|
|
375
|
+
messageText = allText.join(' ');
|
|
376
|
+
|
|
377
|
+
// Search in the combined text
|
|
378
|
+
if (messageText.toLowerCase().includes(searchTerm)) {
|
|
379
|
+
// Find all positions of the search term in this message
|
|
380
|
+
const lowerText = messageText.toLowerCase();
|
|
381
|
+
let position = 0;
|
|
382
|
+
let matchCount = 0;
|
|
383
|
+
|
|
384
|
+
while ((position = lowerText.indexOf(searchTerm, position)) !== -1) {
|
|
385
|
+
matchCount++;
|
|
386
|
+
position += searchTerm.length;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
matches.push({
|
|
390
|
+
messageIndex: index,
|
|
391
|
+
messageId: msg.id,
|
|
392
|
+
role: msg.role,
|
|
393
|
+
timestamp: msg.timestamp,
|
|
394
|
+
preview: this.getMessagePreview(messageText, searchTerm),
|
|
395
|
+
matchCount: matchCount
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
console.log(`🔍 Search in conversation ${conversationId}:`, {
|
|
401
|
+
query: query,
|
|
402
|
+
messagesWithMatches: matches.length,
|
|
403
|
+
totalOccurrences: matches.reduce((sum, m) => sum + m.matchCount, 0)
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
res.json({
|
|
407
|
+
matches: matches,
|
|
408
|
+
totalMatches: matches.length,
|
|
409
|
+
conversationId: conversationId,
|
|
410
|
+
query: query
|
|
411
|
+
});
|
|
412
|
+
} catch (error) {
|
|
413
|
+
console.error('Error searching in conversation:', error);
|
|
414
|
+
res.status(500).json({ error: 'Internal server error', message: error.message });
|
|
415
|
+
}
|
|
416
|
+
});
|
|
417
|
+
|
|
176
418
|
// API to get specific conversation messages (with pagination support)
|
|
177
419
|
this.app.get('/api/conversations/:id/messages', async (req, res) => {
|
|
178
420
|
try {
|
|
@@ -236,6 +478,39 @@ class ChatsMobile {
|
|
|
236
478
|
}
|
|
237
479
|
});
|
|
238
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
|
+
|
|
239
514
|
// Serve the mobile chats page as default
|
|
240
515
|
this.app.get('/', (req, res) => {
|
|
241
516
|
res.sendFile(path.join(__dirname, 'analytics-web', 'chats_mobile.html'));
|
|
@@ -418,6 +693,26 @@ class ChatsMobile {
|
|
|
418
693
|
console.log(chalk.gray('🔧 WebSocket server setup prepared'));
|
|
419
694
|
}
|
|
420
695
|
|
|
696
|
+
/**
|
|
697
|
+
* Helper function to get message preview with context
|
|
698
|
+
*/
|
|
699
|
+
getMessagePreview(text, searchTerm, contextLength = 100) {
|
|
700
|
+
const lowerText = text.toLowerCase();
|
|
701
|
+
const lowerTerm = searchTerm.toLowerCase();
|
|
702
|
+
const position = lowerText.indexOf(lowerTerm);
|
|
703
|
+
|
|
704
|
+
if (position === -1) return text.substring(0, contextLength);
|
|
705
|
+
|
|
706
|
+
const start = Math.max(0, position - contextLength / 2);
|
|
707
|
+
const end = Math.min(text.length, position + searchTerm.length + contextLength / 2);
|
|
708
|
+
|
|
709
|
+
let preview = text.substring(start, end);
|
|
710
|
+
if (start > 0) preview = '...' + preview;
|
|
711
|
+
if (end < text.length) preview = preview + '...';
|
|
712
|
+
|
|
713
|
+
return preview;
|
|
714
|
+
}
|
|
715
|
+
|
|
421
716
|
/**
|
|
422
717
|
* Load initial conversation data
|
|
423
718
|
*/
|
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) {
|