@supermodeltools/mcp-server 0.8.1 → 0.9.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.
- package/dist/cache/graph-cache.js +328 -16
- package/dist/cache/graph-types.js +2 -2
- package/dist/constants.js +8 -4
- package/dist/index.js +167 -9
- package/dist/server.js +61 -98
- package/dist/tools/overview.js +144 -0
- package/dist/tools/symbol-context.js +285 -0
- package/dist/utils/api-helpers.js +8 -22
- package/package.json +2 -2
- package/dist/cache/index.js +0 -21
- package/dist/filtering.js +0 -45
- package/dist/queries/discovery.js +0 -163
- package/dist/queries/index.js +0 -252
- package/dist/queries/summary.js +0 -36
- package/dist/queries/traversal.js +0 -445
- package/dist/queries/types.js +0 -38
- package/dist/tools/create-supermodel-graph.js +0 -691
- package/dist/tools/feature-request.js +0 -84
- package/dist/tools/find-call-sites.js +0 -141
- package/dist/tools/find-definition.js +0 -161
- package/dist/tools/graph-tools.js +0 -277
- package/dist/tools/report-bug.js +0 -133
- package/dist/tools/task-query-tools.js +0 -81
- package/dist/tools/trace-call-chain.js +0 -179
- package/dist/tools/trace-data-flow.js +0 -233
- package/dist/utils/github.js +0 -253
|
@@ -2,13 +2,59 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* LRU Cache for indexed graphs
|
|
4
4
|
* Stores raw API responses + derived indexes for fast query execution
|
|
5
|
+
* Supports disk persistence for pre-computed graphs
|
|
5
6
|
*/
|
|
7
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
8
|
+
if (k2 === undefined) k2 = k;
|
|
9
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
10
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
11
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
12
|
+
}
|
|
13
|
+
Object.defineProperty(o, k2, desc);
|
|
14
|
+
}) : (function(o, m, k, k2) {
|
|
15
|
+
if (k2 === undefined) k2 = k;
|
|
16
|
+
o[k2] = m[k];
|
|
17
|
+
}));
|
|
18
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
19
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
20
|
+
}) : function(o, v) {
|
|
21
|
+
o["default"] = v;
|
|
22
|
+
});
|
|
23
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
24
|
+
var ownKeys = function(o) {
|
|
25
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
26
|
+
var ar = [];
|
|
27
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
28
|
+
return ar;
|
|
29
|
+
};
|
|
30
|
+
return ownKeys(o);
|
|
31
|
+
};
|
|
32
|
+
return function (mod) {
|
|
33
|
+
if (mod && mod.__esModule) return mod;
|
|
34
|
+
var result = {};
|
|
35
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
36
|
+
__setModuleDefault(result, mod);
|
|
37
|
+
return result;
|
|
38
|
+
};
|
|
39
|
+
})();
|
|
6
40
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
41
|
exports.graphCache = exports.GraphCache = void 0;
|
|
8
42
|
exports.buildIndexes = buildIndexes;
|
|
9
43
|
exports.normalizePath = normalizePath;
|
|
10
|
-
exports.
|
|
44
|
+
exports.saveCacheToDisk = saveCacheToDisk;
|
|
45
|
+
exports.loadCacheFromDisk = loadCacheFromDisk;
|
|
46
|
+
exports.sanitizeFileName = sanitizeFileName;
|
|
47
|
+
exports.setRepoMap = setRepoMap;
|
|
48
|
+
exports.setNoApiFallback = setNoApiFallback;
|
|
49
|
+
exports.resolveOrFetchGraph = resolveOrFetchGraph;
|
|
50
|
+
exports.precacheForDirectory = precacheForDirectory;
|
|
11
51
|
const constants_1 = require("../constants");
|
|
52
|
+
const api_helpers_1 = require("../utils/api-helpers");
|
|
53
|
+
const zip_repository_1 = require("../utils/zip-repository");
|
|
54
|
+
const fs_1 = require("fs");
|
|
55
|
+
const path_1 = require("path");
|
|
56
|
+
const buffer_1 = require("buffer");
|
|
57
|
+
const logger = __importStar(require("../utils/logger"));
|
|
12
58
|
/**
|
|
13
59
|
* Build indexes from raw SupermodelIR response
|
|
14
60
|
*/
|
|
@@ -179,21 +225,6 @@ function buildIndexes(raw, cacheKey) {
|
|
|
179
225
|
function normalizePath(path) {
|
|
180
226
|
return path.replace(/\\/g, '/');
|
|
181
227
|
}
|
|
182
|
-
/**
|
|
183
|
-
* Convert full node to lightweight descriptor
|
|
184
|
-
*/
|
|
185
|
-
function toNodeDescriptor(node) {
|
|
186
|
-
const props = node.properties || {};
|
|
187
|
-
return {
|
|
188
|
-
id: node.id,
|
|
189
|
-
labels: node.labels || [],
|
|
190
|
-
name: props.name,
|
|
191
|
-
filePath: props.filePath,
|
|
192
|
-
startLine: props.startLine,
|
|
193
|
-
endLine: props.endLine,
|
|
194
|
-
kind: props.kind,
|
|
195
|
-
};
|
|
196
|
-
}
|
|
197
228
|
/**
|
|
198
229
|
* LRU Cache for indexed graphs
|
|
199
230
|
*/
|
|
@@ -285,5 +316,286 @@ class GraphCache {
|
|
|
285
316
|
}
|
|
286
317
|
}
|
|
287
318
|
exports.GraphCache = GraphCache;
|
|
319
|
+
/**
|
|
320
|
+
* Save a SupermodelIR to disk for later use as a pre-computed cache.
|
|
321
|
+
* Stores as JSON with a metadata wrapper.
|
|
322
|
+
*/
|
|
323
|
+
async function saveCacheToDisk(cacheDir, repoName, raw, commitHash) {
|
|
324
|
+
await fs_1.promises.mkdir(cacheDir, { recursive: true });
|
|
325
|
+
const fileName = `${sanitizeFileName(repoName)}.json`;
|
|
326
|
+
const filePath = (0, path_1.join)(cacheDir, fileName);
|
|
327
|
+
const payload = {
|
|
328
|
+
version: 1,
|
|
329
|
+
repoName,
|
|
330
|
+
commitHash: commitHash || null,
|
|
331
|
+
savedAt: new Date().toISOString(),
|
|
332
|
+
raw,
|
|
333
|
+
};
|
|
334
|
+
await fs_1.promises.writeFile(filePath, JSON.stringify(payload));
|
|
335
|
+
return filePath;
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Load all pre-computed graphs from a cache directory into the GraphCache.
|
|
339
|
+
* Returns a Map of repoName -> IndexedGraph for repo auto-detection.
|
|
340
|
+
*/
|
|
341
|
+
async function loadCacheFromDisk(cacheDir, cache) {
|
|
342
|
+
const repoMap = new Map();
|
|
343
|
+
let entries;
|
|
344
|
+
try {
|
|
345
|
+
entries = await fs_1.promises.readdir(cacheDir);
|
|
346
|
+
}
|
|
347
|
+
catch (error) {
|
|
348
|
+
if (error.code === 'ENOENT') {
|
|
349
|
+
logger.debug('Cache directory does not exist:', cacheDir);
|
|
350
|
+
return repoMap;
|
|
351
|
+
}
|
|
352
|
+
throw error;
|
|
353
|
+
}
|
|
354
|
+
const jsonFiles = entries.filter(e => e.endsWith('.json'));
|
|
355
|
+
logger.debug(`Found ${jsonFiles.length} cache files in ${cacheDir}`);
|
|
356
|
+
for (const file of jsonFiles) {
|
|
357
|
+
try {
|
|
358
|
+
const filePath = (0, path_1.join)(cacheDir, file);
|
|
359
|
+
const content = await fs_1.promises.readFile(filePath, 'utf-8');
|
|
360
|
+
const payload = JSON.parse(content);
|
|
361
|
+
if (!payload.raw || !payload.repoName) {
|
|
362
|
+
logger.warn(`Skipping invalid cache file: ${file}`);
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
const repoName = payload.repoName;
|
|
366
|
+
const cacheKey = `precache:${repoName}`;
|
|
367
|
+
const graph = buildIndexes(payload.raw, cacheKey);
|
|
368
|
+
cache.set(cacheKey, graph);
|
|
369
|
+
repoMap.set(repoName.toLowerCase(), graph);
|
|
370
|
+
// Index by commit hash for exact matching (e.g. "commit:abc1234")
|
|
371
|
+
const commitHash = payload.commitHash;
|
|
372
|
+
if (commitHash) {
|
|
373
|
+
repoMap.set(`commit:${commitHash}`, graph);
|
|
374
|
+
}
|
|
375
|
+
// Also store common variants of the repo name for matching
|
|
376
|
+
// e.g. "django" for "django__django", "astropy" for "astropy__astropy"
|
|
377
|
+
const parts = repoName.toLowerCase().split(/[_\-\/]/);
|
|
378
|
+
for (const part of parts) {
|
|
379
|
+
if (part && part.length > 2 && !repoMap.has(part)) {
|
|
380
|
+
repoMap.set(part, graph);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
logger.debug(`Loaded pre-computed graph for ${repoName} (commit: ${commitHash || 'unknown'}): ${graph.summary.nodeCount} nodes`);
|
|
384
|
+
}
|
|
385
|
+
catch (error) {
|
|
386
|
+
logger.warn(`Failed to load cache file ${file}: ${error.message}`);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
return repoMap;
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Detect which pre-computed graph matches a given directory.
|
|
393
|
+
* Tries: git remote name, directory basename, parent directory name.
|
|
394
|
+
*/
|
|
395
|
+
function detectRepo(directory, repoMap) {
|
|
396
|
+
if (repoMap.size === 0)
|
|
397
|
+
return null;
|
|
398
|
+
// Strategy 0 (highest priority): Match by exact commit hash
|
|
399
|
+
try {
|
|
400
|
+
const { execSync } = require('child_process');
|
|
401
|
+
const commitHash = execSync('git rev-parse --short HEAD', {
|
|
402
|
+
cwd: directory, encoding: 'utf-8', timeout: 2000,
|
|
403
|
+
}).trim();
|
|
404
|
+
if (commitHash && repoMap.has(`commit:${commitHash}`)) {
|
|
405
|
+
return repoMap.get(`commit:${commitHash}`);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
catch {
|
|
409
|
+
// Not a git repo
|
|
410
|
+
}
|
|
411
|
+
// Strategy 1: Try directory basename
|
|
412
|
+
const dirName = (0, path_1.basename)(directory).toLowerCase();
|
|
413
|
+
if (repoMap.has(dirName)) {
|
|
414
|
+
return repoMap.get(dirName);
|
|
415
|
+
}
|
|
416
|
+
// Strategy 2: Try git remote (sync, best-effort)
|
|
417
|
+
try {
|
|
418
|
+
const { execSync } = require('child_process');
|
|
419
|
+
const remote = execSync('git remote get-url origin', {
|
|
420
|
+
cwd: directory,
|
|
421
|
+
encoding: 'utf-8',
|
|
422
|
+
timeout: 2000,
|
|
423
|
+
}).trim();
|
|
424
|
+
// Extract repo name from URL: "https://github.com/django/django.git" -> "django"
|
|
425
|
+
const match = remote.match(/\/([^\/]+?)(?:\.git)?$/);
|
|
426
|
+
if (match) {
|
|
427
|
+
const repoName = match[1].toLowerCase();
|
|
428
|
+
if (repoMap.has(repoName)) {
|
|
429
|
+
return repoMap.get(repoName);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
// Try org/repo format: "django/django" -> try "django"
|
|
433
|
+
const orgMatch = remote.match(/\/([^\/]+)\/([^\/]+?)(?:\.git)?$/);
|
|
434
|
+
if (orgMatch) {
|
|
435
|
+
const orgName = orgMatch[1].toLowerCase();
|
|
436
|
+
const repoName = orgMatch[2].toLowerCase();
|
|
437
|
+
// Try "org__repo" format (swe-bench style)
|
|
438
|
+
const sweKey = `${orgName}__${repoName}`;
|
|
439
|
+
if (repoMap.has(sweKey)) {
|
|
440
|
+
return repoMap.get(sweKey);
|
|
441
|
+
}
|
|
442
|
+
if (repoMap.has(orgName)) {
|
|
443
|
+
return repoMap.get(orgName);
|
|
444
|
+
}
|
|
445
|
+
if (repoMap.has(repoName)) {
|
|
446
|
+
return repoMap.get(repoName);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
catch {
|
|
451
|
+
// Not a git repo or git not available -- that's fine
|
|
452
|
+
}
|
|
453
|
+
// Strategy 3: If there's only one cached graph, use it (common in SWE-bench)
|
|
454
|
+
if (repoMap.size <= 3) {
|
|
455
|
+
// Small map likely has only one real repo with name variants
|
|
456
|
+
const uniqueGraphs = new Set([...repoMap.values()]);
|
|
457
|
+
if (uniqueGraphs.size === 1) {
|
|
458
|
+
return [...uniqueGraphs][0];
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
return null;
|
|
462
|
+
}
|
|
463
|
+
function sanitizeFileName(name) {
|
|
464
|
+
return name.replace(/[^a-zA-Z0-9_\-]/g, '_');
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* Detect the repo name from a directory (git remote or basename).
|
|
468
|
+
*/
|
|
469
|
+
function detectRepoName(directory) {
|
|
470
|
+
try {
|
|
471
|
+
const { execSync } = require('child_process');
|
|
472
|
+
const remote = execSync('git remote get-url origin', {
|
|
473
|
+
cwd: directory,
|
|
474
|
+
encoding: 'utf-8',
|
|
475
|
+
timeout: 2000,
|
|
476
|
+
}).trim();
|
|
477
|
+
const match = remote.match(/\/([^\/]+?)(?:\.git)?$/);
|
|
478
|
+
if (match)
|
|
479
|
+
return match[1];
|
|
480
|
+
}
|
|
481
|
+
catch {
|
|
482
|
+
// Not a git repo
|
|
483
|
+
}
|
|
484
|
+
return (0, path_1.basename)(directory);
|
|
485
|
+
}
|
|
288
486
|
// Global cache instance
|
|
289
487
|
exports.graphCache = new GraphCache();
|
|
488
|
+
// Repo map for pre-computed graphs (populated at startup)
|
|
489
|
+
let _repoMap = new Map();
|
|
490
|
+
let _noApiFallback = false;
|
|
491
|
+
function setRepoMap(map) {
|
|
492
|
+
_repoMap = map;
|
|
493
|
+
}
|
|
494
|
+
function setNoApiFallback(enabled) {
|
|
495
|
+
_noApiFallback = enabled;
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* Resolve a graph for a directory: pre-computed cache → LRU cache → API fallback.
|
|
499
|
+
* When --no-api-fallback is set, throws instead of calling the API.
|
|
500
|
+
*/
|
|
501
|
+
async function resolveOrFetchGraph(client, directory) {
|
|
502
|
+
// 1. Pre-computed cache
|
|
503
|
+
const precomputed = detectRepo(directory, _repoMap);
|
|
504
|
+
if (precomputed)
|
|
505
|
+
return precomputed;
|
|
506
|
+
// 2. LRU cache
|
|
507
|
+
const idempotencyKey = (0, api_helpers_1.generateIdempotencyKey)(directory);
|
|
508
|
+
const cached = exports.graphCache.get(idempotencyKey);
|
|
509
|
+
if (cached)
|
|
510
|
+
return cached;
|
|
511
|
+
// 3. Fast-fail when API fallback is disabled (e.g. SWE-bench)
|
|
512
|
+
if (_noApiFallback) {
|
|
513
|
+
throw {
|
|
514
|
+
response: null,
|
|
515
|
+
request: null,
|
|
516
|
+
message: 'No pre-computed graph available for this repository. Use grep, find, and file reading to explore the codebase instead.',
|
|
517
|
+
code: 'NO_CACHE',
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
// 4. API fallback
|
|
521
|
+
console.error('[Supermodel] Generating codebase graph (this may take a few minutes)...');
|
|
522
|
+
const zipResult = await (0, zip_repository_1.zipRepository)(directory);
|
|
523
|
+
let progressInterval = null;
|
|
524
|
+
let elapsed = 0;
|
|
525
|
+
progressInterval = setInterval(() => {
|
|
526
|
+
elapsed += 15;
|
|
527
|
+
console.error(`[Supermodel] Analysis in progress... (${elapsed}s elapsed)`);
|
|
528
|
+
}, 15000);
|
|
529
|
+
try {
|
|
530
|
+
const fileBuffer = await fs_1.promises.readFile(zipResult.path);
|
|
531
|
+
const fileBlob = new buffer_1.Blob([fileBuffer], { type: 'application/zip' });
|
|
532
|
+
const response = await client.graphs.generateSupermodelGraph(fileBlob, { idempotencyKey });
|
|
533
|
+
const graph = buildIndexes(response, idempotencyKey);
|
|
534
|
+
exports.graphCache.set(idempotencyKey, graph);
|
|
535
|
+
console.error('[Supermodel] Analysis complete.');
|
|
536
|
+
return graph;
|
|
537
|
+
}
|
|
538
|
+
finally {
|
|
539
|
+
if (progressInterval)
|
|
540
|
+
clearInterval(progressInterval);
|
|
541
|
+
await zipResult.cleanup();
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* Pre-compute and cache a graph for a directory during server startup.
|
|
546
|
+
* If a cache already exists (in repoMap or on disk), this is a no-op.
|
|
547
|
+
* Otherwise calls the API, saves to cacheDir for cross-container persistence,
|
|
548
|
+
* and updates the in-memory repoMap.
|
|
549
|
+
*
|
|
550
|
+
* The Supermodel API has server-side idempotency caching, so repeated calls
|
|
551
|
+
* with the same idempotency key (same repo + commit) return instantly.
|
|
552
|
+
*/
|
|
553
|
+
async function precacheForDirectory(client, directory, cacheDir) {
|
|
554
|
+
// Already cached?
|
|
555
|
+
if (detectRepo(directory, _repoMap)) {
|
|
556
|
+
logger.debug('Graph already cached for', directory);
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
logger.info('Pre-computing graph for', directory, '(first run for this repo; subsequent runs will be instant)...');
|
|
560
|
+
const idempotencyKey = (0, api_helpers_1.generateIdempotencyKey)(directory);
|
|
561
|
+
const repoName = detectRepoName(directory);
|
|
562
|
+
const zipResult = await (0, zip_repository_1.zipRepository)(directory);
|
|
563
|
+
let progressInterval = null;
|
|
564
|
+
let elapsed = 0;
|
|
565
|
+
progressInterval = setInterval(() => {
|
|
566
|
+
elapsed += 15;
|
|
567
|
+
logger.info(`Analysis in progress... (${elapsed}s elapsed)`);
|
|
568
|
+
}, 15000);
|
|
569
|
+
try {
|
|
570
|
+
const fileBuffer = await fs_1.promises.readFile(zipResult.path);
|
|
571
|
+
const fileBlob = new buffer_1.Blob([fileBuffer], { type: 'application/zip' });
|
|
572
|
+
const response = await client.graphs.generateSupermodelGraph(fileBlob, { idempotencyKey });
|
|
573
|
+
const graph = buildIndexes(response, `precache:${repoName}`);
|
|
574
|
+
// Update in-memory caches
|
|
575
|
+
exports.graphCache.set(idempotencyKey, graph);
|
|
576
|
+
_repoMap.set(repoName.toLowerCase(), graph);
|
|
577
|
+
const parts = repoName.toLowerCase().split(/[_\-\/]/);
|
|
578
|
+
for (const part of parts) {
|
|
579
|
+
if (part && part.length > 2 && !_repoMap.has(part)) {
|
|
580
|
+
_repoMap.set(part, graph);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
// Persist to disk for cross-container reuse
|
|
584
|
+
if (cacheDir) {
|
|
585
|
+
try {
|
|
586
|
+
const savedPath = await saveCacheToDisk(cacheDir, repoName, response);
|
|
587
|
+
logger.info('Saved graph to:', savedPath);
|
|
588
|
+
}
|
|
589
|
+
catch (err) {
|
|
590
|
+
// Non-fatal: cache dir might be read-only or full
|
|
591
|
+
logger.warn('Failed to persist graph to disk:', err.message);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
logger.info(`Pre-compute complete: ${graph.summary.nodeCount} nodes, ${graph.summary.relationshipCount} relationships`);
|
|
595
|
+
}
|
|
596
|
+
finally {
|
|
597
|
+
if (progressInterval)
|
|
598
|
+
clearInterval(progressInterval);
|
|
599
|
+
await zipResult.cleanup();
|
|
600
|
+
}
|
|
601
|
+
}
|
package/dist/constants.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Single source of truth for configuration values
|
|
5
5
|
*/
|
|
6
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
-
exports.
|
|
7
|
+
exports.MAX_SYMBOL_RELATED = exports.MAX_SYMBOL_CALLEES = exports.MAX_SYMBOL_CALLERS = exports.MAX_OVERVIEW_HUB_FUNCTIONS = exports.MAX_OVERVIEW_DOMAINS = exports.DEFAULT_CACHE_TTL_MS = exports.DEFAULT_MAX_NODES = exports.DEFAULT_MAX_GRAPHS = exports.MAX_ZIP_SIZE_BYTES = exports.ZIP_CLEANUP_AGE_MS = exports.CONNECTION_TIMEOUT_MS = exports.DEFAULT_API_TIMEOUT_MS = void 0;
|
|
8
8
|
// HTTP timeout configuration
|
|
9
9
|
exports.DEFAULT_API_TIMEOUT_MS = 900_000; // 15 minutes (complex repos can take 10+ min)
|
|
10
10
|
exports.CONNECTION_TIMEOUT_MS = 30_000; // 30 seconds to establish connection
|
|
@@ -15,6 +15,10 @@ exports.MAX_ZIP_SIZE_BYTES = 500 * 1024 * 1024; // 500MB default
|
|
|
15
15
|
exports.DEFAULT_MAX_GRAPHS = 20; // Maximum number of graphs to cache
|
|
16
16
|
exports.DEFAULT_MAX_NODES = 1_000_000; // Maximum total nodes across all cached graphs
|
|
17
17
|
exports.DEFAULT_CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour - time-to-live for cached graphs
|
|
18
|
-
//
|
|
19
|
-
exports.
|
|
20
|
-
exports.
|
|
18
|
+
// Overview tool output limits
|
|
19
|
+
exports.MAX_OVERVIEW_DOMAINS = 10; // Top N domains to show in overview
|
|
20
|
+
exports.MAX_OVERVIEW_HUB_FUNCTIONS = 10; // Top N hub functions to show
|
|
21
|
+
// Symbol context tool output limits
|
|
22
|
+
exports.MAX_SYMBOL_CALLERS = 10; // Top N callers to show
|
|
23
|
+
exports.MAX_SYMBOL_CALLEES = 10; // Top N callees to show
|
|
24
|
+
exports.MAX_SYMBOL_RELATED = 8; // Related symbols in same file
|
package/dist/index.js
CHANGED
|
@@ -36,26 +36,184 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
36
36
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
37
37
|
/**
|
|
38
38
|
* Entry point for the Supermodel MCP Server.
|
|
39
|
-
*
|
|
39
|
+
*
|
|
40
|
+
* Usage:
|
|
41
|
+
* node dist/index.js [workdir] [--no-api-fallback] -- Start MCP server
|
|
42
|
+
* node dist/index.js precache <dir> [--output-dir <dir>] -- Pre-compute graph for a repo
|
|
43
|
+
*
|
|
40
44
|
* @module index
|
|
41
45
|
*/
|
|
42
46
|
const server_1 = require("./server");
|
|
43
47
|
const logger = __importStar(require("./utils/logger"));
|
|
44
|
-
/**
|
|
45
|
-
* Main entry point that initializes and starts the MCP server.
|
|
46
|
-
* Accepts an optional workdir argument from the command line.
|
|
47
|
-
*/
|
|
48
48
|
async function main() {
|
|
49
|
-
// Parse command-line arguments to get optional default workdir
|
|
50
|
-
// Usage: node dist/index.js [workdir]
|
|
51
49
|
const args = process.argv.slice(2);
|
|
52
|
-
|
|
50
|
+
// Handle precache subcommand
|
|
51
|
+
if (args[0] === 'precache') {
|
|
52
|
+
await handlePrecache(args.slice(1));
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
// Normal MCP server mode — parse flags
|
|
56
|
+
let defaultWorkdir;
|
|
57
|
+
let noApiFallback = !!process.env.SUPERMODEL_NO_API_FALLBACK;
|
|
58
|
+
let precache = false;
|
|
59
|
+
for (const arg of args) {
|
|
60
|
+
if (arg === '--no-api-fallback') {
|
|
61
|
+
noApiFallback = true;
|
|
62
|
+
}
|
|
63
|
+
else if (arg === '--precache') {
|
|
64
|
+
precache = true;
|
|
65
|
+
}
|
|
66
|
+
else if (!arg.startsWith('--')) {
|
|
67
|
+
defaultWorkdir = arg;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
53
70
|
if (defaultWorkdir) {
|
|
54
71
|
logger.debug('Default workdir:', defaultWorkdir);
|
|
55
72
|
}
|
|
56
|
-
|
|
73
|
+
if (noApiFallback) {
|
|
74
|
+
logger.debug('API fallback disabled (cache-only mode)');
|
|
75
|
+
}
|
|
76
|
+
if (precache) {
|
|
77
|
+
logger.debug('Startup precaching enabled');
|
|
78
|
+
}
|
|
79
|
+
const server = new server_1.Server(defaultWorkdir, { noApiFallback, precache });
|
|
57
80
|
await server.start();
|
|
58
81
|
}
|
|
82
|
+
async function handlePrecache(args) {
|
|
83
|
+
if (args.length === 0) {
|
|
84
|
+
console.error('Usage: supermodel-mcp precache <directory> [--output-dir <dir>] [--name <repo-name>]');
|
|
85
|
+
console.error('');
|
|
86
|
+
console.error('Pre-compute a code graph for a repository and save it to disk.');
|
|
87
|
+
console.error('');
|
|
88
|
+
console.error('Options:');
|
|
89
|
+
console.error(' --output-dir <dir> Directory to save the cache file (default: ./supermodel-cache)');
|
|
90
|
+
console.error(' --name <name> Repository name for the cache key (default: directory basename)');
|
|
91
|
+
console.error('');
|
|
92
|
+
console.error('Environment:');
|
|
93
|
+
console.error(' SUPERMODEL_API_KEY Required. API key for the Supermodel service.');
|
|
94
|
+
console.error(' SUPERMODEL_CACHE_DIR Alternative to --output-dir.');
|
|
95
|
+
process.exit(1);
|
|
96
|
+
}
|
|
97
|
+
// Parse args
|
|
98
|
+
let directory = '';
|
|
99
|
+
let outputDir = process.env.SUPERMODEL_CACHE_DIR || './supermodel-cache';
|
|
100
|
+
let repoName = '';
|
|
101
|
+
for (let i = 0; i < args.length; i++) {
|
|
102
|
+
if (args[i] === '--output-dir' && i + 1 < args.length) {
|
|
103
|
+
outputDir = args[++i];
|
|
104
|
+
}
|
|
105
|
+
else if (args[i] === '--name' && i + 1 < args.length) {
|
|
106
|
+
repoName = args[++i];
|
|
107
|
+
}
|
|
108
|
+
else if (!args[i].startsWith('--')) {
|
|
109
|
+
directory = args[i];
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (!directory) {
|
|
113
|
+
console.error('Error: directory argument is required');
|
|
114
|
+
process.exit(1);
|
|
115
|
+
}
|
|
116
|
+
if (!process.env.SUPERMODEL_API_KEY) {
|
|
117
|
+
console.error('Error: SUPERMODEL_API_KEY environment variable is required');
|
|
118
|
+
process.exit(1);
|
|
119
|
+
}
|
|
120
|
+
const { resolve, basename, join } = require('path');
|
|
121
|
+
const { execSync } = require('child_process');
|
|
122
|
+
const { existsSync } = require('fs');
|
|
123
|
+
const resolvedDir = resolve(directory);
|
|
124
|
+
// Detect repo name from git remote, falling back to directory basename
|
|
125
|
+
let detectedName = basename(resolvedDir);
|
|
126
|
+
try {
|
|
127
|
+
const remote = execSync('git remote get-url origin', {
|
|
128
|
+
cwd: resolvedDir, encoding: 'utf-8', timeout: 2000,
|
|
129
|
+
}).trim();
|
|
130
|
+
const match = remote.match(/\/([^\/]+?)(?:\.git)?$/);
|
|
131
|
+
if (match)
|
|
132
|
+
detectedName = match[1];
|
|
133
|
+
}
|
|
134
|
+
catch { }
|
|
135
|
+
// Get commit hash for commit-specific caching
|
|
136
|
+
let commitHash = '';
|
|
137
|
+
try {
|
|
138
|
+
commitHash = execSync('git rev-parse --short HEAD', {
|
|
139
|
+
cwd: resolvedDir, encoding: 'utf-8', timeout: 2000,
|
|
140
|
+
}).trim();
|
|
141
|
+
}
|
|
142
|
+
catch { }
|
|
143
|
+
const name = repoName || (commitHash ? `${detectedName}_${commitHash}` : detectedName);
|
|
144
|
+
// Check if cache file already exists (skip redundant API calls)
|
|
145
|
+
const { saveCacheToDisk, buildIndexes, sanitizeFileName } = require('./cache/graph-cache');
|
|
146
|
+
const expectedPath = join(outputDir, `${sanitizeFileName(name)}.json`);
|
|
147
|
+
if (existsSync(expectedPath)) {
|
|
148
|
+
console.error(`Cache already exists: ${expectedPath}`);
|
|
149
|
+
console.error('Skipping precache (graph already generated for this commit).');
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
console.error(`Pre-computing graph for: ${resolvedDir}`);
|
|
153
|
+
console.error(`Repository name: ${detectedName}, commit: ${commitHash || 'unknown'}`);
|
|
154
|
+
console.error(`Cache file: ${expectedPath}`);
|
|
155
|
+
console.error('');
|
|
156
|
+
// Import what we need
|
|
157
|
+
const { Configuration, DefaultApi, SupermodelClient } = require('@supermodeltools/sdk');
|
|
158
|
+
const { zipRepository } = require('./utils/zip-repository');
|
|
159
|
+
const { readFile } = require('fs/promises');
|
|
160
|
+
const { Blob } = require('buffer');
|
|
161
|
+
const { generateIdempotencyKey } = require('./utils/api-helpers');
|
|
162
|
+
const { Agent } = require('undici');
|
|
163
|
+
const { DEFAULT_API_TIMEOUT_MS, CONNECTION_TIMEOUT_MS } = require('./constants');
|
|
164
|
+
const parsedTimeout = parseInt(process.env.SUPERMODEL_TIMEOUT_MS || '', 10);
|
|
165
|
+
const timeoutMs = Number.isFinite(parsedTimeout) && parsedTimeout > 0
|
|
166
|
+
? parsedTimeout
|
|
167
|
+
: DEFAULT_API_TIMEOUT_MS;
|
|
168
|
+
const httpAgent = new Agent({
|
|
169
|
+
headersTimeout: timeoutMs,
|
|
170
|
+
bodyTimeout: timeoutMs,
|
|
171
|
+
connectTimeout: CONNECTION_TIMEOUT_MS,
|
|
172
|
+
});
|
|
173
|
+
const fetchWithTimeout = (url, init) => {
|
|
174
|
+
return fetch(url, { ...init, dispatcher: httpAgent });
|
|
175
|
+
};
|
|
176
|
+
const config = new Configuration({
|
|
177
|
+
basePath: process.env.SUPERMODEL_BASE_URL || 'https://api.supermodeltools.com',
|
|
178
|
+
apiKey: process.env.SUPERMODEL_API_KEY,
|
|
179
|
+
fetchApi: fetchWithTimeout,
|
|
180
|
+
});
|
|
181
|
+
const api = new DefaultApi(config);
|
|
182
|
+
const client = new SupermodelClient(api);
|
|
183
|
+
// Step 1: Zip
|
|
184
|
+
console.error('Step 1/3: Creating ZIP archive...');
|
|
185
|
+
const zipResult = await zipRepository(resolvedDir);
|
|
186
|
+
console.error(` ZIP created: ${zipResult.fileCount} files, ${(zipResult.sizeBytes / 1024 / 1024).toFixed(1)} MB`);
|
|
187
|
+
// Step 2: API call
|
|
188
|
+
console.error('Step 2/3: Analyzing codebase (this may take several minutes)...');
|
|
189
|
+
const idempotencyKey = generateIdempotencyKey(resolvedDir);
|
|
190
|
+
let progressInterval = null;
|
|
191
|
+
let elapsed = 0;
|
|
192
|
+
progressInterval = setInterval(() => {
|
|
193
|
+
elapsed += 15;
|
|
194
|
+
console.error(` Analysis in progress... (${elapsed}s elapsed)`);
|
|
195
|
+
}, 15000);
|
|
196
|
+
let response;
|
|
197
|
+
try {
|
|
198
|
+
const fileBuffer = await readFile(zipResult.path);
|
|
199
|
+
const fileBlob = new Blob([fileBuffer], { type: 'application/zip' });
|
|
200
|
+
response = await client.generateSupermodelGraph(fileBlob, { idempotencyKey });
|
|
201
|
+
}
|
|
202
|
+
finally {
|
|
203
|
+
if (progressInterval)
|
|
204
|
+
clearInterval(progressInterval);
|
|
205
|
+
await zipResult.cleanup();
|
|
206
|
+
}
|
|
207
|
+
const graph = buildIndexes(response, `precache:${name}`);
|
|
208
|
+
console.error(` Analysis complete: ${graph.summary.nodeCount} nodes, ${graph.summary.relationshipCount} relationships`);
|
|
209
|
+
console.error(` Files: ${graph.summary.filesProcessed}, Functions: ${graph.summary.functions}, Classes: ${graph.summary.classes}`);
|
|
210
|
+
// Step 3: Save to disk
|
|
211
|
+
console.error('Step 3/3: Saving to disk...');
|
|
212
|
+
const savedPath = await saveCacheToDisk(outputDir, name, response, commitHash || undefined);
|
|
213
|
+
console.error(` Saved to: ${savedPath}`);
|
|
214
|
+
console.error('');
|
|
215
|
+
console.error('Done! To use this cache, set SUPERMODEL_CACHE_DIR=' + outputDir);
|
|
216
|
+
}
|
|
59
217
|
main().catch((error) => {
|
|
60
218
|
logger.error('Fatal error:', error);
|
|
61
219
|
process.exit(1);
|