@supermodeltools/mcp-server 0.4.4 → 0.4.6
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 +0 -12
- package/dist/cache/graph-cache.js +32 -3
- package/dist/constants.js +20 -0
- package/dist/index.js +35 -1
- package/dist/queries/discovery.js +20 -5
- package/dist/queries/index.js +30 -19
- package/dist/queries/traversal.js +75 -22
- package/dist/server.js +62 -6
- package/dist/tools/create-supermodel-graph.js +263 -119
- package/dist/utils/logger.js +41 -0
- package/dist/utils/zip-repository.js +327 -41
- package/package.json +10 -2
|
@@ -1,8 +1,41 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* Automatic repository zipping with gitignore support
|
|
3
|
+
* Automatic repository zipping with gitignore and dockerignore support
|
|
4
4
|
* Creates temporary ZIP files for codebase analysis
|
|
5
5
|
*/
|
|
6
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
7
|
+
if (k2 === undefined) k2 = k;
|
|
8
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
9
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
10
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
11
|
+
}
|
|
12
|
+
Object.defineProperty(o, k2, desc);
|
|
13
|
+
}) : (function(o, m, k, k2) {
|
|
14
|
+
if (k2 === undefined) k2 = k;
|
|
15
|
+
o[k2] = m[k];
|
|
16
|
+
}));
|
|
17
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
18
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
19
|
+
}) : function(o, v) {
|
|
20
|
+
o["default"] = v;
|
|
21
|
+
});
|
|
22
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
23
|
+
var ownKeys = function(o) {
|
|
24
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
25
|
+
var ar = [];
|
|
26
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
27
|
+
return ar;
|
|
28
|
+
};
|
|
29
|
+
return ownKeys(o);
|
|
30
|
+
};
|
|
31
|
+
return function (mod) {
|
|
32
|
+
if (mod && mod.__esModule) return mod;
|
|
33
|
+
var result = {};
|
|
34
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
35
|
+
__setModuleDefault(result, mod);
|
|
36
|
+
return result;
|
|
37
|
+
};
|
|
38
|
+
})();
|
|
6
39
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
7
40
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
8
41
|
};
|
|
@@ -15,9 +48,11 @@ const os_1 = require("os");
|
|
|
15
48
|
const archiver_1 = __importDefault(require("archiver"));
|
|
16
49
|
const ignore_1 = __importDefault(require("ignore"));
|
|
17
50
|
const crypto_1 = require("crypto");
|
|
51
|
+
const constants_1 = require("../constants");
|
|
52
|
+
const logger = __importStar(require("./logger"));
|
|
18
53
|
/**
|
|
19
54
|
* Standard exclusions for security and size optimization
|
|
20
|
-
* These patterns are applied in addition to .gitignore
|
|
55
|
+
* These patterns are applied in addition to .gitignore and .dockerignore
|
|
21
56
|
*/
|
|
22
57
|
const STANDARD_EXCLUSIONS = [
|
|
23
58
|
// Version control
|
|
@@ -82,34 +117,69 @@ const STANDARD_EXCLUSIONS = [
|
|
|
82
117
|
'*.dmg',
|
|
83
118
|
];
|
|
84
119
|
/**
|
|
85
|
-
* Create a ZIP archive of a directory with gitignore support
|
|
120
|
+
* Create a ZIP archive of a directory with gitignore and dockerignore support
|
|
121
|
+
*
|
|
122
|
+
* @param directoryPath - Path to the directory to archive
|
|
123
|
+
* @param options - Configuration options for ZIP creation
|
|
124
|
+
* @param options.maxSizeBytes - Maximum ZIP size in bytes (default: 500MB)
|
|
125
|
+
* @param options.additionalExclusions - Custom patterns to exclude
|
|
126
|
+
* @param options.includeGitignore - Whether to include .gitignore files (default: true)
|
|
127
|
+
* @param options.onProgress - Optional callback to track progress
|
|
128
|
+
* @param options.progressInterval - Files to process between progress callbacks (default: 100)
|
|
129
|
+
* @returns Promise resolving to ZipResult with path, cleanup function, and metadata
|
|
130
|
+
*
|
|
131
|
+
* @example
|
|
132
|
+
* // Create ZIP excluding .gitignore files with progress tracking
|
|
133
|
+
* const result = await zipRepository('/path/to/repo', {
|
|
134
|
+
* includeGitignore: false,
|
|
135
|
+
* maxSizeBytes: 100 * 1024 * 1024, // 100MB
|
|
136
|
+
* onProgress: (stats) => console.log(`${stats.filesProcessed} files`)
|
|
137
|
+
* });
|
|
86
138
|
*/
|
|
87
139
|
async function zipRepository(directoryPath, options = {}) {
|
|
88
|
-
const maxSizeBytes = options.maxSizeBytes ||
|
|
140
|
+
const maxSizeBytes = options.maxSizeBytes || constants_1.MAX_ZIP_SIZE_BYTES;
|
|
89
141
|
// Validate directory exists
|
|
90
142
|
try {
|
|
91
143
|
const stats = await fs_1.promises.stat(directoryPath);
|
|
92
144
|
if (!stats.isDirectory()) {
|
|
93
|
-
|
|
145
|
+
const errorMsg = `Path is not a directory: ${directoryPath}`;
|
|
146
|
+
logger.error(errorMsg);
|
|
147
|
+
throw new Error(errorMsg);
|
|
94
148
|
}
|
|
95
149
|
}
|
|
96
150
|
catch (error) {
|
|
97
151
|
if (error.code === 'ENOENT') {
|
|
98
|
-
|
|
152
|
+
const errorMsg = `Directory does not exist: ${directoryPath}`;
|
|
153
|
+
logger.error(errorMsg);
|
|
154
|
+
throw new Error(errorMsg);
|
|
99
155
|
}
|
|
100
156
|
if (error.code === 'EACCES') {
|
|
101
|
-
|
|
157
|
+
const errorMsg = `Permission denied accessing directory: ${directoryPath}`;
|
|
158
|
+
logger.error(errorMsg);
|
|
159
|
+
throw new Error(errorMsg);
|
|
102
160
|
}
|
|
161
|
+
// Re-throw unknown errors with logging
|
|
162
|
+
logger.error('Failed to validate directory:', directoryPath);
|
|
163
|
+
logger.error('Error:', error.message);
|
|
103
164
|
throw error;
|
|
104
165
|
}
|
|
105
166
|
// Parse gitignore files
|
|
106
|
-
const ignoreFilter = await buildIgnoreFilter(directoryPath, options.additionalExclusions);
|
|
167
|
+
const ignoreFilter = await buildIgnoreFilter(directoryPath, options.additionalExclusions, options.includeGitignore);
|
|
168
|
+
// Estimate directory size before starting ZIP creation
|
|
169
|
+
logger.debug('Estimating directory size...');
|
|
170
|
+
const estimatedSize = await estimateDirectorySize(directoryPath, ignoreFilter);
|
|
171
|
+
logger.debug('Estimated size:', formatBytes(estimatedSize));
|
|
172
|
+
// Check if estimated size exceeds limit
|
|
173
|
+
if (estimatedSize > maxSizeBytes) {
|
|
174
|
+
throw new Error(`Directory size (${formatBytes(estimatedSize)}) exceeds maximum allowed size (${formatBytes(maxSizeBytes)}). ` +
|
|
175
|
+
`Consider excluding more directories or analyzing a subdirectory.`);
|
|
176
|
+
}
|
|
107
177
|
// Create temp file path
|
|
108
178
|
const tempDir = (0, os_1.tmpdir)();
|
|
109
179
|
const zipFileName = `supermodel-${(0, crypto_1.randomBytes)(8).toString('hex')}.zip`;
|
|
110
180
|
const zipPath = (0, path_1.join)(tempDir, zipFileName);
|
|
111
|
-
|
|
112
|
-
|
|
181
|
+
logger.debug('Creating ZIP:', zipPath);
|
|
182
|
+
logger.debug('Source directory:', directoryPath);
|
|
113
183
|
// Create ZIP archive
|
|
114
184
|
let fileCount = 0;
|
|
115
185
|
let totalSize = 0;
|
|
@@ -120,14 +190,15 @@ async function zipRepository(directoryPath, options = {}) {
|
|
|
120
190
|
// Track errors
|
|
121
191
|
let archiveError = null;
|
|
122
192
|
archive.on('error', (err) => {
|
|
193
|
+
logger.error('Archive error:', err.message);
|
|
123
194
|
archiveError = err;
|
|
124
195
|
});
|
|
125
196
|
archive.on('warning', (err) => {
|
|
126
197
|
if (err.code === 'ENOENT') {
|
|
127
|
-
|
|
198
|
+
logger.warn('File not found (skipping):', err.message);
|
|
128
199
|
}
|
|
129
200
|
else {
|
|
130
|
-
|
|
201
|
+
logger.warn('Archive warning:', err.message);
|
|
131
202
|
}
|
|
132
203
|
});
|
|
133
204
|
// Track progress
|
|
@@ -136,16 +207,22 @@ async function zipRepository(directoryPath, options = {}) {
|
|
|
136
207
|
totalSize += entry.stats?.size || 0;
|
|
137
208
|
// Check size limit
|
|
138
209
|
if (totalSize > maxSizeBytes) {
|
|
139
|
-
|
|
140
|
-
archiveError = new Error(`ZIP size exceeds limit (${formatBytes(maxSizeBytes)}). ` +
|
|
210
|
+
const errorMsg = `ZIP size exceeds limit (${formatBytes(maxSizeBytes)}). ` +
|
|
141
211
|
`Current size: ${formatBytes(totalSize)}. ` +
|
|
142
|
-
`Consider excluding more directories or analyzing a subdirectory
|
|
212
|
+
`Consider excluding more directories or analyzing a subdirectory.`;
|
|
213
|
+
logger.error(errorMsg);
|
|
214
|
+
archive.abort();
|
|
215
|
+
archiveError = new Error(errorMsg);
|
|
143
216
|
}
|
|
144
217
|
});
|
|
145
218
|
// Pipe to file
|
|
146
219
|
archive.pipe(output);
|
|
147
220
|
// Add files recursively with filtering
|
|
148
|
-
|
|
221
|
+
// Initialize progress state if progress callback is provided
|
|
222
|
+
const progressState = options.onProgress
|
|
223
|
+
? { filesProcessed: 0, bytesProcessed: 0, lastReportedCount: 0, lastFile: '' }
|
|
224
|
+
: undefined;
|
|
225
|
+
await addFilesRecursively(archive, directoryPath, directoryPath, ignoreFilter, options, progressState);
|
|
149
226
|
// Finalize archive
|
|
150
227
|
await archive.finalize();
|
|
151
228
|
// Wait for output stream to finish
|
|
@@ -158,10 +235,14 @@ async function zipRepository(directoryPath, options = {}) {
|
|
|
158
235
|
resolve();
|
|
159
236
|
}
|
|
160
237
|
});
|
|
161
|
-
output.on('error',
|
|
238
|
+
output.on('error', (err) => {
|
|
239
|
+
logger.error('Output stream error:', err.message);
|
|
240
|
+
reject(err);
|
|
241
|
+
});
|
|
162
242
|
});
|
|
163
243
|
// Check for errors during archiving
|
|
164
244
|
if (archiveError) {
|
|
245
|
+
logger.error('Archiving failed, cleaning up partial ZIP');
|
|
165
246
|
// Clean up partial ZIP
|
|
166
247
|
await fs_1.promises.unlink(zipPath).catch(() => { });
|
|
167
248
|
throw archiveError;
|
|
@@ -169,18 +250,18 @@ async function zipRepository(directoryPath, options = {}) {
|
|
|
169
250
|
// Get final file size
|
|
170
251
|
const zipStats = await fs_1.promises.stat(zipPath);
|
|
171
252
|
const zipSizeBytes = zipStats.size;
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
253
|
+
logger.debug('ZIP created successfully');
|
|
254
|
+
logger.debug('Files included:', fileCount);
|
|
255
|
+
logger.debug('ZIP size:', formatBytes(zipSizeBytes));
|
|
175
256
|
// Create cleanup function
|
|
176
257
|
const cleanup = async () => {
|
|
177
258
|
try {
|
|
178
259
|
await fs_1.promises.unlink(zipPath);
|
|
179
|
-
|
|
260
|
+
logger.debug('Cleaned up ZIP:', zipPath);
|
|
180
261
|
}
|
|
181
262
|
catch (error) {
|
|
182
263
|
if (error.code !== 'ENOENT') {
|
|
183
|
-
|
|
264
|
+
logger.warn('Failed to cleanup ZIP:', error.message);
|
|
184
265
|
}
|
|
185
266
|
}
|
|
186
267
|
};
|
|
@@ -192,9 +273,76 @@ async function zipRepository(directoryPath, options = {}) {
|
|
|
192
273
|
};
|
|
193
274
|
}
|
|
194
275
|
/**
|
|
195
|
-
*
|
|
276
|
+
* Estimate total size of directory with ignore filters applied
|
|
277
|
+
* Returns total size in bytes of files that would be included in the ZIP
|
|
196
278
|
*/
|
|
197
|
-
async function
|
|
279
|
+
async function estimateDirectorySize(rootDir, ignoreFilter) {
|
|
280
|
+
let totalSize = 0;
|
|
281
|
+
async function walkDirectory(currentDir) {
|
|
282
|
+
let entries;
|
|
283
|
+
try {
|
|
284
|
+
entries = await fs_1.promises.readdir(currentDir);
|
|
285
|
+
}
|
|
286
|
+
catch (error) {
|
|
287
|
+
if (error.code === 'EACCES') {
|
|
288
|
+
logger.warn('Permission denied:', currentDir);
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
throw error;
|
|
292
|
+
}
|
|
293
|
+
for (const entry of entries) {
|
|
294
|
+
const fullPath = (0, path_1.join)(currentDir, entry);
|
|
295
|
+
const relativePath = (0, path_1.relative)(rootDir, fullPath);
|
|
296
|
+
// Normalize path for ignore matching (use forward slashes)
|
|
297
|
+
const normalizedRelativePath = relativePath.split(path_1.sep).join('/');
|
|
298
|
+
// Check if ignored
|
|
299
|
+
if (ignoreFilter.ignores(normalizedRelativePath)) {
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
let stats;
|
|
303
|
+
try {
|
|
304
|
+
stats = await fs_1.promises.lstat(fullPath);
|
|
305
|
+
}
|
|
306
|
+
catch (error) {
|
|
307
|
+
if (error.code === 'ENOENT') {
|
|
308
|
+
// File disappeared, skip
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
logger.warn('Failed to stat:', fullPath, error.message);
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
// Skip symlinks to prevent following links outside the repository
|
|
315
|
+
if (stats.isSymbolicLink()) {
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
if (stats.isDirectory()) {
|
|
319
|
+
// Check if directory itself should be ignored
|
|
320
|
+
const dirPath = normalizedRelativePath + '/';
|
|
321
|
+
if (ignoreFilter.ignores(dirPath)) {
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
// Recurse into directory
|
|
325
|
+
await walkDirectory(fullPath);
|
|
326
|
+
}
|
|
327
|
+
else if (stats.isFile()) {
|
|
328
|
+
// Add file size to total
|
|
329
|
+
totalSize += stats.size;
|
|
330
|
+
}
|
|
331
|
+
// Skip other special files (sockets, FIFOs, etc.)
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
await walkDirectory(rootDir);
|
|
335
|
+
return totalSize;
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Build ignore filter from .gitignore, .dockerignore files and standard exclusions
|
|
339
|
+
* Recursively finds and parses .gitignore files in subdirectories
|
|
340
|
+
*
|
|
341
|
+
* @param rootDir - Root directory to build filter for
|
|
342
|
+
* @param additionalExclusions - Additional patterns to exclude
|
|
343
|
+
* @param includeGitignore - Whether to include .gitignore files in the archive (default: true)
|
|
344
|
+
*/
|
|
345
|
+
async function buildIgnoreFilter(rootDir, additionalExclusions = [], includeGitignore = true) {
|
|
198
346
|
const ig = (0, ignore_1.default)();
|
|
199
347
|
// Add standard exclusions
|
|
200
348
|
ig.add(STANDARD_EXCLUSIONS);
|
|
@@ -202,39 +350,142 @@ async function buildIgnoreFilter(rootDir, additionalExclusions = []) {
|
|
|
202
350
|
if (additionalExclusions.length > 0) {
|
|
203
351
|
ig.add(additionalExclusions);
|
|
204
352
|
}
|
|
205
|
-
//
|
|
206
|
-
|
|
353
|
+
// Exclude .gitignore files if requested
|
|
354
|
+
if (includeGitignore === false) {
|
|
355
|
+
ig.add(['.gitignore', '**/.gitignore']);
|
|
356
|
+
logger.debug('Excluding .gitignore files from archive');
|
|
357
|
+
}
|
|
358
|
+
// Recursively find and parse all .gitignore files
|
|
359
|
+
const gitignoreFiles = await findGitignoreFiles(rootDir);
|
|
360
|
+
for (const gitignorePath of gitignoreFiles) {
|
|
361
|
+
try {
|
|
362
|
+
const gitignoreContent = await fs_1.promises.readFile(gitignorePath, 'utf-8');
|
|
363
|
+
const patterns = gitignoreContent
|
|
364
|
+
.split('\n')
|
|
365
|
+
.map(line => line.trim())
|
|
366
|
+
.filter(line => line && !line.startsWith('#'));
|
|
367
|
+
if (patterns.length > 0) {
|
|
368
|
+
// Get the directory containing this .gitignore
|
|
369
|
+
const gitignoreDir = gitignorePath.substring(0, gitignorePath.length - '.gitignore'.length);
|
|
370
|
+
const relativeDir = (0, path_1.relative)(rootDir, gitignoreDir);
|
|
371
|
+
// Scope patterns to their directory
|
|
372
|
+
const scopedPatterns = patterns.map(pattern => {
|
|
373
|
+
// If pattern starts with '/', it's relative to the .gitignore location
|
|
374
|
+
if (pattern.startsWith('/')) {
|
|
375
|
+
const patternWithoutSlash = pattern.substring(1);
|
|
376
|
+
return relativeDir ? `${relativeDir}/${patternWithoutSlash}` : patternWithoutSlash;
|
|
377
|
+
}
|
|
378
|
+
// If pattern starts with '!', handle negation
|
|
379
|
+
else if (pattern.startsWith('!')) {
|
|
380
|
+
const negatedPattern = pattern.substring(1);
|
|
381
|
+
if (negatedPattern.startsWith('/')) {
|
|
382
|
+
const patternWithoutSlash = negatedPattern.substring(1);
|
|
383
|
+
return relativeDir ? `!${relativeDir}/${patternWithoutSlash}` : `!${patternWithoutSlash}`;
|
|
384
|
+
}
|
|
385
|
+
// For non-rooted negation patterns, prefix with directory
|
|
386
|
+
return relativeDir ? `!${relativeDir}/${negatedPattern}` : `!${negatedPattern}`;
|
|
387
|
+
}
|
|
388
|
+
// For non-rooted patterns, prefix with the directory path
|
|
389
|
+
else {
|
|
390
|
+
return relativeDir ? `${relativeDir}/${pattern}` : pattern;
|
|
391
|
+
}
|
|
392
|
+
});
|
|
393
|
+
ig.add(scopedPatterns);
|
|
394
|
+
const location = relativeDir ? `in ${relativeDir}/` : 'in root';
|
|
395
|
+
logger.debug(`Loaded .gitignore ${location} with ${patterns.length} patterns`);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
catch (error) {
|
|
399
|
+
if (error.code !== 'ENOENT') {
|
|
400
|
+
logger.warn('Failed to read .gitignore at', gitignorePath, ':', error.message);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
// Parse .dockerignore in root
|
|
405
|
+
const dockerignorePath = (0, path_1.join)(rootDir, '.dockerignore');
|
|
207
406
|
try {
|
|
208
|
-
const
|
|
209
|
-
const patterns =
|
|
407
|
+
const dockerignoreContent = await fs_1.promises.readFile(dockerignorePath, 'utf-8');
|
|
408
|
+
const patterns = dockerignoreContent
|
|
210
409
|
.split('\n')
|
|
211
410
|
.map(line => line.trim())
|
|
212
411
|
.filter(line => line && !line.startsWith('#'));
|
|
213
412
|
if (patterns.length > 0) {
|
|
214
413
|
ig.add(patterns);
|
|
215
|
-
console.error('[DEBUG] Loaded .
|
|
414
|
+
console.error('[DEBUG] Loaded .dockerignore with', patterns.length, 'patterns');
|
|
216
415
|
}
|
|
217
416
|
}
|
|
218
417
|
catch (error) {
|
|
219
418
|
if (error.code !== 'ENOENT') {
|
|
220
|
-
console.error('[WARN] Failed to read .
|
|
419
|
+
console.error('[WARN] Failed to read .dockerignore:', error.message);
|
|
221
420
|
}
|
|
222
421
|
}
|
|
223
422
|
return ig;
|
|
224
423
|
}
|
|
424
|
+
/**
|
|
425
|
+
* Recursively find all .gitignore files in a directory tree
|
|
426
|
+
*/
|
|
427
|
+
async function findGitignoreFiles(rootDir) {
|
|
428
|
+
const gitignoreFiles = [];
|
|
429
|
+
async function searchDirectory(dir) {
|
|
430
|
+
let entries;
|
|
431
|
+
try {
|
|
432
|
+
entries = await fs_1.promises.readdir(dir);
|
|
433
|
+
}
|
|
434
|
+
catch (error) {
|
|
435
|
+
if (error.code === 'EACCES') {
|
|
436
|
+
logger.warn('Permission denied:', dir);
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
for (const entry of entries) {
|
|
442
|
+
// Skip .git directory and other version control directories
|
|
443
|
+
if (entry === '.git' || entry === '.svn' || entry === '.hg') {
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
446
|
+
const fullPath = (0, path_1.join)(dir, entry);
|
|
447
|
+
// If this is a .gitignore file, add it to the list
|
|
448
|
+
if (entry === '.gitignore') {
|
|
449
|
+
gitignoreFiles.push(fullPath);
|
|
450
|
+
continue;
|
|
451
|
+
}
|
|
452
|
+
// If it's a directory, recurse into it
|
|
453
|
+
try {
|
|
454
|
+
const stats = await fs_1.promises.lstat(fullPath);
|
|
455
|
+
if (stats.isDirectory() && !stats.isSymbolicLink()) {
|
|
456
|
+
await searchDirectory(fullPath);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
catch (error) {
|
|
460
|
+
// Skip files we can't access
|
|
461
|
+
continue;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
await searchDirectory(rootDir);
|
|
466
|
+
// Sort so root .gitignore is processed first
|
|
467
|
+
gitignoreFiles.sort((a, b) => {
|
|
468
|
+
const aDepth = a.split(path_1.sep).length;
|
|
469
|
+
const bDepth = b.split(path_1.sep).length;
|
|
470
|
+
return aDepth - bDepth;
|
|
471
|
+
});
|
|
472
|
+
return gitignoreFiles;
|
|
473
|
+
}
|
|
225
474
|
/**
|
|
226
475
|
* Recursively add files to archive with filtering
|
|
227
476
|
*/
|
|
228
|
-
async function addFilesRecursively(archive, rootDir, currentDir, ignoreFilter) {
|
|
477
|
+
async function addFilesRecursively(archive, rootDir, currentDir, ignoreFilter, options, progressState) {
|
|
229
478
|
let entries;
|
|
230
479
|
try {
|
|
231
480
|
entries = await fs_1.promises.readdir(currentDir);
|
|
232
481
|
}
|
|
233
482
|
catch (error) {
|
|
234
483
|
if (error.code === 'EACCES') {
|
|
235
|
-
|
|
484
|
+
logger.warn('Permission denied:', currentDir);
|
|
236
485
|
return;
|
|
237
486
|
}
|
|
487
|
+
logger.error('Failed to read directory:', currentDir);
|
|
488
|
+
logger.error('Error:', error.message);
|
|
238
489
|
throw error;
|
|
239
490
|
}
|
|
240
491
|
for (const entry of entries) {
|
|
@@ -248,16 +499,21 @@ async function addFilesRecursively(archive, rootDir, currentDir, ignoreFilter) {
|
|
|
248
499
|
}
|
|
249
500
|
let stats;
|
|
250
501
|
try {
|
|
251
|
-
stats = await fs_1.promises.
|
|
502
|
+
stats = await fs_1.promises.lstat(fullPath);
|
|
252
503
|
}
|
|
253
504
|
catch (error) {
|
|
254
505
|
if (error.code === 'ENOENT') {
|
|
255
|
-
//
|
|
506
|
+
// File disappeared, skip
|
|
256
507
|
continue;
|
|
257
508
|
}
|
|
258
509
|
console.error('[WARN] Failed to stat:', fullPath, error.message);
|
|
259
510
|
continue;
|
|
260
511
|
}
|
|
512
|
+
// Skip symlinks to prevent following links outside the repository
|
|
513
|
+
if (stats.isSymbolicLink()) {
|
|
514
|
+
logger.warn('Skipping symlink:', fullPath);
|
|
515
|
+
continue;
|
|
516
|
+
}
|
|
261
517
|
if (stats.isDirectory()) {
|
|
262
518
|
// Check if directory itself should be ignored
|
|
263
519
|
const dirPath = normalizedRelativePath + '/';
|
|
@@ -265,18 +521,48 @@ async function addFilesRecursively(archive, rootDir, currentDir, ignoreFilter) {
|
|
|
265
521
|
continue;
|
|
266
522
|
}
|
|
267
523
|
// Recurse into directory
|
|
268
|
-
await addFilesRecursively(archive, rootDir, fullPath, ignoreFilter);
|
|
524
|
+
await addFilesRecursively(archive, rootDir, fullPath, ignoreFilter, options, progressState);
|
|
269
525
|
}
|
|
270
526
|
else if (stats.isFile()) {
|
|
271
527
|
// Add file to archive
|
|
272
528
|
try {
|
|
273
529
|
archive.file(fullPath, { name: normalizedRelativePath });
|
|
530
|
+
// Track progress if callback is provided
|
|
531
|
+
if (progressState && options?.onProgress) {
|
|
532
|
+
progressState.filesProcessed++;
|
|
533
|
+
progressState.bytesProcessed += stats.size;
|
|
534
|
+
progressState.lastFile = normalizedRelativePath;
|
|
535
|
+
const progressInterval = options.progressInterval || 100;
|
|
536
|
+
// Report progress every N files
|
|
537
|
+
if (progressState.filesProcessed - progressState.lastReportedCount >= progressInterval) {
|
|
538
|
+
logger.debug(`Progress: ${progressState.filesProcessed} files, ${formatBytes(progressState.bytesProcessed)}`);
|
|
539
|
+
options.onProgress({
|
|
540
|
+
filesProcessed: progressState.filesProcessed,
|
|
541
|
+
currentFile: normalizedRelativePath,
|
|
542
|
+
bytesProcessed: progressState.bytesProcessed,
|
|
543
|
+
});
|
|
544
|
+
progressState.lastReportedCount = progressState.filesProcessed;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
274
547
|
}
|
|
275
548
|
catch (error) {
|
|
276
|
-
|
|
549
|
+
logger.warn('Failed to add file:', fullPath, error.message);
|
|
277
550
|
}
|
|
278
551
|
}
|
|
279
|
-
// Skip
|
|
552
|
+
// Skip other special files (sockets, FIFOs, etc.)
|
|
553
|
+
}
|
|
554
|
+
// Report final progress if we have a callback and there are unreported files
|
|
555
|
+
// Only report at the root level (when currentDir === rootDir)
|
|
556
|
+
if (currentDir === rootDir &&
|
|
557
|
+
progressState &&
|
|
558
|
+
options?.onProgress &&
|
|
559
|
+
progressState.filesProcessed > progressState.lastReportedCount) {
|
|
560
|
+
logger.debug(`Final progress: ${progressState.filesProcessed} files, ${formatBytes(progressState.bytesProcessed)}`);
|
|
561
|
+
options.onProgress({
|
|
562
|
+
filesProcessed: progressState.filesProcessed,
|
|
563
|
+
currentFile: progressState.lastFile,
|
|
564
|
+
bytesProcessed: progressState.bytesProcessed,
|
|
565
|
+
});
|
|
280
566
|
}
|
|
281
567
|
}
|
|
282
568
|
/**
|
|
@@ -295,7 +581,7 @@ function formatBytes(bytes) {
|
|
|
295
581
|
* Clean up old ZIP files from temp directory
|
|
296
582
|
* Removes ZIPs older than the specified age
|
|
297
583
|
*/
|
|
298
|
-
async function cleanupOldZips(maxAgeMs =
|
|
584
|
+
async function cleanupOldZips(maxAgeMs = constants_1.ZIP_CLEANUP_AGE_MS) {
|
|
299
585
|
const tempDir = (0, os_1.tmpdir)();
|
|
300
586
|
const now = Date.now();
|
|
301
587
|
try {
|
|
@@ -318,15 +604,15 @@ async function cleanupOldZips(maxAgeMs = 24 * 60 * 60 * 1000) {
|
|
|
318
604
|
catch (error) {
|
|
319
605
|
// File might have been deleted already, ignore
|
|
320
606
|
if (error.code !== 'ENOENT') {
|
|
321
|
-
|
|
607
|
+
logger.warn('Failed to cleanup:', fullPath, error.message);
|
|
322
608
|
}
|
|
323
609
|
}
|
|
324
610
|
}
|
|
325
611
|
if (removedCount > 0) {
|
|
326
|
-
|
|
612
|
+
logger.debug('Cleaned up', removedCount, 'old ZIP files');
|
|
327
613
|
}
|
|
328
614
|
}
|
|
329
615
|
catch (error) {
|
|
330
|
-
|
|
616
|
+
logger.warn('Failed to cleanup temp directory:', error.message);
|
|
331
617
|
}
|
|
332
618
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@supermodeltools/mcp-server",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.6",
|
|
4
4
|
"description": "MCP server for Supermodel API - code graph generation for AI agents",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -13,7 +13,10 @@
|
|
|
13
13
|
"scripts": {
|
|
14
14
|
"build": "tsc",
|
|
15
15
|
"start": "node dist/index.js",
|
|
16
|
-
"typecheck": "tsc --noEmit"
|
|
16
|
+
"typecheck": "tsc --noEmit",
|
|
17
|
+
"test": "jest",
|
|
18
|
+
"test:coverage": "jest --coverage",
|
|
19
|
+
"test:watch": "jest --watch"
|
|
17
20
|
},
|
|
18
21
|
"repository": {
|
|
19
22
|
"type": "git",
|
|
@@ -39,11 +42,16 @@
|
|
|
39
42
|
"archiver": "^7.0.1",
|
|
40
43
|
"ignore": "^7.0.5",
|
|
41
44
|
"jq-web": "^0.6.2",
|
|
45
|
+
"undici": "^7.18.2",
|
|
42
46
|
"zod": "^3.22.4"
|
|
43
47
|
},
|
|
44
48
|
"devDependencies": {
|
|
49
|
+
"@jest/globals": "^30.2.0",
|
|
45
50
|
"@types/archiver": "^7.0.0",
|
|
51
|
+
"@types/jest": "^30.0.0",
|
|
46
52
|
"@types/node": "^20.11.0",
|
|
53
|
+
"jest": "^30.2.0",
|
|
54
|
+
"ts-jest": "^29.4.6",
|
|
47
55
|
"typescript": "^5.3.3"
|
|
48
56
|
}
|
|
49
57
|
}
|