claude-code-templates 1.8.0 → 1.8.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,550 @@
1
+ const fs = require('fs-extra');
2
+ const chalk = require('chalk');
3
+
4
+ /**
5
+ * DataCache - Multi-level caching system for analytics performance optimization
6
+ * Provides file content, parsed data, and computation result caching with smart invalidation
7
+ */
8
+ class DataCache {
9
+ constructor() {
10
+ // Multi-level caching strategy
11
+ this.caches = {
12
+ // File content cache (highest impact)
13
+ fileContent: new Map(), // filepath -> { content, timestamp, stats }
14
+
15
+ // Parsed data cache
16
+ parsedConversations: new Map(), // filepath -> { messages, timestamp }
17
+
18
+ // Computed results cache
19
+ tokenUsage: new Map(), // filepath -> { usage, timestamp }
20
+ modelInfo: new Map(), // filepath -> { info, timestamp }
21
+ statusSquares: new Map(), // filepath -> { squares, timestamp }
22
+
23
+ // Expensive computations cache
24
+ sessions: { data: null, timestamp: 0, dependencies: new Set() },
25
+ summary: { data: null, timestamp: 0, dependencies: new Set() },
26
+
27
+ // Process cache enhancement
28
+ processes: { data: null, timestamp: 0, ttl: 500 },
29
+
30
+ // Metadata cache
31
+ fileStats: new Map(), // filepath -> { stats, timestamp }
32
+ projectStats: new Map(), // projectPath -> { data, timestamp }
33
+ };
34
+
35
+ // Cache configuration
36
+ this.config = {
37
+ fileContentTTL: 60000, // 1 minute for file content
38
+ parsedDataTTL: 30000, // 30 seconds for parsed data
39
+ computationTTL: 20000, // 20 seconds for expensive computations
40
+ metadataTTL: 10000, // 10 seconds for metadata
41
+ processTTL: 500, // 500ms for process data
42
+ maxCacheSize: 100, // Reduced from 1000 to 100 to prevent memory buildup
43
+ };
44
+
45
+ // Dependency tracking for smart invalidation
46
+ this.dependencies = new Map(); // computation -> Set(filepaths)
47
+
48
+ // Performance metrics
49
+ this.metrics = {
50
+ hits: 0,
51
+ misses: 0,
52
+ invalidations: 0,
53
+ filesInvalidated: 0,
54
+ computationsInvalidated: 0,
55
+ evictions: 0
56
+ };
57
+
58
+ // Start automatic cleanup interval
59
+ this.cleanupInterval = setInterval(() => {
60
+ this.evictOldEntries();
61
+ this.enforceSizeLimits();
62
+ }, 30000); // Every 30 seconds
63
+ }
64
+
65
+ /**
66
+ * Get file content with caching and modification time checking
67
+ * @param {string} filepath - Path to file
68
+ * @returns {Promise<string>} File content
69
+ */
70
+ async getFileContent(filepath) {
71
+ const cached = this.caches.fileContent.get(filepath);
72
+
73
+ try {
74
+ const stats = await fs.stat(filepath);
75
+
76
+ // Check if cached content is still valid
77
+ if (cached && cached.timestamp >= stats.mtime.getTime()) {
78
+ this.metrics.hits++;
79
+ return cached.content;
80
+ }
81
+
82
+ // Cache miss - read file
83
+ this.metrics.misses++;
84
+ const content = await fs.readFile(filepath, 'utf8');
85
+
86
+ this.caches.fileContent.set(filepath, {
87
+ content,
88
+ timestamp: stats.mtime.getTime(),
89
+ stats: stats
90
+ });
91
+
92
+ // Also cache the file stats
93
+ this.caches.fileStats.set(filepath, {
94
+ stats: stats,
95
+ timestamp: Date.now()
96
+ });
97
+
98
+ return content;
99
+ } catch (error) {
100
+ // File doesn't exist or can't be read
101
+ this.invalidateFile(filepath);
102
+ throw error;
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Get parsed conversation with caching
108
+ * @param {string} filepath - Path to conversation file
109
+ * @returns {Promise<Array>} Parsed conversation messages
110
+ */
111
+ async getParsedConversation(filepath) {
112
+ const cached = this.caches.parsedConversations.get(filepath);
113
+ const fileStats = await this.getFileStats(filepath);
114
+
115
+ // Check if cached parsed data is still valid
116
+ if (cached && cached.timestamp >= fileStats.mtime.getTime()) {
117
+ this.metrics.hits++;
118
+ return cached.messages;
119
+ }
120
+
121
+ // Cache miss - parse conversation
122
+ this.metrics.misses++;
123
+ const content = await this.getFileContent(filepath);
124
+
125
+ const messages = content.trim().split('\n')
126
+ .filter(line => line.trim())
127
+ .map(line => {
128
+ try {
129
+ const item = JSON.parse(line);
130
+ if (item.message && item.message.role) {
131
+ return {
132
+ role: item.message.role,
133
+ timestamp: new Date(item.timestamp),
134
+ content: item.message.content,
135
+ model: item.message.model || null,
136
+ usage: item.message.usage || null,
137
+ };
138
+ }
139
+ } catch {}
140
+ return null;
141
+ })
142
+ .filter(Boolean);
143
+
144
+ this.caches.parsedConversations.set(filepath, {
145
+ messages,
146
+ timestamp: fileStats.mtime.getTime()
147
+ });
148
+
149
+ return messages;
150
+ }
151
+
152
+ /**
153
+ * Get file stats with caching
154
+ * @param {string} filepath - Path to file
155
+ * @returns {Promise<Object>} File stats
156
+ */
157
+ async getFileStats(filepath) {
158
+ const cached = this.caches.fileStats.get(filepath);
159
+
160
+ // Check if metadata cache is still valid
161
+ if (cached && (Date.now() - cached.timestamp) < this.config.metadataTTL) {
162
+ this.metrics.hits++;
163
+ return cached.stats;
164
+ }
165
+
166
+ // Cache miss - get fresh stats
167
+ this.metrics.misses++;
168
+ const stats = await fs.stat(filepath);
169
+
170
+ this.caches.fileStats.set(filepath, {
171
+ stats: stats,
172
+ timestamp: Date.now()
173
+ });
174
+
175
+ return stats;
176
+ }
177
+
178
+ /**
179
+ * Cache expensive computation results with dependency tracking
180
+ * @param {string} key - Cache key
181
+ * @param {Function} computeFn - Function to compute result
182
+ * @param {Array<string>} dependencies - File dependencies
183
+ * @param {number} ttl - Time to live override
184
+ * @returns {Promise<any>} Computation result
185
+ */
186
+ async getCachedComputation(key, computeFn, dependencies = [], ttl = null) {
187
+ const cached = this.caches[key];
188
+ const effectiveTTL = ttl || this.config.computationTTL;
189
+
190
+ // Check if any dependencies have changed
191
+ let dependenciesChanged = false;
192
+ if (dependencies.length > 0) {
193
+ for (const dep of dependencies) {
194
+ try {
195
+ const depStats = await this.getFileStats(dep);
196
+ const cachedDep = cached?.dependencies?.has(dep);
197
+ const depTimestamp = cached?.dependencyTimestamps?.get(dep);
198
+
199
+ if (!cachedDep || !depTimestamp || depStats.mtime.getTime() > depTimestamp) {
200
+ dependenciesChanged = true;
201
+ break;
202
+ }
203
+ } catch {
204
+ // Dependency file doesn't exist anymore
205
+ dependenciesChanged = true;
206
+ break;
207
+ }
208
+ }
209
+ }
210
+
211
+ // Check cache validity
212
+ const isCacheValid = cached?.data &&
213
+ !dependenciesChanged &&
214
+ (Date.now() - cached.timestamp) < effectiveTTL;
215
+
216
+ if (isCacheValid) {
217
+ this.metrics.hits++;
218
+ return cached.data;
219
+ }
220
+
221
+ // Cache miss - compute result
222
+ this.metrics.misses++;
223
+ const result = await computeFn();
224
+
225
+ // Store dependency timestamps
226
+ const dependencyTimestamps = new Map();
227
+ for (const dep of dependencies) {
228
+ try {
229
+ const stats = await this.getFileStats(dep);
230
+ dependencyTimestamps.set(dep, stats.mtime.getTime());
231
+ } catch {}
232
+ }
233
+
234
+ this.caches[key] = {
235
+ data: result,
236
+ timestamp: Date.now(),
237
+ dependencies: new Set(dependencies),
238
+ dependencyTimestamps: dependencyTimestamps
239
+ };
240
+
241
+ return result;
242
+ }
243
+
244
+ /**
245
+ * Cache token usage calculation
246
+ * @param {string} filepath - File path
247
+ * @param {Function} calculateFn - Token calculation function
248
+ * @returns {Promise<Object>} Token usage data
249
+ */
250
+ async getCachedTokenUsage(filepath, calculateFn) {
251
+ const cached = this.caches.tokenUsage.get(filepath);
252
+ const fileStats = await this.getFileStats(filepath);
253
+
254
+ if (cached && cached.timestamp >= fileStats.mtime.getTime()) {
255
+ this.metrics.hits++;
256
+ return cached.usage;
257
+ }
258
+
259
+ this.metrics.misses++;
260
+ const usage = await calculateFn();
261
+
262
+ this.caches.tokenUsage.set(filepath, {
263
+ usage,
264
+ timestamp: fileStats.mtime.getTime()
265
+ });
266
+
267
+ return usage;
268
+ }
269
+
270
+ /**
271
+ * Cache model info extraction
272
+ * @param {string} filepath - File path
273
+ * @param {Function} extractFn - Model extraction function
274
+ * @returns {Promise<Object>} Model info data
275
+ */
276
+ async getCachedModelInfo(filepath, extractFn) {
277
+ const cached = this.caches.modelInfo.get(filepath);
278
+ const fileStats = await this.getFileStats(filepath);
279
+
280
+ if (cached && cached.timestamp >= fileStats.mtime.getTime()) {
281
+ this.metrics.hits++;
282
+ return cached.info;
283
+ }
284
+
285
+ this.metrics.misses++;
286
+ const info = await extractFn();
287
+
288
+ this.caches.modelInfo.set(filepath, {
289
+ info,
290
+ timestamp: fileStats.mtime.getTime()
291
+ });
292
+
293
+ return info;
294
+ }
295
+
296
+ /**
297
+ * Cache status squares generation
298
+ * @param {string} filepath - File path
299
+ * @param {Function} generateFn - Status squares generation function
300
+ * @returns {Promise<Array>} Status squares data
301
+ */
302
+ async getCachedStatusSquares(filepath, generateFn) {
303
+ const cached = this.caches.statusSquares.get(filepath);
304
+ const fileStats = await this.getFileStats(filepath);
305
+
306
+ if (cached && cached.timestamp >= fileStats.mtime.getTime()) {
307
+ this.metrics.hits++;
308
+ return cached.squares;
309
+ }
310
+
311
+ this.metrics.misses++;
312
+ const squares = await generateFn();
313
+
314
+ this.caches.statusSquares.set(filepath, {
315
+ squares,
316
+ timestamp: fileStats.mtime.getTime()
317
+ });
318
+
319
+ return squares;
320
+ }
321
+
322
+ /**
323
+ * Smart invalidation based on file changes
324
+ * @param {string} filepath - Path of changed file
325
+ */
326
+ invalidateFile(filepath) {
327
+ this.metrics.filesInvalidated++;
328
+
329
+ // Remove direct file caches
330
+ this.caches.fileContent.delete(filepath);
331
+ this.caches.parsedConversations.delete(filepath);
332
+ this.caches.tokenUsage.delete(filepath);
333
+ this.caches.modelInfo.delete(filepath);
334
+ this.caches.statusSquares.delete(filepath);
335
+ this.caches.fileStats.delete(filepath);
336
+
337
+ // Invalidate computations that depend on this file
338
+ ['sessions', 'summary'].forEach(key => {
339
+ const cached = this.caches[key];
340
+ if (cached?.dependencies?.has(filepath)) {
341
+ this.caches[key] = {
342
+ data: null,
343
+ timestamp: 0,
344
+ dependencies: new Set(),
345
+ dependencyTimestamps: new Map()
346
+ };
347
+ this.metrics.computationsInvalidated++;
348
+ }
349
+ });
350
+
351
+ this.metrics.invalidations++;
352
+ }
353
+
354
+ /**
355
+ * Invalidate multiple files (for batch operations)
356
+ * @param {Array<string>} filepaths - Array of file paths
357
+ */
358
+ invalidateFiles(filepaths) {
359
+ filepaths.forEach(filepath => this.invalidateFile(filepath));
360
+ }
361
+
362
+ /**
363
+ * Invalidate all computations (force recalculation)
364
+ */
365
+ invalidateComputations() {
366
+ ['sessions', 'summary'].forEach(key => {
367
+ this.caches[key] = {
368
+ data: null,
369
+ timestamp: 0,
370
+ dependencies: new Set(),
371
+ dependencyTimestamps: new Map()
372
+ };
373
+ });
374
+ this.metrics.computationsInvalidated += 2;
375
+ }
376
+
377
+ /**
378
+ * Clear all caches
379
+ */
380
+ clearAll() {
381
+ Object.values(this.caches).forEach(cache => {
382
+ if (cache instanceof Map) {
383
+ cache.clear();
384
+ } else {
385
+ cache.data = null;
386
+ cache.timestamp = 0;
387
+ cache.dependencies = new Set();
388
+ cache.dependencyTimestamps = new Map();
389
+ }
390
+ });
391
+
392
+ // Reset metrics
393
+ this.metrics = {
394
+ hits: 0,
395
+ misses: 0,
396
+ invalidations: 0,
397
+ filesInvalidated: 0,
398
+ computationsInvalidated: 0
399
+ };
400
+ }
401
+
402
+ /**
403
+ * Evict old entries to manage memory
404
+ */
405
+ evictOldEntries() {
406
+ const now = Date.now();
407
+ let evicted = 0;
408
+
409
+ // Evict old file content
410
+ for (const [filepath, data] of this.caches.fileContent.entries()) {
411
+ if (now - data.timestamp > this.config.fileContentTTL) {
412
+ this.caches.fileContent.delete(filepath);
413
+ evicted++;
414
+ }
415
+ }
416
+
417
+ // Evict old parsed data
418
+ for (const [filepath, data] of this.caches.parsedConversations.entries()) {
419
+ if (now - data.timestamp > this.config.parsedDataTTL) {
420
+ this.caches.parsedConversations.delete(filepath);
421
+ evicted++;
422
+ }
423
+ }
424
+
425
+ // Evict old metadata
426
+ for (const [filepath, data] of this.caches.fileStats.entries()) {
427
+ if (now - data.timestamp > this.config.metadataTTL) {
428
+ this.caches.fileStats.delete(filepath);
429
+ evicted++;
430
+ }
431
+ }
432
+
433
+ if (evicted > 0) {
434
+ this.metrics.evictions += evicted;
435
+ console.log(chalk.gray(`🗑️ Evicted ${evicted} old cache entries`));
436
+ }
437
+ }
438
+
439
+ /**
440
+ * Enforce cache size limits to prevent memory buildup
441
+ */
442
+ enforceSizeLimits() {
443
+ const maxSize = this.config.maxCacheSize;
444
+ let totalEvicted = 0;
445
+
446
+ // Enforce size limits on each cache
447
+ const caches = [
448
+ ['fileContent', this.caches.fileContent],
449
+ ['parsedConversations', this.caches.parsedConversations],
450
+ ['tokenUsage', this.caches.tokenUsage],
451
+ ['modelInfo', this.caches.modelInfo],
452
+ ['statusSquares', this.caches.statusSquares],
453
+ ['fileStats', this.caches.fileStats],
454
+ ['projectStats', this.caches.projectStats]
455
+ ];
456
+
457
+ for (const [, cache] of caches) {
458
+ if (cache.size > maxSize) {
459
+ // Convert to array and sort by timestamp (oldest first)
460
+ const entries = Array.from(cache.entries()).sort((a, b) => {
461
+ const timestampA = a[1].timestamp || 0;
462
+ const timestampB = b[1].timestamp || 0;
463
+ return timestampA - timestampB;
464
+ });
465
+
466
+ // Remove oldest entries until we're under the limit
467
+ const toRemove = cache.size - maxSize;
468
+ for (let i = 0; i < toRemove && i < entries.length; i++) {
469
+ cache.delete(entries[i][0]);
470
+ totalEvicted++;
471
+ }
472
+ }
473
+ }
474
+
475
+ if (totalEvicted > 0) {
476
+ this.metrics.evictions += totalEvicted;
477
+ console.log(chalk.gray(`🗑️ Enforced size limits, evicted ${totalEvicted} entries`));
478
+ }
479
+ }
480
+
481
+ /**
482
+ * Clean up resources and stop timers
483
+ */
484
+ cleanup() {
485
+ if (this.cleanupInterval) {
486
+ clearInterval(this.cleanupInterval);
487
+ this.cleanupInterval = null;
488
+ }
489
+ this.clearAll();
490
+ }
491
+
492
+ /**
493
+ * Get cache statistics
494
+ * @returns {Object} Cache performance metrics
495
+ */
496
+ getStats() {
497
+ const hitRate = this.metrics.hits + this.metrics.misses > 0
498
+ ? (this.metrics.hits / (this.metrics.hits + this.metrics.misses) * 100).toFixed(2)
499
+ : 0;
500
+
501
+ return {
502
+ ...this.metrics,
503
+ hitRate: `${hitRate}%`,
504
+ cacheSize: {
505
+ fileContent: this.caches.fileContent.size,
506
+ parsedConversations: this.caches.parsedConversations.size,
507
+ tokenUsage: this.caches.tokenUsage.size,
508
+ modelInfo: this.caches.modelInfo.size,
509
+ statusSquares: this.caches.statusSquares.size,
510
+ fileStats: this.caches.fileStats.size,
511
+ projectStats: this.caches.projectStats.size,
512
+ },
513
+ memoryUsage: process.memoryUsage()
514
+ };
515
+ }
516
+
517
+ /**
518
+ * Log cache performance
519
+ */
520
+ logStats() {
521
+ const stats = this.getStats();
522
+ console.log(chalk.cyan('📊 Cache Statistics:'));
523
+ console.log(chalk.gray(` Hit Rate: ${stats.hitRate}`));
524
+ console.log(chalk.gray(` Hits: ${stats.hits}, Misses: ${stats.misses}`));
525
+ console.log(chalk.gray(` Invalidations: ${stats.invalidations}`));
526
+ console.log(chalk.gray(` Cache Sizes: ${JSON.stringify(stats.cacheSize)}`));
527
+ }
528
+
529
+ /**
530
+ * Check if cache is warming up (high miss rate)
531
+ * @returns {boolean} True if cache needs warming
532
+ */
533
+ needsWarming() {
534
+ const total = this.metrics.hits + this.metrics.misses;
535
+ if (total < 10) return true; // Not enough data
536
+
537
+ const hitRate = this.metrics.hits / total;
538
+ return hitRate < 0.5; // Less than 50% hit rate
539
+ }
540
+
541
+ /**
542
+ * Set cache configuration
543
+ * @param {Object} config - New configuration
544
+ */
545
+ configure(config) {
546
+ this.config = { ...this.config, ...config };
547
+ }
548
+ }
549
+
550
+ module.exports = DataCache;