@brutalist/mcp 0.5.0 โ†’ 0.6.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.
@@ -0,0 +1,260 @@
1
+ import { createHash } from 'crypto';
2
+ import { gzip, gunzip } from 'zlib';
3
+ import { promisify } from 'util';
4
+ import { logger } from '../logger.js';
5
+ const gzipAsync = promisify(gzip);
6
+ const gunzipAsync = promisify(gunzip);
7
+ /**
8
+ * LRU Response Cache with TTL and memory management
9
+ */
10
+ export class ResponseCache {
11
+ cache = new Map();
12
+ accessOrder = [];
13
+ stats = {
14
+ entries: 0,
15
+ totalSize: 0,
16
+ hits: 0,
17
+ misses: 0,
18
+ evictions: 0
19
+ };
20
+ // Configuration
21
+ maxEntries;
22
+ ttlMs;
23
+ maxTotalSizeMB;
24
+ maxEntrySizeMB;
25
+ compressionThresholdMB;
26
+ cleanupTimer;
27
+ constructor(options = {}) {
28
+ this.maxEntries = options.maxEntries || 50;
29
+ this.ttlMs = (options.ttlHours || 2) * 60 * 60 * 1000; // Convert hours to ms
30
+ this.maxTotalSizeMB = options.maxTotalSizeMB || 500;
31
+ this.maxEntrySizeMB = options.maxEntrySizeMB || 10;
32
+ this.compressionThresholdMB = options.compressionThresholdMB || 1;
33
+ // Log configuration
34
+ logger.info(`๐Ÿ“ฆ ResponseCache initialized:`, {
35
+ maxEntries: this.maxEntries,
36
+ ttlHours: this.ttlMs / (1000 * 60 * 60),
37
+ maxTotalSizeMB: this.maxTotalSizeMB,
38
+ maxEntrySizeMB: this.maxEntrySizeMB,
39
+ compressionThresholdMB: this.compressionThresholdMB
40
+ });
41
+ // Periodic cleanup
42
+ this.cleanupTimer = setInterval(() => this.cleanupExpired(), 5 * 60 * 1000); // Every 5 minutes
43
+ // Allow Node to exit even if timer is active
44
+ this.cleanupTimer.unref();
45
+ }
46
+ /**
47
+ * Generate cache key from request parameters
48
+ */
49
+ generateCacheKey(params) {
50
+ // Create deterministic string from params
51
+ const sortedParams = Object.keys(params)
52
+ .sort()
53
+ .reduce((acc, key) => {
54
+ if (params[key] !== undefined && key !== 'analysis_id' && key !== 'offset' && key !== 'limit' && key !== 'cursor' && key !== 'force_refresh') {
55
+ acc[key] = params[key];
56
+ }
57
+ return acc;
58
+ }, {});
59
+ const paramString = JSON.stringify(sortedParams);
60
+ const hash = createHash('sha256').update(paramString).digest('hex');
61
+ return hash;
62
+ }
63
+ /**
64
+ * Generate short analysis ID from cache key
65
+ */
66
+ generateAnalysisId(cacheKey) {
67
+ return cacheKey.substring(0, 8);
68
+ }
69
+ /**
70
+ * Store response in cache
71
+ */
72
+ async set(params, content, forceKey) {
73
+ const cacheKey = forceKey || this.generateCacheKey(params);
74
+ const analysisId = this.generateAnalysisId(cacheKey);
75
+ // Check entry size
76
+ const contentSize = Buffer.byteLength(content, 'utf-8');
77
+ const sizeMB = contentSize / (1024 * 1024);
78
+ if (sizeMB > this.maxEntrySizeMB) {
79
+ logger.warn(`โš ๏ธ Response too large for cache: ${sizeMB.toFixed(2)}MB > ${this.maxEntrySizeMB}MB`);
80
+ return { cacheKey, analysisId };
81
+ }
82
+ // Compress if needed
83
+ let finalContent = content;
84
+ let compressed = false;
85
+ if (sizeMB > this.compressionThresholdMB) {
86
+ try {
87
+ const compressedBuffer = await gzipAsync(Buffer.from(content, 'utf-8'));
88
+ const compressedSize = compressedBuffer.length / (1024 * 1024);
89
+ logger.info(`๐Ÿ—œ๏ธ Compressed response: ${sizeMB.toFixed(2)}MB โ†’ ${compressedSize.toFixed(2)}MB`);
90
+ finalContent = compressedBuffer.toString('base64');
91
+ compressed = true;
92
+ }
93
+ catch (error) {
94
+ logger.error('Compression failed:', error);
95
+ }
96
+ }
97
+ // Check total cache size
98
+ await this.ensureCapacity(contentSize);
99
+ // Store in cache
100
+ const entry = {
101
+ content: finalContent,
102
+ timestamp: Date.now(),
103
+ analysisId,
104
+ cacheKey,
105
+ requestParams: params,
106
+ compressed,
107
+ size: compressed ? Buffer.byteLength(finalContent, 'base64') : contentSize
108
+ };
109
+ this.cache.set(cacheKey, entry);
110
+ this.cache.set(analysisId, entry); // Also index by short ID
111
+ this.updateAccessOrder(cacheKey); // Only track cache key in LRU order
112
+ this.stats.entries = this.cache.size / 2; // Divided by 2 because we store twice
113
+ this.stats.totalSize += entry.size;
114
+ logger.info(`โœ… Cached response: ${analysisId} (${sizeMB.toFixed(2)}MB${compressed ? ' compressed' : ''})`);
115
+ return { cacheKey, analysisId };
116
+ }
117
+ /**
118
+ * Retrieve response from cache
119
+ */
120
+ async get(keyOrId) {
121
+ const entry = this.cache.get(keyOrId);
122
+ if (!entry) {
123
+ this.stats.misses++;
124
+ logger.debug(`โŒ Cache miss: ${keyOrId}`);
125
+ return null;
126
+ }
127
+ // Check TTL
128
+ const age = Date.now() - entry.timestamp;
129
+ if (age > this.ttlMs) {
130
+ logger.info(`โฐ Cache expired: ${keyOrId} (age: ${(age / 1000 / 60).toFixed(0)} minutes)`);
131
+ this.delete(keyOrId);
132
+ this.stats.misses++;
133
+ return null;
134
+ }
135
+ // Update access order (always use cache key for consistency)
136
+ this.updateAccessOrder(entry.cacheKey);
137
+ this.stats.hits++;
138
+ logger.info(`โœ… Cache hit: ${keyOrId} (age: ${(age / 1000 / 60).toFixed(0)} minutes)`);
139
+ // Decompress if needed
140
+ if (entry.compressed) {
141
+ try {
142
+ const buffer = Buffer.from(entry.content, 'base64');
143
+ const decompressed = await gunzipAsync(buffer);
144
+ return decompressed.toString('utf-8');
145
+ }
146
+ catch (error) {
147
+ logger.error('Decompression failed:', error);
148
+ this.delete(keyOrId);
149
+ return null;
150
+ }
151
+ }
152
+ return entry.content;
153
+ }
154
+ /**
155
+ * Check if key exists in cache
156
+ */
157
+ has(keyOrId) {
158
+ const entry = this.cache.get(keyOrId);
159
+ if (!entry)
160
+ return false;
161
+ // Check if expired
162
+ const age = Date.now() - entry.timestamp;
163
+ if (age > this.ttlMs) {
164
+ this.delete(keyOrId);
165
+ return false;
166
+ }
167
+ return true;
168
+ }
169
+ /**
170
+ * Delete entry from cache
171
+ */
172
+ delete(keyOrId) {
173
+ const entry = this.cache.get(keyOrId);
174
+ if (entry) {
175
+ this.stats.totalSize -= entry.size;
176
+ // Delete both the cache key and analysis ID
177
+ this.cache.delete(entry.cacheKey);
178
+ this.cache.delete(entry.analysisId);
179
+ // Remove cache key from access order
180
+ this.accessOrder = this.accessOrder.filter(k => k !== entry.cacheKey);
181
+ this.stats.entries = this.cache.size / 2;
182
+ }
183
+ }
184
+ /**
185
+ * Update LRU access order
186
+ */
187
+ updateAccessOrder(key) {
188
+ this.accessOrder = this.accessOrder.filter(k => k !== key);
189
+ this.accessOrder.push(key);
190
+ }
191
+ /**
192
+ * Ensure cache has capacity for new entry
193
+ */
194
+ async ensureCapacity(newEntrySize) {
195
+ const maxTotalSize = this.maxTotalSizeMB * 1024 * 1024;
196
+ // Check total size limit
197
+ while (this.stats.totalSize + newEntrySize > maxTotalSize && this.accessOrder.length > 0) {
198
+ const oldestKey = this.accessOrder[0];
199
+ logger.info(`๐Ÿ—‘๏ธ Evicting for size limit: ${oldestKey}`);
200
+ this.delete(oldestKey);
201
+ this.stats.evictions++;
202
+ }
203
+ // Check entry count limit
204
+ while (this.cache.size / 2 >= this.maxEntries && this.accessOrder.length > 0) {
205
+ const oldestKey = this.accessOrder[0];
206
+ logger.info(`๐Ÿ—‘๏ธ Evicting for entry limit: ${oldestKey}`);
207
+ this.delete(oldestKey);
208
+ this.stats.evictions++;
209
+ }
210
+ }
211
+ /**
212
+ * Clean up expired entries
213
+ */
214
+ cleanupExpired() {
215
+ const now = Date.now();
216
+ let cleaned = 0;
217
+ for (const [key, entry] of this.cache.entries()) {
218
+ if (now - entry.timestamp > this.ttlMs) {
219
+ this.delete(key);
220
+ cleaned++;
221
+ }
222
+ }
223
+ if (cleaned > 0) {
224
+ logger.info(`๐Ÿงน Cleaned ${cleaned} expired cache entries`);
225
+ }
226
+ }
227
+ /**
228
+ * Get cache statistics
229
+ */
230
+ getStats() {
231
+ return { ...this.stats };
232
+ }
233
+ /**
234
+ * Clear entire cache
235
+ */
236
+ clear() {
237
+ this.cache.clear();
238
+ this.accessOrder = [];
239
+ this.stats = {
240
+ entries: 0,
241
+ totalSize: 0,
242
+ hits: this.stats.hits,
243
+ misses: this.stats.misses,
244
+ evictions: this.stats.evictions
245
+ };
246
+ logger.info('๐Ÿ—‘๏ธ Cache cleared');
247
+ }
248
+ /**
249
+ * Destroy cache and cleanup resources
250
+ */
251
+ destroy() {
252
+ if (this.cleanupTimer) {
253
+ clearInterval(this.cleanupTimer);
254
+ this.cleanupTimer = undefined;
255
+ }
256
+ this.clear();
257
+ logger.info('๐Ÿ’€ Cache destroyed');
258
+ }
259
+ }
260
+ //# sourceMappingURL=response-cache.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"response-cache.js","sourceRoot":"","sources":["../../src/utils/response-cache.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAGA;;;;;;;;GAQG;AACH,wBAAgB,sBAAsB,CACpC,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,MAAM,EAChB,SAAS,GAAE,OAAe,GACzB,MAAM,CAeR"}
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAGA;;;;;;;;GAQG;AACH,wBAAgB,sBAAsB,CACpC,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,MAAM,EAChB,SAAS,GAAE,OAAe,GACzB,MAAM,CAsCR"}
package/dist/utils.js CHANGED
@@ -10,14 +10,33 @@ import { realpathSync, existsSync } from 'fs';
10
10
  * @throws Error if the path is outside the project root or does not exist (if mustExist is true).
11
11
  */
12
12
  export function resolveAndValidatePath(projectRoot, userPath, mustExist = false) {
13
+ // Check for null byte injection before any path operations
14
+ if (userPath.includes('\0')) {
15
+ throw new Error(`Path traversal detected`);
16
+ }
13
17
  const absoluteProjectRoot = realpathSync(projectRoot);
18
+ // For absolute paths, check if they start outside project root immediately
19
+ if (resolve(userPath) === userPath) { // userPath is absolute
20
+ if (!userPath.startsWith(absoluteProjectRoot + sep) && userPath !== absoluteProjectRoot) {
21
+ throw new Error(`Path traversal detected`);
22
+ }
23
+ }
14
24
  const resolvedPath = resolve(absoluteProjectRoot, userPath);
15
- const absoluteResolvedPath = realpathSync(resolvedPath);
25
+ let absoluteResolvedPath;
26
+ const pathExists = existsSync(resolvedPath);
27
+ if (pathExists) {
28
+ // Use realpathSync to resolve symlinks and detect traversal for existing paths
29
+ absoluteResolvedPath = realpathSync(resolvedPath);
30
+ }
31
+ else {
32
+ // For non-existent paths, use logical resolution for traversal detection
33
+ absoluteResolvedPath = resolvedPath;
34
+ }
16
35
  // Ensure the resolved path is a sub-path of the project root
17
36
  if (!absoluteResolvedPath.startsWith(absoluteProjectRoot + sep) && absoluteResolvedPath !== absoluteProjectRoot) {
18
- throw new Error(`Path traversal detected: ${userPath} resolves outside project root.`);
37
+ throw new Error(`Path traversal detected`);
19
38
  }
20
- if (mustExist && !existsSync(absoluteResolvedPath)) {
39
+ if (mustExist && !pathExists) {
21
40
  throw new Error(`Path does not exist: ${userPath}`);
22
41
  }
23
42
  return absoluteResolvedPath;
package/dist/utils.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAQ,GAAG,EAAE,MAAM,MAAM,CAAC;AAC1C,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,IAAI,CAAC;AAE9C;;;;;;;;GAQG;AACH,MAAM,UAAU,sBAAsB,CACpC,WAAmB,EACnB,QAAgB,EAChB,YAAqB,KAAK;IAE1B,MAAM,mBAAmB,GAAG,YAAY,CAAC,WAAW,CAAC,CAAC;IACtD,MAAM,YAAY,GAAG,OAAO,CAAC,mBAAmB,EAAE,QAAQ,CAAC,CAAC;IAC5D,MAAM,oBAAoB,GAAG,YAAY,CAAC,YAAY,CAAC,CAAC;IAExD,6DAA6D;IAC7D,IAAI,CAAC,oBAAoB,CAAC,UAAU,CAAC,mBAAmB,GAAG,GAAG,CAAC,IAAI,oBAAoB,KAAK,mBAAmB,EAAE,CAAC;QAChH,MAAM,IAAI,KAAK,CAAC,4BAA4B,QAAQ,iCAAiC,CAAC,CAAC;IACzF,CAAC;IAED,IAAI,SAAS,IAAI,CAAC,UAAU,CAAC,oBAAoB,CAAC,EAAE,CAAC;QACnD,MAAM,IAAI,KAAK,CAAC,wBAAwB,QAAQ,EAAE,CAAC,CAAC;IACtD,CAAC;IAED,OAAO,oBAAoB,CAAC;AAC9B,CAAC"}
1
+ {"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAQ,GAAG,EAAE,MAAM,MAAM,CAAC;AAC1C,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,IAAI,CAAC;AAE9C;;;;;;;;GAQG;AACH,MAAM,UAAU,sBAAsB,CACpC,WAAmB,EACnB,QAAgB,EAChB,YAAqB,KAAK;IAE1B,2DAA2D;IAC3D,IAAI,QAAQ,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;QAC5B,MAAM,IAAI,KAAK,CAAC,yBAAyB,CAAC,CAAC;IAC7C,CAAC;IAED,MAAM,mBAAmB,GAAG,YAAY,CAAC,WAAW,CAAC,CAAC;IAEtD,2EAA2E;IAC3E,IAAI,OAAO,CAAC,QAAQ,CAAC,KAAK,QAAQ,EAAE,CAAC,CAAC,uBAAuB;QAC3D,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC,mBAAmB,GAAG,GAAG,CAAC,IAAI,QAAQ,KAAK,mBAAmB,EAAE,CAAC;YACxF,MAAM,IAAI,KAAK,CAAC,yBAAyB,CAAC,CAAC;QAC7C,CAAC;IACH,CAAC;IAED,MAAM,YAAY,GAAG,OAAO,CAAC,mBAAmB,EAAE,QAAQ,CAAC,CAAC;IAE5D,IAAI,oBAA4B,CAAC;IACjC,MAAM,UAAU,GAAG,UAAU,CAAC,YAAY,CAAC,CAAC;IAE5C,IAAI,UAAU,EAAE,CAAC;QACf,+EAA+E;QAC/E,oBAAoB,GAAG,YAAY,CAAC,YAAY,CAAC,CAAC;IACpD,CAAC;SAAM,CAAC;QACN,yEAAyE;QACzE,oBAAoB,GAAG,YAAY,CAAC;IACtC,CAAC;IAED,6DAA6D;IAC7D,IAAI,CAAC,oBAAoB,CAAC,UAAU,CAAC,mBAAmB,GAAG,GAAG,CAAC,IAAI,oBAAoB,KAAK,mBAAmB,EAAE,CAAC;QAChH,MAAM,IAAI,KAAK,CAAC,yBAAyB,CAAC,CAAC;IAC7C,CAAC;IAED,IAAI,SAAS,IAAI,CAAC,UAAU,EAAE,CAAC;QAC7B,MAAM,IAAI,KAAK,CAAC,wBAAwB,QAAQ,EAAE,CAAC,CAAC;IACtD,CAAC;IAED,OAAO,oBAAoB,CAAC;AAC9B,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brutalist/mcp",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Deploy Claude, Codex & Gemini CLI agents to demolish your work before users do. Real file analysis. Brutal honesty. Now with intelligent pagination.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -78,7 +78,7 @@
78
78
  "zod": "^3.24.2"
79
79
  },
80
80
  "devDependencies": {
81
- "@types/express": "^5.0.3",
81
+ "@types/express": "^4.17.21",
82
82
  "@types/jest": "^30.0.0",
83
83
  "@types/node": "^20.17.28",
84
84
  "@types/node-fetch": "^2.6.13",