claude-code-templates 1.16.1 → 1.17.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.
Files changed (101) hide show
  1. package/README.md +7 -7
  2. package/bin/create-claude-config.js +17 -8
  3. package/package.json +2 -3
  4. package/src/analytics/core/AgentAnalyzer.js +17 -3
  5. package/src/analytics/core/ProcessDetector.js +23 -7
  6. package/src/analytics/core/StateCalculator.js +102 -33
  7. package/src/analytics/data/DataCache.js +7 -7
  8. package/src/analytics-web/chats_mobile.html +2590 -0
  9. package/src/analytics-web/components/App.js +10 -10
  10. package/src/analytics-web/components/SessionTimer.js +1 -1
  11. package/src/analytics-web/components/Sidebar.js +5 -14
  12. package/src/analytics-web/index.html +932 -78
  13. package/src/analytics.js +263 -5
  14. package/src/chats-mobile.js +682 -0
  15. package/src/claude-api-proxy.js +460 -0
  16. package/src/file-operations.js +239 -36
  17. package/src/health-check.js +310 -0
  18. package/src/index.js +1245 -36
  19. package/src/tracking-service.js +31 -34
  20. package/components/agents/api-security-audit.md +0 -92
  21. package/components/agents/database-optimization.md +0 -94
  22. package/components/agents/react-performance-optimization.md +0 -64
  23. package/components/commands/check-file.md +0 -53
  24. package/components/commands/generate-tests.md +0 -68
  25. package/components/mcps/deepgraph-nextjs.json +0 -12
  26. package/components/mcps/deepgraph-react.json +0 -12
  27. package/components/mcps/deepgraph-typescript.json +0 -12
  28. package/components/mcps/deepgraph-vue.json +0 -12
  29. package/components/mcps/filesystem-access.json +0 -12
  30. package/components/mcps/github-integration.json +0 -11
  31. package/components/mcps/memory-integration.json +0 -8
  32. package/components/mcps/mysql-integration.json +0 -11
  33. package/components/mcps/postgresql-integration.json +0 -11
  34. package/components/mcps/web-fetch.json +0 -8
  35. package/src/analytics-web/components/AgentsPage.js +0 -4761
  36. package/templates/common/.claude/commands/git-workflow.md +0 -239
  37. package/templates/common/.claude/commands/project-setup.md +0 -316
  38. package/templates/common/.mcp.json +0 -41
  39. package/templates/common/CLAUDE.md +0 -109
  40. package/templates/common/README.md +0 -96
  41. package/templates/go/.mcp.json +0 -78
  42. package/templates/go/README.md +0 -25
  43. package/templates/javascript-typescript/.claude/commands/api-endpoint.md +0 -51
  44. package/templates/javascript-typescript/.claude/commands/debug.md +0 -52
  45. package/templates/javascript-typescript/.claude/commands/lint.md +0 -48
  46. package/templates/javascript-typescript/.claude/commands/npm-scripts.md +0 -48
  47. package/templates/javascript-typescript/.claude/commands/refactor.md +0 -55
  48. package/templates/javascript-typescript/.claude/commands/test.md +0 -61
  49. package/templates/javascript-typescript/.claude/commands/typescript-migrate.md +0 -51
  50. package/templates/javascript-typescript/.claude/settings.json +0 -142
  51. package/templates/javascript-typescript/.mcp.json +0 -80
  52. package/templates/javascript-typescript/CLAUDE.md +0 -185
  53. package/templates/javascript-typescript/README.md +0 -259
  54. package/templates/javascript-typescript/examples/angular-app/.claude/commands/components.md +0 -63
  55. package/templates/javascript-typescript/examples/angular-app/.claude/commands/services.md +0 -62
  56. package/templates/javascript-typescript/examples/node-api/.claude/commands/api-endpoint.md +0 -46
  57. package/templates/javascript-typescript/examples/node-api/.claude/commands/database.md +0 -56
  58. package/templates/javascript-typescript/examples/node-api/.claude/commands/middleware.md +0 -61
  59. package/templates/javascript-typescript/examples/node-api/.claude/commands/route.md +0 -57
  60. package/templates/javascript-typescript/examples/node-api/CLAUDE.md +0 -102
  61. package/templates/javascript-typescript/examples/react-app/.claude/commands/component.md +0 -29
  62. package/templates/javascript-typescript/examples/react-app/.claude/commands/hooks.md +0 -44
  63. package/templates/javascript-typescript/examples/react-app/.claude/commands/state-management.md +0 -45
  64. package/templates/javascript-typescript/examples/react-app/CLAUDE.md +0 -81
  65. package/templates/javascript-typescript/examples/react-app/agents/react-performance-optimization.md +0 -530
  66. package/templates/javascript-typescript/examples/react-app/agents/react-state-management.md +0 -295
  67. package/templates/javascript-typescript/examples/vue-app/.claude/commands/components.md +0 -46
  68. package/templates/javascript-typescript/examples/vue-app/.claude/commands/composables.md +0 -51
  69. package/templates/python/.claude/commands/lint.md +0 -111
  70. package/templates/python/.claude/commands/test.md +0 -73
  71. package/templates/python/.claude/settings.json +0 -153
  72. package/templates/python/.mcp.json +0 -78
  73. package/templates/python/CLAUDE.md +0 -276
  74. package/templates/python/examples/django-app/.claude/commands/admin.md +0 -264
  75. package/templates/python/examples/django-app/.claude/commands/django-model.md +0 -124
  76. package/templates/python/examples/django-app/.claude/commands/views.md +0 -222
  77. package/templates/python/examples/django-app/CLAUDE.md +0 -313
  78. package/templates/python/examples/django-app/agents/django-api-security.md +0 -642
  79. package/templates/python/examples/django-app/agents/django-database-optimization.md +0 -752
  80. package/templates/python/examples/fastapi-app/.claude/commands/api-endpoints.md +0 -513
  81. package/templates/python/examples/fastapi-app/.claude/commands/auth.md +0 -775
  82. package/templates/python/examples/fastapi-app/.claude/commands/database.md +0 -657
  83. package/templates/python/examples/fastapi-app/.claude/commands/deployment.md +0 -160
  84. package/templates/python/examples/fastapi-app/.claude/commands/testing.md +0 -927
  85. package/templates/python/examples/fastapi-app/CLAUDE.md +0 -229
  86. package/templates/python/examples/flask-app/.claude/commands/app-factory.md +0 -384
  87. package/templates/python/examples/flask-app/.claude/commands/blueprint.md +0 -243
  88. package/templates/python/examples/flask-app/.claude/commands/database.md +0 -410
  89. package/templates/python/examples/flask-app/.claude/commands/deployment.md +0 -620
  90. package/templates/python/examples/flask-app/.claude/commands/flask-route.md +0 -217
  91. package/templates/python/examples/flask-app/.claude/commands/testing.md +0 -559
  92. package/templates/python/examples/flask-app/CLAUDE.md +0 -391
  93. package/templates/ruby/.claude/commands/model.md +0 -360
  94. package/templates/ruby/.claude/commands/test.md +0 -480
  95. package/templates/ruby/.claude/settings.json +0 -146
  96. package/templates/ruby/.mcp.json +0 -83
  97. package/templates/ruby/CLAUDE.md +0 -284
  98. package/templates/ruby/examples/rails-app/.claude/commands/authentication.md +0 -490
  99. package/templates/ruby/examples/rails-app/CLAUDE.md +0 -376
  100. package/templates/rust/.mcp.json +0 -78
  101. package/templates/rust/README.md +0 -26
@@ -0,0 +1,682 @@
1
+ const chalk = require('chalk');
2
+ const fs = require('fs-extra');
3
+ const path = require('path');
4
+ const express = require('express');
5
+ const open = require('open');
6
+ const os = require('os');
7
+ const { spawn } = require('child_process');
8
+ const ConversationAnalyzer = require('./analytics/core/ConversationAnalyzer');
9
+ const StateCalculator = require('./analytics/core/StateCalculator');
10
+ const FileWatcher = require('./analytics/core/FileWatcher');
11
+ const DataCache = require('./analytics/data/DataCache');
12
+ const WebSocketServer = require('./analytics/notifications/WebSocketServer');
13
+
14
+ class ChatsMobile {
15
+ constructor(options = {}) {
16
+ this.app = express();
17
+ this.port = 9876; // Uncommon port for chats mobile
18
+ this.fileWatcher = new FileWatcher();
19
+ this.stateCalculator = new StateCalculator();
20
+ this.dataCache = new DataCache();
21
+ this.httpServer = null;
22
+ this.refreshTimeout = null;
23
+ this.webSocketServer = null;
24
+ this.options = options;
25
+ this.verbose = options.verbose || false;
26
+
27
+ // Initialize ConversationAnalyzer with proper parameters
28
+ const homeDir = os.homedir();
29
+ const claudeDir = path.join(homeDir, '.claude');
30
+ this.conversationAnalyzer = new ConversationAnalyzer(claudeDir, this.dataCache);
31
+
32
+ this.data = {
33
+ conversations: [],
34
+ conversationStates: {},
35
+ lastUpdate: new Date().toISOString()
36
+ };
37
+
38
+ // Track message counts per conversation to detect new messages
39
+ this.conversationMessageCounts = new Map();
40
+
41
+ // Track message snapshots to detect message updates (e.g., tool correlation)
42
+ this.conversationMessageSnapshots = new Map();
43
+ }
44
+
45
+ /**
46
+ * Log messages only if verbose mode is enabled
47
+ * @param {string} level - Log level ('info', 'warn', 'error')
48
+ * @param {string} message - Message to log
49
+ * @param {...any} args - Additional arguments
50
+ */
51
+ log(level, message, ...args) {
52
+ if (!this.verbose) return;
53
+
54
+ switch (level) {
55
+ case 'error':
56
+ console.error(message, ...args);
57
+ break;
58
+ case 'warn':
59
+ console.warn(message, ...args);
60
+ break;
61
+ case 'info':
62
+ default:
63
+ console.log(message, ...args);
64
+ break;
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Initialize the chats mobile server
70
+ */
71
+ async initialize() {
72
+ console.log(chalk.gray('🔧 Initializing Claude Code Chats Mobile...'));
73
+
74
+ try {
75
+ // Setup middleware
76
+ this.setupMiddleware();
77
+
78
+ // Setup routes
79
+ this.setupRoutes();
80
+
81
+ // Setup file watching
82
+ await this.setupFileWatching();
83
+
84
+ // Load initial data
85
+ await this.loadInitialData();
86
+
87
+ // Setup WebSocket server
88
+ await this.setupWebSocket();
89
+
90
+ this.log('info', chalk.green('✅ Chats Mobile initialized successfully'));
91
+ } catch (error) {
92
+ console.error(chalk.red('❌ Failed to initialize Chats Mobile:'), error);
93
+ throw error;
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Setup Express middleware
99
+ */
100
+ setupMiddleware() {
101
+ this.app.use(express.json());
102
+
103
+ // Serve static files from analytics-web directory (for services, components, etc.)
104
+ this.app.use('/services', express.static(path.join(__dirname, 'analytics-web', 'services')));
105
+ this.app.use('/components', express.static(path.join(__dirname, 'analytics-web', 'components')));
106
+ this.app.use('/assets', express.static(path.join(__dirname, 'analytics-web', 'assets')));
107
+ }
108
+
109
+ /**
110
+ * Setup API routes
111
+ */
112
+ setupRoutes() {
113
+ // API to get conversations
114
+ this.app.get('/api/conversations', (req, res) => {
115
+ try {
116
+ res.json({
117
+ conversations: this.data.conversations,
118
+ timestamp: new Date().toISOString(),
119
+ lastUpdate: this.data.lastUpdate
120
+ });
121
+ } catch (error) {
122
+ console.error('Error serving conversations:', error);
123
+ res.status(500).json({ error: 'Internal server error' });
124
+ }
125
+ });
126
+
127
+ // API to get conversation states (plural - for compatibility)
128
+ this.app.get('/api/conversation-states', (req, res) => {
129
+ try {
130
+ res.json({
131
+ activeStates: this.data.conversationStates,
132
+ timestamp: new Date().toISOString()
133
+ });
134
+ } catch (error) {
135
+ console.error('Error serving conversation states:', error);
136
+ res.status(500).json({ error: 'Internal server error' });
137
+ }
138
+ });
139
+
140
+ // API to get conversation state (singular - like main analytics server)
141
+ this.app.get('/api/conversation-state', async (req, res) => {
142
+ try {
143
+ // Calculate states for ALL conversations using StateCalculator
144
+ const activeStates = {};
145
+
146
+ for (const conversation of this.data.conversations) {
147
+ try {
148
+ // Get parsed messages for state calculation
149
+ const parsedMessages = await this.conversationAnalyzer.getParsedConversation(conversation.filePath);
150
+
151
+ // Use StateCalculator to determine current state
152
+ const state = this.stateCalculator.determineConversationState(
153
+ parsedMessages,
154
+ conversation.lastModified,
155
+ null // No running process detection for now
156
+ );
157
+
158
+ activeStates[conversation.id] = state;
159
+ } catch (error) {
160
+ console.warn(`Error calculating state for conversation ${conversation.id}:`, error.message);
161
+ activeStates[conversation.id] = 'Inactive';
162
+ }
163
+ }
164
+
165
+ res.json({
166
+ activeStates,
167
+ timestamp: new Date().toISOString(),
168
+ totalConversations: this.data.conversations.length
169
+ });
170
+ } catch (error) {
171
+ console.error('Error calculating conversation states:', error);
172
+ res.status(500).json({ error: 'Internal server error' });
173
+ }
174
+ });
175
+
176
+ // API to get specific conversation messages (with pagination support)
177
+ this.app.get('/api/conversations/:id/messages', async (req, res) => {
178
+ try {
179
+ const conversationId = req.params.id;
180
+ const conversation = this.data.conversations.find(conv => conv.id === conversationId);
181
+
182
+ if (!conversation) {
183
+ return res.status(404).json({ error: 'Conversation not found' });
184
+ }
185
+
186
+ // Get the actual parsed messages from the conversation file
187
+ const allMessages = await this.conversationAnalyzer.getParsedConversation(conversation.filePath);
188
+
189
+ // Parse pagination parameters
190
+ const page = parseInt(req.query.page) || 0;
191
+ const limit = parseInt(req.query.limit) || 50; // Default to 50 messages if no limit specified
192
+
193
+ if (!req.query.page && !req.query.limit) {
194
+ // No pagination requested - return all messages (backward compatibility)
195
+ res.json({
196
+ conversation: conversation,
197
+ messages: allMessages || [],
198
+ timestamp: new Date().toISOString()
199
+ });
200
+ return;
201
+ }
202
+
203
+ // Sort messages chronologically (oldest first)
204
+ const sortedMessages = (allMessages || []).sort((a, b) =>
205
+ new Date(a.timestamp) - new Date(b.timestamp)
206
+ );
207
+
208
+ const totalMessages = sortedMessages.length;
209
+ const totalPages = Math.ceil(totalMessages / limit);
210
+
211
+ // For reverse pagination: page 0 = most recent messages, page 1 = older messages, etc.
212
+ // Calculate from the end of the array going backwards
213
+ const endIndex = totalMessages - (page * limit);
214
+ const startIndex = Math.max(0, endIndex - limit);
215
+
216
+ // Get the requested page of messages
217
+ const paginatedMessages = sortedMessages.slice(startIndex, endIndex);
218
+
219
+ res.json({
220
+ conversation: conversation,
221
+ messages: paginatedMessages,
222
+ pagination: {
223
+ page: page,
224
+ limit: limit,
225
+ totalMessages: totalMessages,
226
+ totalPages: totalPages,
227
+ hasMore: startIndex > 0,
228
+ isFirstPage: page === 0,
229
+ isLastPage: startIndex <= 0
230
+ },
231
+ timestamp: new Date().toISOString()
232
+ });
233
+ } catch (error) {
234
+ console.error('Error serving conversation messages:', error);
235
+ res.status(500).json({ error: 'Internal server error' });
236
+ }
237
+ });
238
+
239
+ // Serve the mobile chats page as default
240
+ this.app.get('/', (req, res) => {
241
+ res.sendFile(path.join(__dirname, 'analytics-web', 'chats_mobile.html'));
242
+ });
243
+
244
+ // Fallback for any other routes (but not for API or static files)
245
+ this.app.get('*', (req, res) => {
246
+ // Don't redirect API calls or static files
247
+ if (req.path.startsWith('/api/') ||
248
+ req.path.startsWith('/services/') ||
249
+ req.path.startsWith('/components/') ||
250
+ req.path.startsWith('/assets/')) {
251
+ res.status(404).json({ error: 'Not found' });
252
+ return;
253
+ }
254
+ res.sendFile(path.join(__dirname, 'analytics-web', 'chats_mobile.html'));
255
+ });
256
+ }
257
+
258
+ /**
259
+ * Setup file watching for Claude Code conversations
260
+ */
261
+ async setupFileWatching() {
262
+ try {
263
+ const homeDir = os.homedir();
264
+ const claudeDir = path.join(homeDir, '.claude');
265
+
266
+ this.fileWatcher.setupFileWatchers(
267
+ claudeDir,
268
+ this.handleDataRefresh.bind(this),
269
+ () => {}, // processRefreshCallback (not needed for mobile)
270
+ this.dataCache,
271
+ this.handleConversationChange.bind(this)
272
+ );
273
+
274
+ this.log('info', chalk.green('👀 File watching setup successful'));
275
+ } catch (error) {
276
+ this.log('warn', chalk.yellow('⚠️ File watching setup failed:', error.message));
277
+ }
278
+ }
279
+
280
+ /**
281
+ * Handle data refresh from file watcher (with debouncing)
282
+ */
283
+ async handleDataRefresh() {
284
+ // Clear previous timeout to debounce rapid file changes
285
+ if (this.refreshTimeout) {
286
+ clearTimeout(this.refreshTimeout);
287
+ }
288
+
289
+ // Set a new timeout to refresh after 2 seconds of inactivity
290
+ this.refreshTimeout = setTimeout(async () => {
291
+ try {
292
+ await this.loadInitialData();
293
+ console.log(chalk.gray('🔄 Data refreshed from file changes'));
294
+ } catch (error) {
295
+ console.error('Error refreshing data:', error);
296
+ }
297
+ }, 2000);
298
+ }
299
+
300
+ /**
301
+ * Generate a snapshot of a message for change detection
302
+ * @param {Object} message - Message object
303
+ * @returns {string} Message snapshot hash
304
+ */
305
+ generateMessageSnapshot(message) {
306
+ // Create a hash based on key message properties that can change
307
+ const snapshot = {
308
+ id: message.id,
309
+ role: message.role,
310
+ contentLength: Array.isArray(message.content) ? message.content.length : (message.content?.length || 0),
311
+ toolResultsCount: message.toolResults ? message.toolResults.length : 0,
312
+ hasToolUse: Array.isArray(message.content) && message.content.some(block => block.type === 'tool_use'),
313
+ hasToolResults: !!(message.toolResults && message.toolResults.length > 0)
314
+ };
315
+ return JSON.stringify(snapshot);
316
+ }
317
+
318
+ /**
319
+ * Handle conversation changes
320
+ */
321
+ async handleConversationChange(conversationId) {
322
+ this.log('info', chalk.gray(`💬 Conversation ${conversationId.slice(-8)} changed`));
323
+
324
+ // Get the conversation to find new messages
325
+ const conversation = this.data.conversations.find(conv => conv.id === conversationId);
326
+ if (!conversation) return;
327
+
328
+ try {
329
+ // Get the latest parsed messages with proper tool correlation
330
+ const parsedMessages = await this.conversationAnalyzer.getParsedConversation(conversation.filePath);
331
+
332
+ if (parsedMessages && parsedMessages.length > 0) {
333
+ // Get the previous message count and snapshots for this conversation
334
+ const previousCount = this.conversationMessageCounts.get(conversationId) || 0;
335
+ const currentCount = parsedMessages.length;
336
+ const previousSnapshots = this.conversationMessageSnapshots.get(conversationId) || [];
337
+
338
+ // Update the count
339
+ this.conversationMessageCounts.set(conversationId, currentCount);
340
+
341
+ // Generate current snapshots
342
+ const currentSnapshots = parsedMessages.map(msg => this.generateMessageSnapshot(msg));
343
+ this.conversationMessageSnapshots.set(conversationId, currentSnapshots);
344
+
345
+ // Find new messages (by count increase)
346
+ const newMessages = currentCount > previousCount ? parsedMessages.slice(previousCount) : [];
347
+
348
+ // Find updated messages (by comparing snapshots)
349
+ const updatedMessages = [];
350
+ for (let i = 0; i < Math.min(previousCount, currentCount); i++) {
351
+ if (i < previousSnapshots.length && currentSnapshots[i] !== previousSnapshots[i]) {
352
+ this.log('info', chalk.yellow(`🔄 Message ${i} changed:`));
353
+ this.log('info', chalk.gray(` Previous: ${previousSnapshots[i]}`));
354
+ this.log('info', chalk.gray(` Current: ${currentSnapshots[i]}`));
355
+ this.log('info', chalk.gray(` Message: role=${parsedMessages[i].role}, content=${typeof parsedMessages[i].content}, toolResults=${parsedMessages[i].toolResults?.length || 0}`));
356
+ updatedMessages.push(parsedMessages[i]);
357
+ }
358
+ }
359
+
360
+ // Combine new and updated messages, avoiding duplicates
361
+ const messagesToBroadcast = [...newMessages];
362
+ for (const updatedMsg of updatedMessages) {
363
+ if (!newMessages.find(newMsg => newMsg.id === updatedMsg.id)) {
364
+ messagesToBroadcast.push(updatedMsg);
365
+ }
366
+ }
367
+
368
+ if (messagesToBroadcast.length > 0) {
369
+ this.log('info', chalk.cyan(`🔧 Found ${newMessages.length} new messages and ${updatedMessages.length} updated messages in conversation ${conversationId.slice(-8)}`));
370
+
371
+ // Broadcast each message (new or updated)
372
+ for (const message of messagesToBroadcast) {
373
+ if (this.webSocketServer) {
374
+ // Log message details for debugging
375
+ const messageType = message.toolResults && message.toolResults.length > 0 ? 'tool' : 'text';
376
+ const toolCount = message.toolResults ? message.toolResults.length : 0;
377
+ const hasToolsInContent = Array.isArray(message.content) &&
378
+ message.content.some(block => block.type === 'tool_use');
379
+ const isUpdatedMessage = updatedMessages.includes(message);
380
+
381
+ this.log('info', chalk.cyan(`🌐 Broadcasting ${isUpdatedMessage ? 'updated' : 'new'} ${messageType} message (${toolCount} tools) for ${conversationId.slice(-8)}`));
382
+ this.log('info', chalk.gray(` Message details: role=${message.role}, hasToolResults=${!!message.toolResults}, hasToolsInContent=${hasToolsInContent}`));
383
+ if (message.toolResults) {
384
+ this.log('info', chalk.gray(` Tool results: ${message.toolResults.map(tr => tr.tool_use_id || 'no-id').join(', ')}`));
385
+ }
386
+
387
+ this.webSocketServer.broadcast({
388
+ type: 'new_message',
389
+ data: {
390
+ conversationId: conversationId,
391
+ message: message,
392
+ metadata: {
393
+ timestamp: new Date().toISOString(),
394
+ totalMessages: currentCount,
395
+ hasTools: !!(message.toolResults && message.toolResults.length > 0),
396
+ toolCount: toolCount,
397
+ messageIndex: parsedMessages.indexOf(message),
398
+ isUpdated: isUpdatedMessage
399
+ }
400
+ }
401
+ });
402
+ }
403
+ }
404
+ } else {
405
+ console.log(chalk.gray(`📝 No new messages in conversation ${conversationId.slice(-8)} (${currentCount} total)`));
406
+ }
407
+ }
408
+ } catch (error) {
409
+ console.warn(chalk.yellow('⚠️ Error handling conversation change:', error.message));
410
+ }
411
+ }
412
+
413
+ /**
414
+ * Setup WebSocket server for real-time updates (will be initialized after HTTP server starts)
415
+ */
416
+ async setupWebSocket() {
417
+ // WebSocketServer will be initialized after HTTP server is created
418
+ console.log(chalk.gray('🔧 WebSocket server setup prepared'));
419
+ }
420
+
421
+ /**
422
+ * Load initial conversation data
423
+ */
424
+ async loadInitialData() {
425
+ try {
426
+ const homeDir = os.homedir();
427
+ const claudeDataDir = path.join(homeDir, '.claude');
428
+
429
+ if (await fs.pathExists(claudeDataDir)) {
430
+ // Use ConversationAnalyzer to load conversations
431
+ const conversations = await this.conversationAnalyzer.loadConversations(this.stateCalculator);
432
+
433
+ this.data.conversations = conversations || [];
434
+ this.data.conversationStates = {}; // Will be populated by state calculation if needed
435
+ this.data.lastUpdate = new Date().toISOString();
436
+
437
+ // Initialize message counts and snapshots for each conversation
438
+ for (const conversation of conversations) {
439
+ try {
440
+ const parsedMessages = await this.conversationAnalyzer.getParsedConversation(conversation.filePath);
441
+ this.conversationMessageCounts.set(conversation.id, parsedMessages.length);
442
+
443
+ // Initialize snapshots for change detection
444
+ const snapshots = parsedMessages.map(msg => this.generateMessageSnapshot(msg));
445
+ this.conversationMessageSnapshots.set(conversation.id, snapshots);
446
+ } catch (error) {
447
+ // If we can't parse the conversation, set count to 0 and empty snapshots
448
+ this.conversationMessageCounts.set(conversation.id, 0);
449
+ this.conversationMessageSnapshots.set(conversation.id, []);
450
+ }
451
+ }
452
+
453
+ console.log(chalk.green(`📂 Loaded ${this.data.conversations.length} conversations`));
454
+ console.log(chalk.gray(`📊 Initialized message counts for ${this.conversationMessageCounts.size} conversations`));
455
+ } else {
456
+ console.log(chalk.yellow('⚠️ No Claude Code data directory found'));
457
+ console.log(chalk.gray(` Expected directory: ${claudeDataDir}`));
458
+ }
459
+ } catch (error) {
460
+ console.warn(chalk.yellow('⚠️ Failed to load initial data:', error.message));
461
+ }
462
+ }
463
+
464
+ /**
465
+ * Start the mobile chats server
466
+ */
467
+ async startServer() {
468
+ return new Promise(async (resolve) => {
469
+ this.httpServer = this.app.listen(this.port, async () => {
470
+ this.localUrl = `http://localhost:${this.port}`;
471
+ console.log(chalk.green(`📱 Chats Mobile server started at ${this.localUrl}`));
472
+
473
+ // Initialize WebSocket server with HTTP server
474
+ try {
475
+ this.webSocketServer = new WebSocketServer(this.httpServer, {
476
+ port: this.port,
477
+ path: '/ws'
478
+ });
479
+ await this.webSocketServer.initialize();
480
+ this.log('info', chalk.green('🌐 WebSocket server initialized'));
481
+ } catch (error) {
482
+ this.log('warn', chalk.yellow('⚠️ WebSocket server failed to initialize:', error.message));
483
+ }
484
+
485
+ // Setup Cloudflare Tunnel if requested
486
+ if (this.options.tunnel) {
487
+ await this.setupCloudflaredTunnel();
488
+ }
489
+
490
+ resolve();
491
+ });
492
+ });
493
+ }
494
+
495
+ /**
496
+ * Setup Cloudflare Tunnel for remote access
497
+ */
498
+ async setupCloudflaredTunnel() {
499
+ console.log(chalk.blue('☁️ Setting up Cloudflare Tunnel...'));
500
+ console.log(chalk.gray(`📡 Tunneling ${this.localUrl}...`));
501
+
502
+ try {
503
+ const { spawn } = require('child_process');
504
+
505
+ // Spawn cloudflared tunnel with more options for better compatibility
506
+ const cloudflared = spawn('cloudflared', [
507
+ 'tunnel',
508
+ '--url', this.localUrl,
509
+ '--no-autoupdate' // Prevent update check that can cause delays
510
+ ], {
511
+ stdio: ['pipe', 'pipe', 'pipe'],
512
+ env: { ...process.env, NO_UPDATE_NOTIFIER: '1' } // Disable update notifier
513
+ });
514
+
515
+ // Store process reference for cleanup
516
+ this.cloudflaredProcess = cloudflared;
517
+
518
+ // Parse tunnel URL from cloudflared output
519
+ return new Promise((resolve) => {
520
+ let output = '';
521
+
522
+ cloudflared.stdout.on('data', (data) => {
523
+ const str = data.toString();
524
+ output += str;
525
+
526
+ // Always show cloudflared output for debugging tunnel issues
527
+ console.log(chalk.gray(`[cloudflared] ${str.trim()}`));
528
+
529
+ // Look for various tunnel URL patterns
530
+ let urlMatch = str.match(/https:\/\/[a-zA-Z0-9-]+\.trycloudflare\.com/);
531
+ if (!urlMatch) {
532
+ // Try alternative patterns
533
+ urlMatch = str.match(/https:\/\/[a-zA-Z0-9-]+\.cfargotunnel\.com/);
534
+ }
535
+ if (!urlMatch) {
536
+ // Try to find any HTTPS URL in the output
537
+ urlMatch = str.match(/https:\/\/[a-zA-Z0-9.-]+\.(?:trycloudflare|cfargotunnel)\.com/);
538
+ }
539
+
540
+ if (urlMatch) {
541
+ this.tunnelUrl = urlMatch[0];
542
+ console.log(chalk.green(`☁️ Cloudflare Tunnel ready: ${this.tunnelUrl}`));
543
+ resolve(this.tunnelUrl);
544
+ }
545
+ });
546
+
547
+ cloudflared.stderr.on('data', (data) => {
548
+ const str = data.toString();
549
+ // Always show stderr for debugging
550
+ console.error(chalk.gray(`[cloudflared stderr] ${str.trim()}`));
551
+
552
+ // Sometimes tunnel URLs appear in stderr
553
+ let urlMatch = str.match(/https:\/\/[a-zA-Z0-9-]+\.(?:trycloudflare|cfargotunnel)\.com/);
554
+ if (urlMatch && !this.tunnelUrl) {
555
+ this.tunnelUrl = urlMatch[0];
556
+ console.log(chalk.green(`☁️ Cloudflare Tunnel ready: ${this.tunnelUrl}`));
557
+ resolve(this.tunnelUrl);
558
+ }
559
+ });
560
+
561
+ cloudflared.on('error', (error) => {
562
+ console.error(chalk.red('❌ Failed to start Cloudflare Tunnel:'), error.message);
563
+ console.log(chalk.yellow('💡 Make sure cloudflared is installed: https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/installation/'));
564
+ resolve(null);
565
+ });
566
+
567
+ cloudflared.on('close', (code) => {
568
+ console.log(chalk.yellow(`⚠️ Cloudflared process exited with code ${code}`));
569
+ if (!this.tunnelUrl) {
570
+ resolve(null);
571
+ }
572
+ });
573
+
574
+ // Timeout after 45 seconds (increased from 30)
575
+ setTimeout(() => {
576
+ if (!this.tunnelUrl) {
577
+ console.warn(chalk.yellow('⚠️ Tunnel URL not detected within 45 seconds'));
578
+ console.log(chalk.gray('Full cloudflared output:'));
579
+ console.log(chalk.gray(output));
580
+ console.log(chalk.blue('💡 You can manually run: ') + chalk.white(`cloudflared tunnel --url ${this.localUrl}`));
581
+ console.log(chalk.blue(' Then copy the tunnel URL and access it in your browser.'));
582
+ resolve(null);
583
+ }
584
+ }, 45000);
585
+ });
586
+ } catch (error) {
587
+ console.error(chalk.red('❌ Error setting up Cloudflare Tunnel:'), error.message);
588
+ return null;
589
+ }
590
+ }
591
+
592
+ /**
593
+ * Open browser to the mobile chats interface
594
+ */
595
+ async openBrowser() {
596
+ try {
597
+ // Use tunnel URL if available, otherwise local URL
598
+ const url = this.tunnelUrl || this.localUrl || `http://localhost:${this.port}`;
599
+ console.log(chalk.cyan(`🌐 Opening browser to ${url}`));
600
+ await open(url);
601
+ } catch (error) {
602
+ console.warn(chalk.yellow('⚠️ Could not auto-open browser:', error.message));
603
+ }
604
+ }
605
+
606
+ /**
607
+ * Stop the server
608
+ */
609
+ async stop() {
610
+ if (this.cloudflaredProcess) {
611
+ try {
612
+ this.cloudflaredProcess.kill('SIGTERM');
613
+ this.log('info', chalk.gray('☁️ Cloudflare Tunnel stopped'));
614
+ } catch (error) {
615
+ this.log('warn', chalk.yellow('⚠️ Error stopping Cloudflare Tunnel:', error.message));
616
+ }
617
+ }
618
+
619
+ if (this.webSocketServer) {
620
+ try {
621
+ await this.webSocketServer.close();
622
+ this.log('info', chalk.gray('🌐 WebSocket server stopped'));
623
+ } catch (error) {
624
+ this.log('warn', chalk.yellow('⚠️ Error stopping WebSocket server:', error.message));
625
+ }
626
+ }
627
+
628
+ if (this.httpServer) {
629
+ await new Promise((resolve) => {
630
+ this.httpServer.close(resolve);
631
+ });
632
+ }
633
+
634
+ if (this.fileWatcher) {
635
+ await this.fileWatcher.stop();
636
+ }
637
+
638
+ console.log(chalk.gray('🛑 Chats Mobile server stopped'));
639
+ }
640
+ }
641
+
642
+ /**
643
+ * Start the mobile chats server
644
+ */
645
+ async function startChatsMobile(options = {}) {
646
+ console.log(chalk.blue('📱 Starting Claude Code Chats Mobile...'));
647
+
648
+ const chatsMobile = new ChatsMobile(options);
649
+
650
+ try {
651
+ await chatsMobile.initialize();
652
+ await chatsMobile.startServer();
653
+
654
+ if (!options.noOpen) {
655
+ await chatsMobile.openBrowser();
656
+ }
657
+
658
+ console.log(chalk.green('✅ Claude Code Chats Mobile is running!'));
659
+
660
+ // Show access URLs
661
+ console.log(chalk.cyan(`📱 Local access: ${chatsMobile.localUrl}`));
662
+ if (chatsMobile.tunnelUrl) {
663
+ console.log(chalk.cyan(`☁️ Remote access: ${chatsMobile.tunnelUrl}`));
664
+ console.log(chalk.blue(`🌐 Opening remote URL: ${chatsMobile.tunnelUrl}`));
665
+ }
666
+
667
+ console.log(chalk.gray('Press Ctrl+C to stop'));
668
+
669
+ // Handle graceful shutdown
670
+ process.on('SIGINT', async () => {
671
+ console.log(chalk.yellow('\n🛑 Shutting down...'));
672
+ await chatsMobile.stop();
673
+ process.exit(0);
674
+ });
675
+
676
+ } catch (error) {
677
+ console.error(chalk.red('❌ Failed to start Chats Mobile:'), error);
678
+ process.exit(1);
679
+ }
680
+ }
681
+
682
+ module.exports = { ChatsMobile, startChatsMobile };