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.
- package/bin/create-claude-config.js +5 -1
- package/package.json +2 -1
- 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/index.js +43 -5
- package/src/analytics-web/components/AgentsPage.js +0 -4761
package/src/analytics.js
CHANGED
|
@@ -4,6 +4,9 @@ const path = require('path');
|
|
|
4
4
|
const express = require('express');
|
|
5
5
|
const open = require('open');
|
|
6
6
|
const os = require('os');
|
|
7
|
+
const inquirer = require('inquirer');
|
|
8
|
+
const boxen = require('boxen');
|
|
9
|
+
const { spawn } = require('child_process');
|
|
7
10
|
const packageJson = require('../package.json');
|
|
8
11
|
const StateCalculator = require('./analytics/core/StateCalculator');
|
|
9
12
|
const ProcessDetector = require('./analytics/core/ProcessDetector');
|
|
@@ -16,9 +19,12 @@ const WebSocketServer = require('./analytics/notifications/WebSocketServer');
|
|
|
16
19
|
const NotificationManager = require('./analytics/notifications/NotificationManager');
|
|
17
20
|
const PerformanceMonitor = require('./analytics/utils/PerformanceMonitor');
|
|
18
21
|
const ConsoleBridge = require('./console-bridge');
|
|
22
|
+
const ClaudeAPIProxy = require('./claude-api-proxy');
|
|
19
23
|
|
|
20
24
|
class ClaudeAnalytics {
|
|
21
|
-
constructor() {
|
|
25
|
+
constructor(options = {}) {
|
|
26
|
+
this.options = options;
|
|
27
|
+
this.verbose = options.verbose || false;
|
|
22
28
|
this.app = express();
|
|
23
29
|
this.port = 3333;
|
|
24
30
|
this.stateCalculator = new StateCalculator();
|
|
@@ -36,6 +42,9 @@ class ClaudeAnalytics {
|
|
|
36
42
|
this.notificationManager = null;
|
|
37
43
|
this.httpServer = null;
|
|
38
44
|
this.consoleBridge = null;
|
|
45
|
+
this.cloudflareProcess = null;
|
|
46
|
+
this.publicUrl = null;
|
|
47
|
+
this.claudeApiProxy = null;
|
|
39
48
|
this.data = {
|
|
40
49
|
conversations: [],
|
|
41
50
|
summary: {},
|
|
@@ -49,6 +58,29 @@ class ClaudeAnalytics {
|
|
|
49
58
|
};
|
|
50
59
|
}
|
|
51
60
|
|
|
61
|
+
/**
|
|
62
|
+
* Log messages only if verbose mode is enabled
|
|
63
|
+
* @param {string} level - Log level ('info', 'warn', 'error')
|
|
64
|
+
* @param {string} message - Message to log
|
|
65
|
+
* @param {...any} args - Additional arguments
|
|
66
|
+
*/
|
|
67
|
+
log(level, message, ...args) {
|
|
68
|
+
if (!this.verbose) return;
|
|
69
|
+
|
|
70
|
+
switch (level) {
|
|
71
|
+
case 'error':
|
|
72
|
+
console.error(message, ...args);
|
|
73
|
+
break;
|
|
74
|
+
case 'warn':
|
|
75
|
+
console.warn(message, ...args);
|
|
76
|
+
break;
|
|
77
|
+
case 'info':
|
|
78
|
+
default:
|
|
79
|
+
console.log(message, ...args);
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
52
84
|
async initialize() {
|
|
53
85
|
const homeDir = os.homedir();
|
|
54
86
|
this.claudeDir = path.join(homeDir, '.claude');
|
|
@@ -1181,7 +1213,7 @@ class ClaudeAnalytics {
|
|
|
1181
1213
|
}
|
|
1182
1214
|
|
|
1183
1215
|
async openBrowser(openTo = null) {
|
|
1184
|
-
const baseUrl = `http://localhost:${this.port}`;
|
|
1216
|
+
const baseUrl = this.publicUrl || `http://localhost:${this.port}`;
|
|
1185
1217
|
let fullUrl = baseUrl;
|
|
1186
1218
|
|
|
1187
1219
|
// Add fragment/hash for specific page
|
|
@@ -1200,6 +1232,191 @@ class ClaudeAnalytics {
|
|
|
1200
1232
|
}
|
|
1201
1233
|
}
|
|
1202
1234
|
|
|
1235
|
+
/**
|
|
1236
|
+
* Prompt user if they want to use Cloudflare Tunnel
|
|
1237
|
+
*/
|
|
1238
|
+
async promptCloudflareSetup() {
|
|
1239
|
+
console.log('');
|
|
1240
|
+
console.log(chalk.yellow('🌐 Analytics Dashboard Access Options'));
|
|
1241
|
+
console.log('');
|
|
1242
|
+
console.log(chalk.cyan('🔒 About Cloudflare Tunnel:'));
|
|
1243
|
+
console.log(chalk.gray('• Creates a secure connection between your localhost and the web'));
|
|
1244
|
+
console.log(chalk.gray('• Only you will have access to the generated URL (not public)'));
|
|
1245
|
+
console.log(chalk.gray('• The connection is end-to-end encrypted'));
|
|
1246
|
+
console.log(chalk.gray('• Automatically closes when you end the session'));
|
|
1247
|
+
console.log(chalk.gray('• No firewall or port configuration required'));
|
|
1248
|
+
console.log('');
|
|
1249
|
+
console.log(chalk.green('✅ It is completely secure - only you can access the dashboard'));
|
|
1250
|
+
console.log('');
|
|
1251
|
+
|
|
1252
|
+
const { useCloudflare } = await inquirer.prompt([{
|
|
1253
|
+
type: 'confirm',
|
|
1254
|
+
name: 'useCloudflare',
|
|
1255
|
+
message: 'Enable Cloudflare Tunnel for secure remote access?',
|
|
1256
|
+
default: true
|
|
1257
|
+
}]);
|
|
1258
|
+
|
|
1259
|
+
return useCloudflare;
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
/**
|
|
1263
|
+
* Start Cloudflare Tunnel
|
|
1264
|
+
*/
|
|
1265
|
+
async startCloudflareTunnel() {
|
|
1266
|
+
try {
|
|
1267
|
+
console.log(chalk.blue('🔧 Starting Cloudflare Tunnel...'));
|
|
1268
|
+
|
|
1269
|
+
// Check if cloudflared is installed
|
|
1270
|
+
const checkProcess = spawn('cloudflared', ['version'], { stdio: 'pipe' });
|
|
1271
|
+
|
|
1272
|
+
return new Promise((resolve, reject) => {
|
|
1273
|
+
checkProcess.on('error', (error) => {
|
|
1274
|
+
console.log(chalk.red('❌ Cloudflared is not installed.'));
|
|
1275
|
+
console.log('');
|
|
1276
|
+
console.log(chalk.yellow('📥 To install Cloudflare Tunnel:'));
|
|
1277
|
+
console.log(chalk.gray('• macOS: brew install cloudflared'));
|
|
1278
|
+
console.log(chalk.gray('• Windows: winget install --id Cloudflare.cloudflared'));
|
|
1279
|
+
console.log(chalk.gray('• Linux: apt-get install cloudflared'));
|
|
1280
|
+
console.log('');
|
|
1281
|
+
console.log(chalk.blue('💡 More info: https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/'));
|
|
1282
|
+
resolve(false);
|
|
1283
|
+
});
|
|
1284
|
+
|
|
1285
|
+
checkProcess.on('close', (code) => {
|
|
1286
|
+
if (code === 0) {
|
|
1287
|
+
this.createCloudflareTunnel();
|
|
1288
|
+
resolve(true);
|
|
1289
|
+
} else {
|
|
1290
|
+
resolve(false);
|
|
1291
|
+
}
|
|
1292
|
+
});
|
|
1293
|
+
});
|
|
1294
|
+
} catch (error) {
|
|
1295
|
+
console.log(chalk.red(`❌ Error checking Cloudflare Tunnel: ${error.message}`));
|
|
1296
|
+
return false;
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
/**
|
|
1301
|
+
* Create the actual Cloudflare Tunnel
|
|
1302
|
+
*/
|
|
1303
|
+
async createCloudflareTunnel() {
|
|
1304
|
+
try {
|
|
1305
|
+
console.log(chalk.blue('🚀 Creating secure tunnel...'));
|
|
1306
|
+
|
|
1307
|
+
// Start cloudflared tunnel normally, but filter the output to capture URL
|
|
1308
|
+
this.cloudflareProcess = spawn('cloudflared', [
|
|
1309
|
+
'tunnel',
|
|
1310
|
+
'--url', `http://localhost:${this.port}`
|
|
1311
|
+
], {
|
|
1312
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
1313
|
+
});
|
|
1314
|
+
|
|
1315
|
+
let tunnelEstablished = false;
|
|
1316
|
+
|
|
1317
|
+
return new Promise((resolve) => {
|
|
1318
|
+
// Monitor stderr for the tunnel URL (cloudflared outputs most info to stderr)
|
|
1319
|
+
this.cloudflareProcess.stderr.on('data', (data) => {
|
|
1320
|
+
const output = data.toString();
|
|
1321
|
+
|
|
1322
|
+
// Use the cleaner regex to extract URL from the logs
|
|
1323
|
+
const urlMatch = output.match(/https:\/\/[a-zA-Z0-9.-]+\.trycloudflare\.com/);
|
|
1324
|
+
|
|
1325
|
+
if (urlMatch && !tunnelEstablished) {
|
|
1326
|
+
tunnelEstablished = true;
|
|
1327
|
+
this.publicUrl = urlMatch[0];
|
|
1328
|
+
|
|
1329
|
+
// Create a prominent, boxed display for the tunnel URL
|
|
1330
|
+
const tunnelMessage = chalk.green.bold('🌍 CLOUDFLARE TUNNEL ACTIVE') + '\n\n' +
|
|
1331
|
+
chalk.cyan.bold('Public URL: ') + chalk.white.underline(this.publicUrl) + '\n\n' +
|
|
1332
|
+
chalk.yellow('🔗 Share this URL to access your dashboard remotely') + '\n' +
|
|
1333
|
+
chalk.gray('🔒 This tunnel is private and secure - only accessible by you');
|
|
1334
|
+
|
|
1335
|
+
console.log('\n');
|
|
1336
|
+
console.log(boxen(tunnelMessage, {
|
|
1337
|
+
padding: 1,
|
|
1338
|
+
margin: 1,
|
|
1339
|
+
borderStyle: 'double',
|
|
1340
|
+
borderColor: 'cyan',
|
|
1341
|
+
backgroundColor: '#1a1a1a'
|
|
1342
|
+
}));
|
|
1343
|
+
console.log('\n');
|
|
1344
|
+
resolve(true);
|
|
1345
|
+
}
|
|
1346
|
+
});
|
|
1347
|
+
|
|
1348
|
+
// Also check stdout just in case
|
|
1349
|
+
this.cloudflareProcess.stdout.on('data', (data) => {
|
|
1350
|
+
const output = data.toString();
|
|
1351
|
+
const urlMatch = output.match(/https:\/\/[a-zA-Z0-9.-]+\.trycloudflare\.com/);
|
|
1352
|
+
|
|
1353
|
+
if (urlMatch && !tunnelEstablished) {
|
|
1354
|
+
tunnelEstablished = true;
|
|
1355
|
+
this.publicUrl = urlMatch[0];
|
|
1356
|
+
|
|
1357
|
+
// Create a prominent, boxed display for the tunnel URL
|
|
1358
|
+
const tunnelMessage = chalk.green.bold('🌍 CLOUDFLARE TUNNEL ACTIVE') + '\n\n' +
|
|
1359
|
+
chalk.cyan.bold('Public URL: ') + chalk.white.underline(this.publicUrl) + '\n\n' +
|
|
1360
|
+
chalk.yellow('🔗 Share this URL to access your dashboard remotely') + '\n' +
|
|
1361
|
+
chalk.gray('🔒 This tunnel is private and secure - only accessible by you');
|
|
1362
|
+
|
|
1363
|
+
console.log('\n');
|
|
1364
|
+
console.log(boxen(tunnelMessage, {
|
|
1365
|
+
padding: 1,
|
|
1366
|
+
margin: 1,
|
|
1367
|
+
borderStyle: 'double',
|
|
1368
|
+
borderColor: 'cyan',
|
|
1369
|
+
backgroundColor: '#1a1a1a'
|
|
1370
|
+
}));
|
|
1371
|
+
console.log('\n');
|
|
1372
|
+
resolve(true);
|
|
1373
|
+
}
|
|
1374
|
+
});
|
|
1375
|
+
|
|
1376
|
+
this.cloudflareProcess.on('close', (code) => {
|
|
1377
|
+
if (code !== 0) {
|
|
1378
|
+
console.log(chalk.red(`❌ Cloudflare Tunnel terminated with code: ${code}`));
|
|
1379
|
+
}
|
|
1380
|
+
this.publicUrl = null;
|
|
1381
|
+
this.cloudflareProcess = null;
|
|
1382
|
+
if (!tunnelEstablished) {
|
|
1383
|
+
resolve(false);
|
|
1384
|
+
}
|
|
1385
|
+
});
|
|
1386
|
+
|
|
1387
|
+
this.cloudflareProcess.on('error', (error) => {
|
|
1388
|
+
console.log(chalk.red(`❌ Error with Cloudflare Tunnel: ${error.message}`));
|
|
1389
|
+
this.publicUrl = null;
|
|
1390
|
+
this.cloudflareProcess = null;
|
|
1391
|
+
resolve(false);
|
|
1392
|
+
});
|
|
1393
|
+
|
|
1394
|
+
// Timeout after 15 seconds if tunnel doesn't establish
|
|
1395
|
+
setTimeout(() => {
|
|
1396
|
+
if (!tunnelEstablished) {
|
|
1397
|
+
console.log(chalk.red('❌ Timeout waiting for Cloudflare Tunnel to establish'));
|
|
1398
|
+
resolve(false);
|
|
1399
|
+
}
|
|
1400
|
+
}, 15000);
|
|
1401
|
+
});
|
|
1402
|
+
} catch (error) {
|
|
1403
|
+
console.log(chalk.red(`❌ Error creating Cloudflare Tunnel: ${error.message}`));
|
|
1404
|
+
return false;
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
/**
|
|
1409
|
+
* Stop Cloudflare Tunnel
|
|
1410
|
+
*/
|
|
1411
|
+
stopCloudflareTunnel() {
|
|
1412
|
+
if (this.cloudflareProcess) {
|
|
1413
|
+
console.log(chalk.yellow('🛑 Closing Cloudflare Tunnel...'));
|
|
1414
|
+
this.cloudflareProcess.kill('SIGTERM');
|
|
1415
|
+
this.cloudflareProcess = null;
|
|
1416
|
+
this.publicUrl = null;
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1203
1420
|
/**
|
|
1204
1421
|
* Initialize WebSocket server and notification manager
|
|
1205
1422
|
*/
|
|
@@ -1219,6 +1436,12 @@ class ClaudeAnalytics {
|
|
|
1219
1436
|
// Connect notification manager to file watcher for typing detection
|
|
1220
1437
|
this.fileWatcher.setNotificationManager(this.notificationManager);
|
|
1221
1438
|
|
|
1439
|
+
// Initialize Claude API Proxy for bidirectional communication
|
|
1440
|
+
console.log(chalk.blue('🌉 Initializing Claude API Proxy...'));
|
|
1441
|
+
this.claudeApiProxy = new ClaudeAPIProxy();
|
|
1442
|
+
await this.claudeApiProxy.start();
|
|
1443
|
+
console.log(chalk.green('✅ Claude API Proxy initialized on port 3335'));
|
|
1444
|
+
|
|
1222
1445
|
// Setup notification subscriptions
|
|
1223
1446
|
this.setupNotificationSubscriptions();
|
|
1224
1447
|
|
|
@@ -1815,6 +2038,9 @@ class ClaudeAnalytics {
|
|
|
1815
2038
|
// Stop file watchers
|
|
1816
2039
|
this.fileWatcher.stop();
|
|
1817
2040
|
|
|
2041
|
+
// Stop Cloudflare Tunnel
|
|
2042
|
+
this.stopCloudflareTunnel();
|
|
2043
|
+
|
|
1818
2044
|
// Stop server
|
|
1819
2045
|
// Close WebSocket server
|
|
1820
2046
|
if (this.webSocketServer) {
|
|
@@ -1831,6 +2057,11 @@ class ClaudeAnalytics {
|
|
|
1831
2057
|
this.consoleBridge.shutdown();
|
|
1832
2058
|
}
|
|
1833
2059
|
|
|
2060
|
+
// Stop Claude API Proxy
|
|
2061
|
+
if (this.claudeApiProxy) {
|
|
2062
|
+
this.claudeApiProxy.stop();
|
|
2063
|
+
}
|
|
2064
|
+
|
|
1834
2065
|
if (this.httpServer) {
|
|
1835
2066
|
this.httpServer.close();
|
|
1836
2067
|
}
|
|
@@ -1857,24 +2088,51 @@ async function runAnalytics(options = {}) {
|
|
|
1857
2088
|
console.log(chalk.blue('📊 Starting Claude Code Analytics Dashboard...'));
|
|
1858
2089
|
}
|
|
1859
2090
|
|
|
1860
|
-
const analytics = new ClaudeAnalytics();
|
|
2091
|
+
const analytics = new ClaudeAnalytics(options);
|
|
1861
2092
|
|
|
1862
2093
|
try {
|
|
2094
|
+
// Handle Cloudflare Tunnel prompt BEFORE initializing anything
|
|
2095
|
+
let useCloudflare = false;
|
|
2096
|
+
|
|
2097
|
+
if (options.tunnel) {
|
|
2098
|
+
useCloudflare = await analytics.promptCloudflareSetup();
|
|
2099
|
+
}
|
|
2100
|
+
|
|
1863
2101
|
await analytics.initialize();
|
|
1864
2102
|
|
|
1865
2103
|
// Create web dashboard files
|
|
1866
2104
|
// Web dashboard files are now static in analytics-web directory
|
|
1867
2105
|
|
|
1868
2106
|
await analytics.startServer();
|
|
2107
|
+
|
|
2108
|
+
// Start Cloudflare Tunnel BEFORE other services if requested and confirmed
|
|
2109
|
+
if (useCloudflare) {
|
|
2110
|
+
const cloudflareStarted = await analytics.startCloudflareTunnel();
|
|
2111
|
+
if (!cloudflareStarted) {
|
|
2112
|
+
console.log(chalk.yellow('⚠️ Continuing with localhost only...'));
|
|
2113
|
+
}
|
|
2114
|
+
// Wait a bit longer for tunnel to stabilize
|
|
2115
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
2116
|
+
}
|
|
2117
|
+
|
|
1869
2118
|
await analytics.openBrowser(openTo);
|
|
1870
2119
|
|
|
2120
|
+
const accessUrl = analytics.publicUrl || `http://localhost:${analytics.port}`;
|
|
2121
|
+
|
|
1871
2122
|
if (openTo === 'agents') {
|
|
1872
2123
|
console.log(chalk.green('✅ Claude Code Chats dashboard is running!'));
|
|
1873
|
-
console.log(chalk.cyan(`📱 Access at:
|
|
2124
|
+
console.log(chalk.cyan(`📱 Access at: ${accessUrl}/#agents`));
|
|
1874
2125
|
} else {
|
|
1875
2126
|
console.log(chalk.green('✅ Analytics dashboard is running!'));
|
|
1876
|
-
console.log(chalk.cyan(`📱 Access at:
|
|
2127
|
+
console.log(chalk.cyan(`📱 Access at: ${accessUrl}`));
|
|
1877
2128
|
}
|
|
2129
|
+
|
|
2130
|
+
if (analytics.publicUrl) {
|
|
2131
|
+
console.log(chalk.gray('🔒 Secure access via Cloudflare Tunnel'));
|
|
2132
|
+
} else {
|
|
2133
|
+
console.log(chalk.gray('🏠 Local access only'));
|
|
2134
|
+
}
|
|
2135
|
+
|
|
1878
2136
|
console.log(chalk.gray('Press Ctrl+C to stop the server'));
|
|
1879
2137
|
|
|
1880
2138
|
// Handle graceful shutdown
|