@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.
@@ -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.toNodeDescriptor = toNodeDescriptor;
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
+ }
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  /**
3
- * Type definitions for Supermodel IR graph data
4
- * Mirrors the API response schema
3
+ * Re-export graph types from the SDK.
4
+ * No local copies -- single source of truth.
5
5
  */
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
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.MAX_NEIGHBORHOOD_DEPTH = exports.DEFAULT_QUERY_LIMIT = 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;
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
- // Query defaults
19
- exports.DEFAULT_QUERY_LIMIT = 200; // Default result limit for queries
20
- exports.MAX_NEIGHBORHOOD_DEPTH = 3; // Maximum traversal depth for neighborhood queries
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
- * Starts the MCP server with optional default working directory.
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
- const defaultWorkdir = args.length > 0 ? args[0] : undefined;
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
- const server = new server_1.Server(defaultWorkdir);
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);