claude-code-templates 1.14.16 → 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.
@@ -0,0 +1,460 @@
1
+ const express = require('express');
2
+ const fs = require('fs-extra');
3
+ const path = require('path');
4
+ const os = require('os');
5
+ const { v4: uuidv4 } = require('uuid');
6
+ const chalk = require('chalk');
7
+
8
+ class ClaudeAPIProxy {
9
+ constructor() {
10
+ this.app = express();
11
+ this.port = 3335;
12
+ this.claudeDir = path.join(os.homedir(), '.claude');
13
+
14
+ // Store active sessions and conversation contexts
15
+ this.activeSessions = new Map();
16
+ this.conversationContexts = new Map();
17
+
18
+ this.setupMiddleware();
19
+ this.setupRoutes();
20
+ }
21
+
22
+ setupMiddleware() {
23
+ this.app.use(express.json());
24
+ this.app.use((req, res, next) => {
25
+ res.header('Access-Control-Allow-Origin', '*');
26
+ res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
27
+ res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization');
28
+ if (req.method === 'OPTIONS') {
29
+ res.sendStatus(200);
30
+ } else {
31
+ next();
32
+ }
33
+ });
34
+ }
35
+
36
+ setupRoutes() {
37
+ // Get active conversations/sessions
38
+ this.app.get('/api/sessions', async (req, res) => {
39
+ try {
40
+ const sessions = await this.getActiveSessions();
41
+ res.json({ sessions });
42
+ } catch (error) {
43
+ console.error('Error getting sessions:', error);
44
+ res.status(500).json({ error: error.message });
45
+ }
46
+ });
47
+
48
+ // Send message to Claude (main endpoint)
49
+ this.app.post('/api/send-message', async (req, res) => {
50
+ try {
51
+ const { sessionId, message, projectPath } = req.body;
52
+
53
+ if (!sessionId || !message) {
54
+ return res.status(400).json({ error: 'sessionId and message are required' });
55
+ }
56
+
57
+ const result = await this.sendMessageToClaude(sessionId, message, projectPath);
58
+ res.json(result);
59
+
60
+ } catch (error) {
61
+ console.error('Error sending message:', error);
62
+ res.status(500).json({ error: error.message });
63
+ }
64
+ });
65
+
66
+ // Get conversation history
67
+ this.app.get('/api/conversation/:sessionId', async (req, res) => {
68
+ try {
69
+ const { sessionId } = req.params;
70
+ const conversation = await this.getConversationHistory(sessionId);
71
+ res.json({ conversation });
72
+ } catch (error) {
73
+ console.error('Error getting conversation:', error);
74
+ res.status(500).json({ error: error.message });
75
+ }
76
+ });
77
+ }
78
+
79
+ async getActiveSessions() {
80
+ const projectsDir = path.join(this.claudeDir, 'projects');
81
+
82
+ if (!(await fs.pathExists(projectsDir))) {
83
+ return [];
84
+ }
85
+
86
+ const sessions = [];
87
+ const projectDirs = await fs.readdir(projectsDir);
88
+
89
+ for (const projectDir of projectDirs) {
90
+ const projectPath = path.join(projectsDir, projectDir);
91
+ const files = await fs.readdir(projectPath);
92
+
93
+ for (const file of files) {
94
+ if (file.endsWith('.jsonl')) {
95
+ const sessionId = path.basename(file, '.jsonl');
96
+ const filePath = path.join(projectPath, file);
97
+ const stats = await fs.stat(filePath);
98
+
99
+ // Get basic info about the session
100
+ const lastMessage = await this.getLastMessage(filePath);
101
+
102
+ sessions.push({
103
+ sessionId,
104
+ projectPath: this.decodeProjectPath(projectDir),
105
+ filePath,
106
+ lastModified: stats.mtime,
107
+ lastMessage: lastMessage?.content || 'No messages',
108
+ messageCount: await this.getMessageCount(filePath)
109
+ });
110
+ }
111
+ }
112
+ }
113
+
114
+ // Sort by most recent activity
115
+ return sessions.sort((a, b) => new Date(b.lastModified) - new Date(a.lastModified));
116
+ }
117
+
118
+ decodeProjectPath(encodedPath) {
119
+ return encodedPath.replace(/-/g, '/').replace(/^Users/, '/Users');
120
+ }
121
+
122
+ async getLastMessage(filePath) {
123
+ try {
124
+ const content = await fs.readFile(filePath, 'utf8');
125
+ const lines = content.trim().split('\n').filter(line => line.trim());
126
+
127
+ if (lines.length === 0) return null;
128
+
129
+ const lastLine = lines[lines.length - 1];
130
+ const lastMessage = JSON.parse(lastLine);
131
+
132
+ return {
133
+ content: this.extractMessageContent(lastMessage),
134
+ timestamp: lastMessage.timestamp,
135
+ role: lastMessage.message?.role || lastMessage.type
136
+ };
137
+ } catch (error) {
138
+ console.error('Error reading last message:', error);
139
+ return null;
140
+ }
141
+ }
142
+
143
+ async getMessageCount(filePath) {
144
+ try {
145
+ const content = await fs.readFile(filePath, 'utf8');
146
+ const lines = content.trim().split('\n').filter(line => line.trim());
147
+ return lines.length;
148
+ } catch (error) {
149
+ return 0;
150
+ }
151
+ }
152
+
153
+ extractMessageContent(messageObj) {
154
+ if (messageObj.message?.content) {
155
+ if (typeof messageObj.message.content === 'string') {
156
+ return messageObj.message.content;
157
+ }
158
+ if (Array.isArray(messageObj.message.content)) {
159
+ const textContent = messageObj.message.content
160
+ .filter(item => item.type === 'text')
161
+ .map(item => item.text)
162
+ .join(' ');
163
+ return textContent || '[Tool use or other content]';
164
+ }
165
+ }
166
+ return '[No content]';
167
+ }
168
+
169
+ async sendMessageToClaude(sessionId, messageContent, projectPath) {
170
+ console.log(chalk.blue(`📤 Sending message to session ${sessionId}`));
171
+
172
+ // Find the conversation file
173
+ const conversationFile = await this.findConversationFile(sessionId, projectPath);
174
+
175
+ if (!conversationFile) {
176
+ throw new Error(`Conversation file not found for session ${sessionId}`);
177
+ }
178
+
179
+ // Get conversation context
180
+ const context = await this.getConversationContext(conversationFile);
181
+
182
+ // Create user message in Claude Code format
183
+ const userMessage = this.createUserMessage(messageContent, context, sessionId);
184
+
185
+ // Append to JSONL file
186
+ await this.appendToConversation(conversationFile, userMessage);
187
+
188
+ console.log(chalk.green(`✅ Message sent to ${conversationFile}`));
189
+
190
+ // Try to notify Claude Code process about the file change
191
+ await this.notifyClaudeProcess();
192
+
193
+ // TODO: Monitor for Claude Code response
194
+
195
+ return {
196
+ success: true,
197
+ messageId: userMessage.uuid,
198
+ sessionId,
199
+ message: 'Message sent to Claude Code conversation'
200
+ };
201
+ }
202
+
203
+ async findConversationFile(sessionId, projectPath) {
204
+ const projectsDir = path.join(this.claudeDir, 'projects');
205
+
206
+ // If projectPath provided, look there first
207
+ if (projectPath) {
208
+ const encodedPath = this.encodeProjectPath(projectPath);
209
+ const targetDir = path.join(projectsDir, encodedPath);
210
+ const conversationFile = path.join(targetDir, `${sessionId}.jsonl`);
211
+
212
+ if (await fs.pathExists(conversationFile)) {
213
+ return conversationFile;
214
+ }
215
+ }
216
+
217
+ // Otherwise, search all projects
218
+ const projectDirs = await fs.readdir(projectsDir);
219
+
220
+ for (const projectDir of projectDirs) {
221
+ const conversationFile = path.join(projectsDir, projectDir, `${sessionId}.jsonl`);
222
+
223
+ if (await fs.pathExists(conversationFile)) {
224
+ return conversationFile;
225
+ }
226
+ }
227
+
228
+ return null;
229
+ }
230
+
231
+ encodeProjectPath(projectPath) {
232
+ return projectPath.replace(/\\//g, '-').replace(/^-/, '');
233
+ }
234
+
235
+ async getConversationContext(conversationFile) {
236
+ try {
237
+ const content = await fs.readFile(conversationFile, 'utf8');
238
+ const lines = content.trim().split('\n').filter(line => line.trim());
239
+
240
+ if (lines.length === 0) {
241
+ return { lastMessage: null, cwd: process.cwd(), version: '1.0.44' };
242
+ }
243
+
244
+ // Find the last valid JSON line (iterate backwards)
245
+ let lastMessage = null;
246
+ for (let i = lines.length - 1; i >= 0; i--) {
247
+ const line = lines[i].trim();
248
+ try {
249
+ lastMessage = JSON.parse(line);
250
+ break; // Found valid JSON, break out of loop
251
+ } catch (jsonError) {
252
+ // Skip invalid JSON lines
253
+ console.warn(`Skipping invalid JSON line ${i + 1}: ${line.substring(0, 50)}...`);
254
+ continue;
255
+ }
256
+ }
257
+
258
+ if (!lastMessage) {
259
+ console.warn('No valid JSON message found in conversation file');
260
+ return { lastMessage: null, cwd: process.cwd(), version: '1.0.44' };
261
+ }
262
+
263
+ return {
264
+ lastMessage,
265
+ cwd: lastMessage.cwd || process.cwd(),
266
+ version: lastMessage.version || '1.0.44',
267
+ sessionId: lastMessage.sessionId
268
+ };
269
+ } catch (error) {
270
+ console.error('Error getting conversation context:', error);
271
+ return { lastMessage: null, cwd: process.cwd(), version: '1.0.44' };
272
+ }
273
+ }
274
+
275
+ createUserMessage(content, context, sessionId) {
276
+ const uuid = uuidv4();
277
+ const timestamp = new Date().toISOString();
278
+
279
+ return {
280
+ parentUuid: context.lastMessage?.uuid || null,
281
+ isSidechain: false,
282
+ userType: "external",
283
+ cwd: context.cwd,
284
+ sessionId: sessionId,
285
+ version: context.version,
286
+ type: "user",
287
+ message: {
288
+ role: "user",
289
+ content: content
290
+ },
291
+ uuid: uuid,
292
+ timestamp: timestamp
293
+ };
294
+ }
295
+
296
+ async appendToConversation(conversationFile, messageObj) {
297
+ const messageJson = JSON.stringify(messageObj);
298
+ await fs.appendFile(conversationFile, messageJson + '\n');
299
+
300
+ // Force file system change notification by touching the file
301
+ const now = new Date();
302
+ await fs.utimes(conversationFile, now, now);
303
+
304
+ console.log(chalk.green(`📝 Message appended to ${path.basename(conversationFile)}`));
305
+ }
306
+
307
+ async getConversationHistory(sessionId) {
308
+ const conversationFile = await this.findConversationFile(sessionId);
309
+
310
+ if (!conversationFile) {
311
+ throw new Error(`Conversation not found for session ${sessionId}`);
312
+ }
313
+
314
+ const content = await fs.readFile(conversationFile, 'utf8');
315
+ const lines = content.trim().split('\n').filter(line => line.trim());
316
+
317
+ const messages = lines
318
+ .map(line => {
319
+ try {
320
+ return JSON.parse(line);
321
+ } catch (e) {
322
+ return null;
323
+ }
324
+ })
325
+ .filter(msg => msg !== null);
326
+
327
+ return messages;
328
+ }
329
+
330
+ start() {
331
+ return new Promise((resolve) => {
332
+ this.server = this.app.listen(this.port, () => {
333
+ console.log(chalk.green(`🌉 Claude API Proxy running on http://localhost:${this.port}`));
334
+ console.log(chalk.blue(`📡 Ready to intercept and send messages to Claude Code`));
335
+ resolve();
336
+ });
337
+ });
338
+ }
339
+
340
+ stop() {
341
+ if (this.server) {
342
+ this.server.close();
343
+ console.log(chalk.yellow(`🔌 Claude API Proxy stopped`));
344
+ }
345
+ }
346
+ }
347
+
348
+ module.exports = ClaudeAPIProxy;
349
+
350
+ // Method to notify Claude Code process
351
+ ClaudeAPIProxy.prototype.notifyClaudeProcess = async function() {
352
+ try {
353
+ console.log(chalk.blue('🔔 Attempting to activate Claude Code process...'));
354
+
355
+ // Method 1: Find Claude Code process and try to send input
356
+ const { exec, spawn } = require('child_process');
357
+
358
+ // First, find Claude Code processes
359
+ exec('ps aux | grep "claude"', (error, stdout, stderr) => {
360
+ if (stdout) {
361
+ const claudeProcesses = stdout.split('\n')
362
+ .filter(line => line.includes('claude') && !line.includes('grep'))
363
+ .filter(line => !line.includes('claude-code-templates')); // Exclude our dashboard
364
+
365
+ console.log(chalk.blue(`🔍 Found ${claudeProcesses.length} Claude process(es)`));
366
+
367
+ claudeProcesses.forEach(processLine => {
368
+ const pid = processLine.trim().split(/\s+/)[1];
369
+ console.log(chalk.gray(` - PID ${pid}: ${processLine.substring(0, 100)}...`));
370
+ });
371
+ }
372
+ });
373
+
374
+ // Method 2: Try to write to the Claude Code terminal using applescript (macOS)
375
+ if (process.platform === 'darwin') {
376
+ this.tryAppleScriptNotification();
377
+ }
378
+
379
+ // Method 3: Try sending wake-up signal
380
+ try {
381
+ exec('pkill -SIGUSR1 claude', () => {});
382
+ } catch (e) {/* ignore */}
383
+
384
+ } catch (error) {
385
+ console.log(chalk.gray('🔕 Could not notify Claude Code process'));
386
+ }
387
+ };
388
+
389
+ // Try to use AppleScript to send input to Claude Code terminal
390
+ ClaudeAPIProxy.prototype.tryAppleScriptNotification = function() {
391
+ try {
392
+ const { exec } = require('child_process');
393
+
394
+ // This AppleScript tries to find Terminal/iTerm with Claude Code and send a key
395
+ const appleScript = `
396
+ tell application "System Events"
397
+ set claudeFound to false
398
+ try
399
+ -- Try Terminal first
400
+ tell application "Terminal"
401
+ repeat with w in windows
402
+ repeat with t in tabs of w
403
+ if (custom title of t contains "claude" or name of t contains "claude") then
404
+ set claudeFound to true
405
+ set frontmost to true
406
+ do script "" in t -- Send empty command to wake up
407
+ exit repeat
408
+ end if
409
+ end repeat
410
+ if claudeFound then exit repeat
411
+ end repeat
412
+ end tell
413
+ end try
414
+
415
+ if not claudeFound then
416
+ try
417
+ -- Try iTerm2
418
+ tell application "iTerm"
419
+ repeat with w in windows
420
+ repeat with t in tabs of w
421
+ tell current session of t
422
+ if (name contains "claude") then
423
+ set claudeFound to true
424
+ select
425
+ write text ""
426
+ exit repeat
427
+ end if
428
+ end tell
429
+ end repeat
430
+ if claudeFound then exit repeat
431
+ end repeat
432
+ end tell
433
+ end try
434
+ end if
435
+ end tell
436
+ `;
437
+
438
+ exec(`osascript -e '${appleScript.replace(/'/g, "\\'")}'`, (error) => {
439
+ if (error) {
440
+ console.log(chalk.gray('🔕 AppleScript notification failed'));
441
+ } else {
442
+ console.log(chalk.green('✅ AppleScript notification sent'));
443
+ }
444
+ });
445
+
446
+ } catch (error) {
447
+ // Silent fail
448
+ }
449
+ };
450
+
451
+ // If run directly
452
+ if (require.main === module) {
453
+ const proxy = new ClaudeAPIProxy();
454
+ proxy.start();
455
+
456
+ process.on('SIGINT', () => {
457
+ proxy.stop();
458
+ process.exit(0);
459
+ });
460
+ }
package/src/index.js CHANGED
@@ -13,6 +13,7 @@ const { runCommandStats } = require('./command-stats');
13
13
  const { runHookStats } = require('./hook-stats');
14
14
  const { runMCPStats } = require('./mcp-stats');
15
15
  const { runAnalytics } = require('./analytics');
16
+ const { startChatsMobile } = require('./chats-mobile');
16
17
  const { runHealthCheck } = require('./health-check');
17
18
  const { trackingService } = require('./tracking-service');
18
19
 
@@ -30,9 +31,14 @@ async function showMainMenu() {
30
31
  short: 'Analytics Dashboard'
31
32
  },
32
33
  {
33
- name: '💬 Chats Dashboard - View and analyze your Claude conversations',
34
+ name: '💬 Chats Mobile - AI-first mobile interface for conversations',
34
35
  value: 'chats',
35
- short: 'Chats Dashboard'
36
+ short: 'Chats Mobile'
37
+ },
38
+ {
39
+ name: '🤖 Agents Dashboard - View and analyze Claude conversations with agent tools',
40
+ value: 'agents',
41
+ short: 'Agents Dashboard'
36
42
  },
37
43
  {
38
44
  name: '⚙️ Project Setup - Configure Claude Code for your project',
@@ -56,12 +62,20 @@ async function showMainMenu() {
56
62
  }
57
63
 
58
64
  if (initialChoice.action === 'chats') {
59
- console.log(chalk.blue('💬 Launching Claude Code Chats Dashboard...'));
65
+ console.log(chalk.blue('💬 Launching Claude Code Mobile Chats...'));
66
+ trackingService.trackAnalyticsDashboard({ page: 'chats-mobile', source: 'interactive_menu' });
67
+ await startChatsMobile({});
68
+ return;
69
+ }
70
+
71
+ if (initialChoice.action === 'agents') {
72
+ console.log(chalk.blue('🤖 Launching Claude Code Agents Dashboard...'));
60
73
  trackingService.trackAnalyticsDashboard({ page: 'agents', source: 'interactive_menu' });
61
74
  await runAnalytics({ openTo: 'agents' });
62
75
  return;
63
76
  }
64
77
 
78
+
65
79
  if (initialChoice.action === 'health') {
66
80
  console.log(chalk.blue('🔍 Running Health Check...'));
67
81
  const healthResult = await runHealthCheck();
@@ -90,6 +104,16 @@ async function showMainMenu() {
90
104
  async function createClaudeConfig(options = {}) {
91
105
  const targetDir = options.directory || process.cwd();
92
106
 
107
+ // Validate --tunnel usage
108
+ if (options.tunnel && !options.analytics && !options.chats && !options.agents && !options.chatsMobile) {
109
+ console.log(chalk.red('❌ Error: --tunnel can only be used with --analytics, --chats, or --chats-mobile'));
110
+ console.log(chalk.yellow('💡 Examples:'));
111
+ console.log(chalk.gray(' cct --analytics --tunnel'));
112
+ console.log(chalk.gray(' cct --chats --tunnel'));
113
+ console.log(chalk.gray(' cct --chats-mobile'));
114
+ return;
115
+ }
116
+
93
117
  // Handle multiple components installation (new approach)
94
118
  if (options.agent || options.command || options.mcp) {
95
119
  // If --workflow is used with components, treat it as YAML
@@ -131,13 +155,27 @@ async function createClaudeConfig(options = {}) {
131
155
  return;
132
156
  }
133
157
 
134
- // Handle chats/agents dashboard
135
- if (options.chats || options.agents) {
158
+ // Handle chats dashboard (now points to mobile chats interface)
159
+ if (options.chats) {
160
+ trackingService.trackAnalyticsDashboard({ page: 'chats-mobile', source: 'command_line' });
161
+ await startChatsMobile(options);
162
+ return;
163
+ }
164
+
165
+ // Handle agents dashboard (separate from chats)
166
+ if (options.agents) {
136
167
  trackingService.trackAnalyticsDashboard({ page: 'agents', source: 'command_line' });
137
168
  await runAnalytics({ ...options, openTo: 'agents' });
138
169
  return;
139
170
  }
140
171
 
172
+ // Handle mobile chats interface
173
+ if (options.chatsMobile) {
174
+ trackingService.trackAnalyticsDashboard({ page: 'chats-mobile', source: 'command_line' });
175
+ await startChatsMobile(options);
176
+ return;
177
+ }
178
+
141
179
  // Handle health check
142
180
  let shouldRunSetup = false;
143
181
  if (options.healthCheck || options.health || options.check || options.verify) {