claude-code-templates 1.15.0 → 1.15.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/README.md +7 -7
- package/bin/create-claude-config.js +15 -8
- package/package.json +2 -3
- package/src/analytics/core/AgentAnalyzer.js +17 -3
- package/src/analytics/core/ProcessDetector.js +23 -7
- package/src/analytics/core/StateCalculator.js +102 -33
- package/src/analytics/data/DataCache.js +7 -7
- package/src/analytics-web/chats_mobile.html +2590 -0
- package/src/analytics-web/components/App.js +10 -10
- package/src/analytics-web/components/SessionTimer.js +1 -1
- package/src/analytics-web/components/Sidebar.js +5 -14
- package/src/analytics-web/index.html +932 -78
- package/src/analytics.js +263 -5
- package/src/chats-mobile.js +682 -0
- package/src/claude-api-proxy.js +460 -0
- package/src/file-operations.js +422 -83
- package/src/health-check.js +310 -0
- package/src/index.js +944 -56
- package/src/tracking-service.js +31 -34
- package/components/agents/api-security-audit.md +0 -92
- package/components/agents/database-optimization.md +0 -94
- package/components/agents/react-performance-optimization.md +0 -64
- package/components/commands/check-file.md +0 -53
- package/components/commands/generate-tests.md +0 -68
- package/components/mcps/deepgraph-nextjs.json +0 -12
- package/components/mcps/deepgraph-react.json +0 -12
- package/components/mcps/deepgraph-typescript.json +0 -12
- package/components/mcps/deepgraph-vue.json +0 -12
- package/components/mcps/filesystem-access.json +0 -12
- package/components/mcps/github-integration.json +0 -11
- package/components/mcps/memory-integration.json +0 -8
- package/components/mcps/mysql-integration.json +0 -11
- package/components/mcps/postgresql-integration.json +0 -11
- package/components/mcps/web-fetch.json +0 -8
- package/src/analytics-web/components/AgentsPage.js +0 -4761
- package/templates/common/.claude/commands/git-workflow.md +0 -239
- package/templates/common/.claude/commands/project-setup.md +0 -316
- package/templates/common/.mcp.json +0 -41
- package/templates/common/CLAUDE.md +0 -109
- package/templates/common/README.md +0 -96
- package/templates/go/.mcp.json +0 -78
- package/templates/go/README.md +0 -25
- package/templates/javascript-typescript/.claude/commands/api-endpoint.md +0 -51
- package/templates/javascript-typescript/.claude/commands/debug.md +0 -52
- package/templates/javascript-typescript/.claude/commands/lint.md +0 -48
- package/templates/javascript-typescript/.claude/commands/npm-scripts.md +0 -48
- package/templates/javascript-typescript/.claude/commands/refactor.md +0 -55
- package/templates/javascript-typescript/.claude/commands/test.md +0 -61
- package/templates/javascript-typescript/.claude/commands/typescript-migrate.md +0 -51
- package/templates/javascript-typescript/.claude/settings.json +0 -142
- package/templates/javascript-typescript/.mcp.json +0 -80
- package/templates/javascript-typescript/CLAUDE.md +0 -185
- package/templates/javascript-typescript/README.md +0 -259
- package/templates/javascript-typescript/examples/angular-app/.claude/commands/components.md +0 -63
- package/templates/javascript-typescript/examples/angular-app/.claude/commands/services.md +0 -62
- package/templates/javascript-typescript/examples/node-api/.claude/commands/api-endpoint.md +0 -46
- package/templates/javascript-typescript/examples/node-api/.claude/commands/database.md +0 -56
- package/templates/javascript-typescript/examples/node-api/.claude/commands/middleware.md +0 -61
- package/templates/javascript-typescript/examples/node-api/.claude/commands/route.md +0 -57
- package/templates/javascript-typescript/examples/node-api/CLAUDE.md +0 -102
- package/templates/javascript-typescript/examples/react-app/.claude/commands/component.md +0 -29
- package/templates/javascript-typescript/examples/react-app/.claude/commands/hooks.md +0 -44
- package/templates/javascript-typescript/examples/react-app/.claude/commands/state-management.md +0 -45
- package/templates/javascript-typescript/examples/react-app/CLAUDE.md +0 -81
- package/templates/javascript-typescript/examples/react-app/agents/react-performance-optimization.md +0 -530
- package/templates/javascript-typescript/examples/react-app/agents/react-state-management.md +0 -295
- package/templates/javascript-typescript/examples/vue-app/.claude/commands/components.md +0 -46
- package/templates/javascript-typescript/examples/vue-app/.claude/commands/composables.md +0 -51
- package/templates/python/.claude/commands/lint.md +0 -111
- package/templates/python/.claude/commands/test.md +0 -73
- package/templates/python/.claude/settings.json +0 -153
- package/templates/python/.mcp.json +0 -78
- package/templates/python/CLAUDE.md +0 -276
- package/templates/python/examples/django-app/.claude/commands/admin.md +0 -264
- package/templates/python/examples/django-app/.claude/commands/django-model.md +0 -124
- package/templates/python/examples/django-app/.claude/commands/views.md +0 -222
- package/templates/python/examples/django-app/CLAUDE.md +0 -313
- package/templates/python/examples/django-app/agents/django-api-security.md +0 -642
- package/templates/python/examples/django-app/agents/django-database-optimization.md +0 -752
- package/templates/python/examples/fastapi-app/.claude/commands/api-endpoints.md +0 -513
- package/templates/python/examples/fastapi-app/.claude/commands/auth.md +0 -775
- package/templates/python/examples/fastapi-app/.claude/commands/database.md +0 -657
- package/templates/python/examples/fastapi-app/.claude/commands/deployment.md +0 -160
- package/templates/python/examples/fastapi-app/.claude/commands/testing.md +0 -927
- package/templates/python/examples/fastapi-app/CLAUDE.md +0 -229
- package/templates/python/examples/flask-app/.claude/commands/app-factory.md +0 -384
- package/templates/python/examples/flask-app/.claude/commands/blueprint.md +0 -243
- package/templates/python/examples/flask-app/.claude/commands/database.md +0 -410
- package/templates/python/examples/flask-app/.claude/commands/deployment.md +0 -620
- package/templates/python/examples/flask-app/.claude/commands/flask-route.md +0 -217
- package/templates/python/examples/flask-app/.claude/commands/testing.md +0 -559
- package/templates/python/examples/flask-app/CLAUDE.md +0 -391
- package/templates/ruby/.claude/commands/model.md +0 -360
- package/templates/ruby/.claude/commands/test.md +0 -480
- package/templates/ruby/.claude/settings.json +0 -146
- package/templates/ruby/.mcp.json +0 -83
- package/templates/ruby/CLAUDE.md +0 -284
- package/templates/ruby/examples/rails-app/.claude/commands/authentication.md +0 -490
- package/templates/ruby/examples/rails-app/CLAUDE.md +0 -376
- package/templates/rust/.mcp.json +0 -78
- 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 };
|