typegraph-mcp 0.9.19 → 0.9.21

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/cli.js CHANGED
@@ -13,11 +13,11 @@ var __export = (target, all) => {
13
13
  import * as path from "path";
14
14
  function resolveConfig(toolDir) {
15
15
  const cwd = process.cwd();
16
- const projectRoot2 = process.env["TYPEGRAPH_PROJECT_ROOT"] ? path.resolve(cwd, process.env["TYPEGRAPH_PROJECT_ROOT"]) : path.basename(path.dirname(toolDir)) === "plugins" ? path.resolve(toolDir, "../..") : cwd;
17
- const tsconfigPath2 = process.env["TYPEGRAPH_TSCONFIG"] || "./tsconfig.json";
18
- const toolIsEmbedded = toolDir.startsWith(projectRoot2 + path.sep);
19
- const toolRelPath = toolIsEmbedded ? path.relative(projectRoot2, toolDir) : toolDir;
20
- return { projectRoot: projectRoot2, tsconfigPath: tsconfigPath2, toolDir, toolIsEmbedded, toolRelPath };
16
+ const projectRoot3 = process.env["TYPEGRAPH_PROJECT_ROOT"] ? path.resolve(cwd, process.env["TYPEGRAPH_PROJECT_ROOT"]) : path.basename(path.dirname(toolDir)) === "plugins" ? path.resolve(toolDir, "../..") : cwd;
17
+ const tsconfigPath3 = process.env["TYPEGRAPH_TSCONFIG"] || "./tsconfig.json";
18
+ const toolIsEmbedded = toolDir.startsWith(projectRoot3 + path.sep);
19
+ const toolRelPath = toolIsEmbedded ? path.relative(projectRoot3, toolDir) : toolDir;
20
+ return { projectRoot: projectRoot3, tsconfigPath: tsconfigPath3, toolDir, toolIsEmbedded, toolRelPath };
21
21
  }
22
22
  var init_config = __esm({
23
23
  "config.ts"() {
@@ -111,36 +111,36 @@ function parseFileImports(filePath, source) {
111
111
  }
112
112
  return imports;
113
113
  }
114
- function distToSource(resolvedPath, projectRoot2) {
115
- if (!resolvedPath.startsWith(projectRoot2)) return resolvedPath;
116
- const rel2 = path2.relative(projectRoot2, resolvedPath);
114
+ function distToSource(resolvedPath, projectRoot3) {
115
+ if (!resolvedPath.startsWith(projectRoot3)) return resolvedPath;
116
+ const rel2 = path2.relative(projectRoot3, resolvedPath);
117
117
  const distIdx = rel2.indexOf("dist" + path2.sep);
118
118
  if (distIdx === -1) return resolvedPath;
119
119
  const prefix = rel2.slice(0, distIdx);
120
120
  const afterDist = rel2.slice(distIdx + 5);
121
121
  const withoutExt = afterDist.replace(/\.(m?j|c)s$/, "");
122
122
  for (const ext of SOURCE_EXTS) {
123
- const candidate = path2.resolve(projectRoot2, prefix, "src", withoutExt + ext);
123
+ const candidate = path2.resolve(projectRoot3, prefix, "src", withoutExt + ext);
124
124
  if (fs.existsSync(candidate)) return candidate;
125
125
  }
126
126
  for (const ext of SOURCE_EXTS) {
127
- const candidate = path2.resolve(projectRoot2, prefix, withoutExt + ext);
127
+ const candidate = path2.resolve(projectRoot3, prefix, withoutExt + ext);
128
128
  if (fs.existsSync(candidate)) return candidate;
129
129
  }
130
130
  if (withoutExt.endsWith("/index")) {
131
131
  const dirPath = withoutExt.slice(0, -6);
132
132
  for (const ext of SOURCE_EXTS) {
133
- const candidate = path2.resolve(projectRoot2, prefix, "src", dirPath + ext);
133
+ const candidate = path2.resolve(projectRoot3, prefix, "src", dirPath + ext);
134
134
  if (fs.existsSync(candidate)) return candidate;
135
135
  }
136
136
  }
137
137
  return resolvedPath;
138
138
  }
139
- function resolveImport(resolver, fromDir, specifier, projectRoot2) {
139
+ function resolveImport(resolver, fromDir, specifier, projectRoot3) {
140
140
  try {
141
141
  const result = resolver.sync(fromDir, specifier);
142
142
  if (result.path && !result.path.includes("node_modules")) {
143
- const mapped = distToSource(result.path, projectRoot2);
143
+ const mapped = distToSource(result.path, projectRoot3);
144
144
  const ext = path2.extname(mapped);
145
145
  if (!TS_EXTENSIONS.has(ext)) return null;
146
146
  if (SKIP_FILES.has(path2.basename(mapped))) return null;
@@ -150,10 +150,10 @@ function resolveImport(resolver, fromDir, specifier, projectRoot2) {
150
150
  }
151
151
  return null;
152
152
  }
153
- function createResolver(projectRoot2, tsconfigPath2) {
153
+ function createResolver(projectRoot3, tsconfigPath3) {
154
154
  return new ResolverFactory({
155
155
  tsconfig: {
156
- configFile: path2.resolve(projectRoot2, tsconfigPath2),
156
+ configFile: path2.resolve(projectRoot3, tsconfigPath3),
157
157
  references: "auto"
158
158
  },
159
159
  extensions: [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"],
@@ -167,7 +167,7 @@ function createResolver(projectRoot2, tsconfigPath2) {
167
167
  mainFields: ["module", "main"]
168
168
  });
169
169
  }
170
- function buildForwardEdges(files, resolver, projectRoot2) {
170
+ function buildForwardEdges(files, resolver, projectRoot3) {
171
171
  const forward = /* @__PURE__ */ new Map();
172
172
  const parseFailures = [];
173
173
  for (const filePath of files) {
@@ -187,7 +187,7 @@ function buildForwardEdges(files, resolver, projectRoot2) {
187
187
  const edges = [];
188
188
  const fromDir = path2.dirname(filePath);
189
189
  for (const raw of rawImports) {
190
- const target = resolveImport(resolver, fromDir, raw.specifier, projectRoot2);
190
+ const target = resolveImport(resolver, fromDir, raw.specifier, projectRoot3);
191
191
  if (target) {
192
192
  edges.push({
193
193
  target,
@@ -221,12 +221,12 @@ function buildReverseMap(forward) {
221
221
  }
222
222
  return reverse;
223
223
  }
224
- async function buildGraph(projectRoot2, tsconfigPath2) {
224
+ async function buildGraph(projectRoot3, tsconfigPath3) {
225
225
  const startTime = performance.now();
226
- const resolver = createResolver(projectRoot2, tsconfigPath2);
227
- const fileList = discoverFiles(projectRoot2);
226
+ const resolver = createResolver(projectRoot3, tsconfigPath3);
227
+ const fileList = discoverFiles(projectRoot3);
228
228
  log(`Discovered ${fileList.length} source files`);
229
- const { forward, parseFailures } = buildForwardEdges(fileList, resolver, projectRoot2);
229
+ const { forward, parseFailures } = buildForwardEdges(fileList, resolver, projectRoot3);
230
230
  const reverse = buildReverseMap(forward);
231
231
  const files = new Set(fileList);
232
232
  const edgeCount = [...forward.values()].reduce((sum, edges) => sum + edges.length, 0);
@@ -240,7 +240,7 @@ async function buildGraph(projectRoot2, tsconfigPath2) {
240
240
  resolver
241
241
  };
242
242
  }
243
- function updateFile(graph, filePath, resolver, projectRoot2) {
243
+ function updateFile(graph, filePath, resolver, projectRoot3) {
244
244
  const oldEdges = graph.forward.get(filePath) ?? [];
245
245
  for (const edge of oldEdges) {
246
246
  const revEdges = graph.reverse.get(edge.target);
@@ -268,7 +268,7 @@ function updateFile(graph, filePath, resolver, projectRoot2) {
268
268
  const fromDir = path2.dirname(filePath);
269
269
  const newEdges = [];
270
270
  for (const raw of rawImports) {
271
- const target = resolveImport(resolver, fromDir, raw.specifier, projectRoot2);
271
+ const target = resolveImport(resolver, fromDir, raw.specifier, projectRoot3);
272
272
  if (target) {
273
273
  newEdges.push({
274
274
  target,
@@ -316,10 +316,10 @@ function removeFile(graph, filePath) {
316
316
  graph.reverse.delete(filePath);
317
317
  graph.files.delete(filePath);
318
318
  }
319
- function startWatcher(projectRoot2, graph, resolver) {
319
+ function startWatcher(projectRoot3, graph, resolver) {
320
320
  try {
321
321
  const watcher = fs.watch(
322
- projectRoot2,
322
+ projectRoot3,
323
323
  { recursive: true },
324
324
  (_eventType, filename) => {
325
325
  if (!filename) return;
@@ -330,9 +330,9 @@ function startWatcher(projectRoot2, graph, resolver) {
330
330
  if (SKIP_FILES.has(path2.basename(filename))) return;
331
331
  if (filename.endsWith(".d.ts") || filename.endsWith(".d.mts") || filename.endsWith(".d.cts"))
332
332
  return;
333
- const absPath2 = path2.resolve(projectRoot2, filename);
333
+ const absPath2 = path2.resolve(projectRoot3, filename);
334
334
  if (fs.existsSync(absPath2)) {
335
- updateFile(graph, absPath2, resolver, projectRoot2);
335
+ updateFile(graph, absPath2, resolver, projectRoot3);
336
336
  } else {
337
337
  removeFile(graph, absPath2);
338
338
  }
@@ -394,23 +394,23 @@ function findFirstTsFile(dir) {
394
394
  }
395
395
  return null;
396
396
  }
397
- function testTsserver(projectRoot2) {
398
- return new Promise((resolve8) => {
397
+ function testTsserver(projectRoot3) {
398
+ return new Promise((resolve9) => {
399
399
  let tsserverPath;
400
400
  try {
401
- const require2 = createRequire(path3.resolve(projectRoot2, "package.json"));
401
+ const require2 = createRequire(path3.resolve(projectRoot3, "package.json"));
402
402
  tsserverPath = require2.resolve("typescript/lib/tsserver.js");
403
403
  } catch {
404
- resolve8(false);
404
+ resolve9(false);
405
405
  return;
406
406
  }
407
407
  const child = spawn("node", [tsserverPath, "--disableAutomaticTypingAcquisition"], {
408
- cwd: projectRoot2,
408
+ cwd: projectRoot3,
409
409
  stdio: ["pipe", "pipe", "pipe"]
410
410
  });
411
411
  const timeout = setTimeout(() => {
412
412
  child.kill();
413
- resolve8(false);
413
+ resolve9(false);
414
414
  }, 1e4);
415
415
  let buffer = "";
416
416
  child.stdout.on("data", (chunk) => {
@@ -418,12 +418,12 @@ function testTsserver(projectRoot2) {
418
418
  if (buffer.includes('"success":true')) {
419
419
  clearTimeout(timeout);
420
420
  child.kill();
421
- resolve8(true);
421
+ resolve9(true);
422
422
  }
423
423
  });
424
424
  child.on("error", () => {
425
425
  clearTimeout(timeout);
426
- resolve8(false);
426
+ resolve9(false);
427
427
  });
428
428
  child.on("exit", () => {
429
429
  clearTimeout(timeout);
@@ -440,7 +440,7 @@ function testTsserver(projectRoot2) {
440
440
  });
441
441
  }
442
442
  async function main(configOverride) {
443
- const { projectRoot: projectRoot2, tsconfigPath: tsconfigPath2, toolDir, toolIsEmbedded, toolRelPath } = configOverride ?? resolveConfig(import.meta.dirname);
443
+ const { projectRoot: projectRoot3, tsconfigPath: tsconfigPath3, toolDir, toolIsEmbedded, toolRelPath } = configOverride ?? resolveConfig(import.meta.dirname);
444
444
  let passed = 0;
445
445
  let failed = 0;
446
446
  let warned = 0;
@@ -464,7 +464,7 @@ async function main(configOverride) {
464
464
  console.log("");
465
465
  console.log("typegraph-mcp Health Check");
466
466
  console.log("=======================");
467
- console.log(`Project root: ${projectRoot2}`);
467
+ console.log(`Project root: ${projectRoot3}`);
468
468
  console.log("");
469
469
  const nodeVersion = process.version;
470
470
  const nodeMajor = parseInt(nodeVersion.slice(1).split(".")[0], 10);
@@ -473,7 +473,7 @@ async function main(configOverride) {
473
473
  } else {
474
474
  fail(`Node.js ${nodeVersion} is too old`, "Upgrade Node.js to >= 18");
475
475
  }
476
- const tsxInRoot = fs2.existsSync(path3.join(projectRoot2, "node_modules/.bin/tsx"));
476
+ const tsxInRoot = fs2.existsSync(path3.join(projectRoot3, "node_modules/.bin/tsx"));
477
477
  const tsxInTool = fs2.existsSync(path3.join(toolDir, "node_modules/.bin/tsx"));
478
478
  if (tsxInRoot || tsxInTool) {
479
479
  pass(`tsx available (in ${tsxInRoot ? "project" : "tool"} node_modules)`);
@@ -482,7 +482,7 @@ async function main(configOverride) {
482
482
  }
483
483
  let tsVersion = null;
484
484
  try {
485
- const require2 = createRequire(path3.resolve(projectRoot2, "package.json"));
485
+ const require2 = createRequire(path3.resolve(projectRoot3, "package.json"));
486
486
  const tsserverPath = require2.resolve("typescript/lib/tsserver.js");
487
487
  const tsPkgPath = path3.resolve(path3.dirname(tsserverPath), "..", "package.json");
488
488
  const tsPkg = JSON.parse(fs2.readFileSync(tsPkgPath, "utf-8"));
@@ -494,11 +494,11 @@ async function main(configOverride) {
494
494
  "Add `typescript` to devDependencies and run `npm install`"
495
495
  );
496
496
  }
497
- const tsconfigAbs = path3.resolve(projectRoot2, tsconfigPath2);
497
+ const tsconfigAbs = path3.resolve(projectRoot3, tsconfigPath3);
498
498
  if (fs2.existsSync(tsconfigAbs)) {
499
- pass(`tsconfig.json exists at ${tsconfigPath2}`);
499
+ pass(`tsconfig.json exists at ${tsconfigPath3}`);
500
500
  } else {
501
- fail(`tsconfig.json not found at ${tsconfigPath2}`, `Create a tsconfig.json at ${tsconfigPath2}`);
501
+ fail(`tsconfig.json not found at ${tsconfigPath3}`, `Create a tsconfig.json at ${tsconfigPath3}`);
502
502
  }
503
503
  const pluginMcpPath = path3.join(toolDir, ".mcp.json");
504
504
  const hasPluginMcp = fs2.existsSync(pluginMcpPath) && fs2.existsSync(path3.join(toolDir, ".claude-plugin/plugin.json"));
@@ -507,7 +507,7 @@ async function main(configOverride) {
507
507
  } else if (hasPluginMcp) {
508
508
  pass("MCP registered via plugin (.mcp.json + .claude-plugin/ present)");
509
509
  } else {
510
- const mcpJsonPath = path3.resolve(projectRoot2, ".claude/mcp.json");
510
+ const mcpJsonPath = path3.resolve(projectRoot3, ".claude/mcp.json");
511
511
  if (fs2.existsSync(mcpJsonPath)) {
512
512
  try {
513
513
  const mcpJson = JSON.parse(fs2.readFileSync(mcpJsonPath, "utf-8"));
@@ -595,7 +595,7 @@ async function main(configOverride) {
595
595
  extensionAlias: { ".js": [".ts", ".tsx", ".js"] }
596
596
  });
597
597
  let resolveOk = false;
598
- const testFile = findFirstTsFile(projectRoot2);
598
+ const testFile = findFirstTsFile(projectRoot3);
599
599
  if (testFile) {
600
600
  const dir = path3.dirname(testFile);
601
601
  const base = "./" + path3.basename(testFile);
@@ -618,7 +618,7 @@ async function main(configOverride) {
618
618
  }
619
619
  if (tsVersion) {
620
620
  try {
621
- const ok = await testTsserver(projectRoot2);
621
+ const ok = await testTsserver(projectRoot3);
622
622
  if (ok) {
623
623
  pass("tsserver responds to configure");
624
624
  } else {
@@ -644,7 +644,7 @@ async function main(configOverride) {
644
644
  ({ buildGraph: buildGraph2 } = await Promise.resolve().then(() => (init_module_graph(), module_graph_exports)));
645
645
  }
646
646
  const start2 = performance.now();
647
- const { graph } = await buildGraph2(projectRoot2, tsconfigPath2);
647
+ const { graph } = await buildGraph2(projectRoot3, tsconfigPath3);
648
648
  const elapsed = (performance.now() - start2).toFixed(0);
649
649
  const edgeCount = [...graph.forward.values()].reduce(
650
650
  (s, e) => s + e.length,
@@ -670,7 +670,7 @@ async function main(configOverride) {
670
670
  );
671
671
  }
672
672
  if (toolIsEmbedded) {
673
- const eslintConfigPath = path3.resolve(projectRoot2, "eslint.config.mjs");
673
+ const eslintConfigPath = path3.resolve(projectRoot3, "eslint.config.mjs");
674
674
  if (fs2.existsSync(eslintConfigPath)) {
675
675
  const eslintContent = fs2.readFileSync(eslintConfigPath, "utf-8");
676
676
  const parentDir = path3.basename(path3.dirname(toolDir));
@@ -691,7 +691,7 @@ async function main(configOverride) {
691
691
  } else {
692
692
  skip("ESLint config check (typegraph-mcp is external to project)");
693
693
  }
694
- const gitignorePath = path3.resolve(projectRoot2, ".gitignore");
694
+ const gitignorePath = path3.resolve(projectRoot3, ".gitignore");
695
695
  if (fs2.existsSync(gitignorePath)) {
696
696
  const gitignoreContent = fs2.readFileSync(gitignorePath, "utf-8");
697
697
  const lines = gitignoreContent.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
@@ -745,9 +745,9 @@ var init_tsserver_client = __esm({
745
745
  log2 = (...args2) => console.error("[typegraph/tsserver]", ...args2);
746
746
  REQUEST_TIMEOUT_MS = 1e4;
747
747
  TsServerClient = class {
748
- constructor(projectRoot2, tsconfigPath2 = "./tsconfig.json") {
749
- this.projectRoot = projectRoot2;
750
- this.tsconfigPath = tsconfigPath2;
748
+ constructor(projectRoot3, tsconfigPath3 = "./tsconfig.json") {
749
+ this.projectRoot = projectRoot3;
750
+ this.tsconfigPath = tsconfigPath3;
751
751
  }
752
752
  child = null;
753
753
  seq = 0;
@@ -910,12 +910,12 @@ var init_tsserver_client = __esm({
910
910
  command: command2,
911
911
  arguments: args2
912
912
  };
913
- return new Promise((resolve8, reject) => {
913
+ return new Promise((resolve9, reject) => {
914
914
  const timer = setTimeout(() => {
915
915
  this.pending.delete(seq);
916
916
  reject(new Error(`tsserver ${command2} timed out after ${REQUEST_TIMEOUT_MS}ms`));
917
917
  }, REQUEST_TIMEOUT_MS);
918
- this.pending.set(seq, { resolve: resolve8, reject, timer, command: command2 });
918
+ this.pending.set(seq, { resolve: resolve9, reject, timer, command: command2 });
919
919
  this.child.stdin.write(JSON.stringify(request) + "\n");
920
920
  });
921
921
  }
@@ -1287,8 +1287,8 @@ __export(smoke_test_exports, {
1287
1287
  });
1288
1288
  import * as fs5 from "fs";
1289
1289
  import * as path6 from "path";
1290
- function rel(absPath2, projectRoot2) {
1291
- return path6.relative(projectRoot2, absPath2);
1290
+ function rel(absPath2, projectRoot3) {
1291
+ return path6.relative(projectRoot3, absPath2);
1292
1292
  }
1293
1293
  function findInNavBar(items, predicate) {
1294
1294
  for (const item of items) {
@@ -1345,7 +1345,7 @@ function findImporter(graph, file) {
1345
1345
  return (preferred ?? revEdges[0]).target;
1346
1346
  }
1347
1347
  async function main2(configOverride) {
1348
- const { projectRoot: projectRoot2, tsconfigPath: tsconfigPath2 } = configOverride ?? resolveConfig(import.meta.dirname);
1348
+ const { projectRoot: projectRoot3, tsconfigPath: tsconfigPath3 } = configOverride ?? resolveConfig(import.meta.dirname);
1349
1349
  let passed = 0;
1350
1350
  let failed = 0;
1351
1351
  let skipped = 0;
@@ -1366,14 +1366,14 @@ async function main2(configOverride) {
1366
1366
  console.log("");
1367
1367
  console.log("typegraph-mcp Smoke Test");
1368
1368
  console.log("=====================");
1369
- console.log(`Project root: ${projectRoot2}`);
1369
+ console.log(`Project root: ${projectRoot3}`);
1370
1370
  console.log("");
1371
- const testFile = findTestFile(projectRoot2);
1371
+ const testFile = findTestFile(projectRoot3);
1372
1372
  if (!testFile) {
1373
1373
  console.log(" No suitable .ts file found in project. Cannot run smoke tests.");
1374
1374
  return { passed, failed: failed + 1, skipped };
1375
1375
  }
1376
- const testFileRel = rel(testFile, projectRoot2);
1376
+ const testFileRel = rel(testFile, projectRoot3);
1377
1377
  console.log(`Test subject: ${testFileRel}`);
1378
1378
  console.log("");
1379
1379
  console.log("\u2500\u2500 Module Graph \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
@@ -1381,7 +1381,7 @@ async function main2(configOverride) {
1381
1381
  let t0;
1382
1382
  t0 = performance.now();
1383
1383
  try {
1384
- const result = await buildGraph(projectRoot2, tsconfigPath2);
1384
+ const result = await buildGraph(projectRoot3, tsconfigPath3);
1385
1385
  graph = result.graph;
1386
1386
  const ms = performance.now() - t0;
1387
1387
  const edgeCount = [...graph.forward.values()].reduce((s, e) => s + e.length, 0);
@@ -1432,9 +1432,9 @@ async function main2(configOverride) {
1432
1432
  const result = shortestPath(graph, importer, testFile);
1433
1433
  const ms = performance.now() - t0;
1434
1434
  if (result.path) {
1435
- pass("shortest_path", `${result.hops} hops: ${result.path.map((p2) => rel(p2, projectRoot2)).join(" -> ")}`, ms);
1435
+ pass("shortest_path", `${result.hops} hops: ${result.path.map((p2) => rel(p2, projectRoot3)).join(" -> ")}`, ms);
1436
1436
  } else {
1437
- pass("shortest_path", `No path from ${rel(importer, projectRoot2)} (may be type-only)`, ms);
1437
+ pass("shortest_path", `No path from ${rel(importer, projectRoot3)} (may be type-only)`, ms);
1438
1438
  }
1439
1439
  } else {
1440
1440
  skip("shortest_path", "No importer found for test file");
@@ -1457,15 +1457,15 @@ async function main2(configOverride) {
1457
1457
  const result = moduleBoundary(graph, siblings);
1458
1458
  pass(
1459
1459
  "module_boundary",
1460
- `${siblings.length} files in ${rel(dir, projectRoot2)}/: ${result.internalEdges} internal, ${result.incomingEdges.length} in, ${result.outgoingEdges.length} out`,
1460
+ `${siblings.length} files in ${rel(dir, projectRoot3)}/: ${result.internalEdges} internal, ${result.incomingEdges.length} in, ${result.outgoingEdges.length} out`,
1461
1461
  performance.now() - t0
1462
1462
  );
1463
1463
  } else {
1464
- skip("module_boundary", `Only ${siblings.length} file(s) in ${rel(dir, projectRoot2)}/`);
1464
+ skip("module_boundary", `Only ${siblings.length} file(s) in ${rel(dir, projectRoot3)}/`);
1465
1465
  }
1466
1466
  console.log("");
1467
1467
  console.log("\u2500\u2500 tsserver \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
1468
- const client2 = new TsServerClient(projectRoot2, tsconfigPath2);
1468
+ const client2 = new TsServerClient(projectRoot3, tsconfigPath3);
1469
1469
  t0 = performance.now();
1470
1470
  await client2.start();
1471
1471
  console.log(` (started in ${(performance.now() - t0).toFixed(0)}ms)`);
@@ -1662,17 +1662,536 @@ var init_smoke_test = __esm({
1662
1662
  }
1663
1663
  });
1664
1664
 
1665
+ // benchmark.ts
1666
+ var benchmark_exports = {};
1667
+ __export(benchmark_exports, {
1668
+ main: () => main3
1669
+ });
1670
+ import * as fs6 from "fs";
1671
+ import * as path7 from "path";
1672
+ import { execSync } from "child_process";
1673
+ function estimateTokens(text) {
1674
+ return Math.ceil(text.length / 4);
1675
+ }
1676
+ function grepCount(pattern) {
1677
+ try {
1678
+ const result = execSync(
1679
+ `grep -r --include='*.ts' --include='*.tsx' -l "${pattern}" . 2>/dev/null || true`,
1680
+ { cwd: projectRoot, encoding: "utf-8", maxBuffer: 10 * 1024 * 1024 }
1681
+ ).trim();
1682
+ const files = result ? result.split("\n").filter(Boolean) : [];
1683
+ const countResult = execSync(
1684
+ `grep -r --include='*.ts' --include='*.tsx' -c "${pattern}" . 2>/dev/null || true`,
1685
+ { cwd: projectRoot, encoding: "utf-8", maxBuffer: 10 * 1024 * 1024 }
1686
+ ).trim();
1687
+ const matches = countResult.split("\n").filter(Boolean).reduce((sum, line) => {
1688
+ const count = parseInt(line.split(":").pop(), 10);
1689
+ return sum + (isNaN(count) ? 0 : count);
1690
+ }, 0);
1691
+ let totalBytes = 0;
1692
+ for (const file of files) {
1693
+ try {
1694
+ totalBytes += fs6.statSync(path7.resolve(projectRoot, file)).size;
1695
+ } catch {
1696
+ }
1697
+ }
1698
+ return { matches, files: files.length, totalBytes };
1699
+ } catch {
1700
+ return { matches: 0, files: 0, totalBytes: 0 };
1701
+ }
1702
+ }
1703
+ function relPath(abs) {
1704
+ return path7.relative(projectRoot, abs);
1705
+ }
1706
+ function percentile(sorted, p2) {
1707
+ const idx = Math.ceil(p2 / 100 * sorted.length) - 1;
1708
+ return sorted[Math.max(0, idx)];
1709
+ }
1710
+ function flattenNavBar(items) {
1711
+ const result = [];
1712
+ for (const item of items) {
1713
+ result.push(item);
1714
+ if (item.childItems?.length > 0) result.push(...flattenNavBar(item.childItems));
1715
+ }
1716
+ return result;
1717
+ }
1718
+ function findBarrelChain(graph) {
1719
+ const barrels = [...graph.files].filter((f) => path7.basename(f) === "index.ts");
1720
+ for (const barrel of barrels) {
1721
+ const edges = graph.forward.get(barrel) ?? [];
1722
+ for (const edge of edges) {
1723
+ if (edge.specifiers.length > 0 && !edge.specifiers.includes("*") && !edge.target.endsWith("index.ts") && !edge.isTypeOnly) {
1724
+ const parentBarrels = (graph.reverse.get(barrel) ?? []).filter(
1725
+ (e) => path7.basename(e.target) === "index.ts"
1726
+ );
1727
+ if (parentBarrels.length > 0) {
1728
+ return { barrelFile: barrel, sourceFile: edge.target, specifiers: edge.specifiers };
1729
+ }
1730
+ }
1731
+ }
1732
+ }
1733
+ for (const barrel of barrels) {
1734
+ const edges = graph.forward.get(barrel) ?? [];
1735
+ for (const edge of edges) {
1736
+ if (edge.specifiers.length > 0 && !edge.specifiers.includes("*") && !edge.target.endsWith("index.ts")) {
1737
+ return { barrelFile: barrel, sourceFile: edge.target, specifiers: edge.specifiers };
1738
+ }
1739
+ }
1740
+ }
1741
+ return null;
1742
+ }
1743
+ function findHighFanoutSymbol(graph) {
1744
+ const specCounts = /* @__PURE__ */ new Map();
1745
+ for (const edges of graph.forward.values()) {
1746
+ for (const edge of edges) {
1747
+ for (const spec of edge.specifiers) {
1748
+ if (spec === "*" || spec === "default" || spec.length < 4) continue;
1749
+ specCounts.set(spec, (specCounts.get(spec) ?? 0) + 1);
1750
+ }
1751
+ }
1752
+ }
1753
+ const sorted = [...specCounts.entries()].sort((a, b) => b[1] - a[1]);
1754
+ return sorted[0]?.[0] ?? null;
1755
+ }
1756
+ function findPrefixSymbol(graph) {
1757
+ const specCounts = /* @__PURE__ */ new Map();
1758
+ for (const edges of graph.forward.values()) {
1759
+ for (const edge of edges) {
1760
+ for (const spec of edge.specifiers) {
1761
+ if (spec === "*" || spec === "default" || spec.length < 5) continue;
1762
+ specCounts.set(spec, (specCounts.get(spec) ?? 0) + 1);
1763
+ }
1764
+ }
1765
+ }
1766
+ const allSpecs = [...specCounts.keys()];
1767
+ for (const [base, count] of [...specCounts.entries()].sort((a, b) => b[1] - a[1])) {
1768
+ if (count < 3) continue;
1769
+ const variants = allSpecs.filter((s) => s !== base && s.startsWith(base) && s[base.length]?.match(/[A-Z]/));
1770
+ if (variants.length >= 2) {
1771
+ return { base, variants: variants.slice(0, 5) };
1772
+ }
1773
+ }
1774
+ return null;
1775
+ }
1776
+ function findMixedImportFile(graph) {
1777
+ for (const [file, edges] of graph.forward) {
1778
+ const hasTypeOnly = edges.some((e) => e.isTypeOnly);
1779
+ const hasRuntime = edges.some((e) => !e.isTypeOnly);
1780
+ if (hasTypeOnly && hasRuntime && edges.length >= 4) {
1781
+ return file;
1782
+ }
1783
+ }
1784
+ return null;
1785
+ }
1786
+ function findMostDependedFile(graph) {
1787
+ let maxDeps = 0;
1788
+ let maxFile = null;
1789
+ for (const [file, revEdges] of graph.reverse) {
1790
+ if (file.endsWith("index.ts")) continue;
1791
+ if (revEdges.length > maxDeps) {
1792
+ maxDeps = revEdges.length;
1793
+ maxFile = file;
1794
+ }
1795
+ }
1796
+ return maxFile;
1797
+ }
1798
+ function findChainFile(graph) {
1799
+ for (const [file, edges] of graph.forward) {
1800
+ if (file.endsWith("index.ts") || file.includes(".test.")) continue;
1801
+ for (const edge of edges) {
1802
+ if (edge.isTypeOnly || edge.specifiers.includes("*")) continue;
1803
+ const targetEdges = graph.forward.get(edge.target) ?? [];
1804
+ const nonTrivialTarget = targetEdges.filter((e) => !e.isTypeOnly && !e.specifiers.includes("*"));
1805
+ if (nonTrivialTarget.length > 0 && edge.specifiers.length > 0) {
1806
+ return { file, symbol: edge.specifiers[0] };
1807
+ }
1808
+ }
1809
+ }
1810
+ return null;
1811
+ }
1812
+ function findLatencyTestFile(graph) {
1813
+ const candidates = [...graph.files].filter((f) => !f.endsWith("index.ts") && !f.includes(".test.") && !f.includes(".spec.")).map((f) => {
1814
+ try {
1815
+ return { file: f, size: fs6.statSync(f).size };
1816
+ } catch {
1817
+ return null;
1818
+ }
1819
+ }).filter((c) => c !== null && c.size > 500 && c.size < 3e4).sort((a, b) => b.size - a.size);
1820
+ const preferred = candidates.find(
1821
+ (c) => /service|handler|controller|repository|provider/i.test(path7.basename(c.file))
1822
+ );
1823
+ return preferred?.file ?? candidates[0]?.file ?? null;
1824
+ }
1825
+ async function benchmarkTokens(client2, graph) {
1826
+ console.log("=== Benchmark 1: Token Comparison (grep vs typegraph-mcp) ===");
1827
+ console.log("");
1828
+ const scenarios = [];
1829
+ const barrel = findBarrelChain(graph);
1830
+ if (barrel) {
1831
+ const symbol = barrel.specifiers[0];
1832
+ const grep = grepCount(symbol);
1833
+ const grepTokens = estimateTokens("x".repeat(Math.min(grep.totalBytes, 5e5)));
1834
+ const navItems = await client2.navto(symbol, 5);
1835
+ const def = navItems.find((i) => i.name === symbol);
1836
+ let responseText = JSON.stringify({ results: navItems, count: navItems.length });
1837
+ if (def) {
1838
+ const defs = await client2.definition(def.file, def.start.line, def.start.offset);
1839
+ responseText += JSON.stringify({ definitions: defs });
1840
+ }
1841
+ scenarios.push({
1842
+ name: "Barrel re-export resolution",
1843
+ symbol,
1844
+ description: `Re-exported through ${relPath(barrel.barrelFile)}`,
1845
+ grep: { matches: grep.matches, files: grep.files, tokensToRead: grepTokens },
1846
+ typegraph: { responseTokens: estimateTokens(responseText), toolCalls: 2 },
1847
+ reduction: grepTokens > 0 ? `${((1 - estimateTokens(responseText) / grepTokens) * 100).toFixed(0)}%` : "N/A"
1848
+ });
1849
+ }
1850
+ const highFanout = findHighFanoutSymbol(graph);
1851
+ if (highFanout) {
1852
+ const grep = grepCount(highFanout);
1853
+ const grepTokens = estimateTokens("x".repeat(Math.min(grep.totalBytes, 5e5)));
1854
+ const navItems = await client2.navto(highFanout, 10);
1855
+ const refs = navItems.length > 0 ? await client2.references(navItems[0].file, navItems[0].start.line, navItems[0].start.offset) : [];
1856
+ const responseText = JSON.stringify({ results: navItems }) + JSON.stringify({ count: refs.length });
1857
+ scenarios.push({
1858
+ name: "High-fanout symbol lookup",
1859
+ symbol: highFanout,
1860
+ description: `Most-imported symbol in the project`,
1861
+ grep: { matches: grep.matches, files: grep.files, tokensToRead: grepTokens },
1862
+ typegraph: { responseTokens: estimateTokens(responseText), toolCalls: 2 },
1863
+ reduction: grepTokens > 0 ? `${((1 - estimateTokens(responseText) / grepTokens) * 100).toFixed(0)}%` : "N/A"
1864
+ });
1865
+ }
1866
+ const chainTarget = findChainFile(graph);
1867
+ if (chainTarget) {
1868
+ const grep = grepCount(chainTarget.symbol);
1869
+ const grepTokens = estimateTokens("x".repeat(Math.min(grep.totalBytes, 5e5)));
1870
+ const navItems = await client2.navto(chainTarget.symbol, 5);
1871
+ let totalResponse = JSON.stringify({ results: navItems });
1872
+ let hops = 0;
1873
+ if (navItems.length > 0) {
1874
+ let cur = { file: navItems[0].file, line: navItems[0].start.line, offset: navItems[0].start.offset };
1875
+ for (let i = 0; i < 5; i++) {
1876
+ const defs = await client2.definition(cur.file, cur.line, cur.offset);
1877
+ if (defs.length === 0) break;
1878
+ const hop = defs[0];
1879
+ if (hop.file === cur.file && hop.start.line === cur.line) break;
1880
+ if (hop.file.includes("node_modules")) break;
1881
+ hops++;
1882
+ totalResponse += JSON.stringify({ definitions: defs });
1883
+ cur = { file: hop.file, line: hop.start.line, offset: hop.start.offset };
1884
+ }
1885
+ }
1886
+ scenarios.push({
1887
+ name: "Call chain tracing",
1888
+ symbol: chainTarget.symbol,
1889
+ description: `${hops} hop(s) from ${relPath(chainTarget.file)}`,
1890
+ grep: { matches: grep.matches, files: grep.files, tokensToRead: grepTokens },
1891
+ typegraph: { responseTokens: estimateTokens(totalResponse), toolCalls: 1 + hops },
1892
+ reduction: grepTokens > 0 ? `${((1 - estimateTokens(totalResponse) / grepTokens) * 100).toFixed(0)}%` : "N/A"
1893
+ });
1894
+ }
1895
+ const mostDepended = findMostDependedFile(graph);
1896
+ if (mostDepended) {
1897
+ const basename8 = path7.basename(mostDepended, path7.extname(mostDepended));
1898
+ const grep = grepCount(basename8);
1899
+ const grepTokens = estimateTokens("x".repeat(Math.min(grep.totalBytes, 5e5)));
1900
+ const deps = dependents(graph, mostDepended);
1901
+ const responseText = JSON.stringify({
1902
+ root: relPath(mostDepended),
1903
+ nodes: deps.nodes,
1904
+ directCount: deps.directCount,
1905
+ byPackage: deps.byPackage
1906
+ });
1907
+ scenarios.push({
1908
+ name: "Impact analysis (most-depended file)",
1909
+ symbol: basename8,
1910
+ description: `${relPath(mostDepended)} \u2014 ${deps.directCount} direct, ${deps.nodes} transitive`,
1911
+ grep: { matches: grep.matches, files: grep.files, tokensToRead: grepTokens },
1912
+ typegraph: { responseTokens: estimateTokens(responseText), toolCalls: 1 },
1913
+ reduction: grepTokens > 0 ? `${((1 - estimateTokens(responseText) / grepTokens) * 100).toFixed(0)}%` : "N/A"
1914
+ });
1915
+ }
1916
+ if (scenarios.length > 0) {
1917
+ console.log("| Scenario | Symbol | grep matches | grep files | grep tokens | tg tokens | tg calls | reduction |");
1918
+ console.log("|----------|--------|-------------|-----------|-------------|-----------|----------|-----------|");
1919
+ for (const s of scenarios) {
1920
+ console.log(
1921
+ `| ${s.name} | \`${s.symbol}\` | ${s.grep.matches} | ${s.grep.files} | ${s.grep.tokensToRead.toLocaleString()} | ${s.typegraph.responseTokens.toLocaleString()} | ${s.typegraph.toolCalls} | ${s.reduction} |`
1922
+ );
1923
+ }
1924
+ } else {
1925
+ console.log(" No suitable scenarios discovered for this codebase.");
1926
+ }
1927
+ console.log("");
1928
+ return scenarios;
1929
+ }
1930
+ async function benchmarkLatency(client2, graph) {
1931
+ console.log("=== Benchmark 2: Latency (ms per tool call) ===");
1932
+ console.log("");
1933
+ const results = [];
1934
+ const RUNS = 5;
1935
+ const testFile = findLatencyTestFile(graph);
1936
+ if (!testFile) {
1937
+ console.log(" No suitable test file found for latency benchmark.");
1938
+ console.log("");
1939
+ return results;
1940
+ }
1941
+ const testFileRel = relPath(testFile);
1942
+ console.log(`Test file: ${testFileRel}`);
1943
+ console.log(`Runs per tool: ${RUNS}`);
1944
+ console.log("");
1945
+ const bar = await client2.navbar(testFileRel);
1946
+ const allSymbols = flattenNavBar(bar);
1947
+ const concreteKinds = /* @__PURE__ */ new Set(["const", "function", "class", "var", "let", "enum"]);
1948
+ const sym = allSymbols.find(
1949
+ (item) => concreteKinds.has(item.kind) && item.text !== "<function>" && item.spans.length > 0
1950
+ );
1951
+ if (!sym) {
1952
+ console.log(" No concrete symbol found in test file.");
1953
+ console.log("");
1954
+ return results;
1955
+ }
1956
+ const span = sym.spans[0];
1957
+ console.log(`Test symbol: ${sym.text} [${sym.kind}]`);
1958
+ console.log("");
1959
+ const tsserverTools = [
1960
+ { name: "ts_find_symbol", fn: () => client2.navbar(testFileRel) },
1961
+ { name: "ts_definition", fn: () => client2.definition(testFileRel, span.start.line, span.start.offset) },
1962
+ { name: "ts_references", fn: () => client2.references(testFileRel, span.start.line, span.start.offset) },
1963
+ { name: "ts_type_info", fn: () => client2.quickinfo(testFileRel, span.start.line, span.start.offset) },
1964
+ { name: "ts_navigate_to", fn: () => client2.navto(sym.text, 10) },
1965
+ { name: "ts_module_exports", fn: () => client2.navbar(testFileRel) }
1966
+ ];
1967
+ for (const tool of tsserverTools) {
1968
+ const times = [];
1969
+ for (let i = 0; i < RUNS; i++) {
1970
+ const t0 = performance.now();
1971
+ await tool.fn();
1972
+ times.push(performance.now() - t0);
1973
+ }
1974
+ times.sort((a, b) => a - b);
1975
+ results.push({
1976
+ tool: tool.name,
1977
+ runs: RUNS,
1978
+ p50: percentile(times, 50),
1979
+ p95: percentile(times, 95),
1980
+ avg: times.reduce((a, b) => a + b, 0) / times.length,
1981
+ min: times[0],
1982
+ max: times[times.length - 1]
1983
+ });
1984
+ }
1985
+ const graphTools = [
1986
+ { name: "ts_dependency_tree", fn: () => dependencyTree(graph, testFile) },
1987
+ { name: "ts_dependents", fn: () => dependents(graph, testFile) },
1988
+ { name: "ts_import_cycles", fn: () => importCycles(graph) },
1989
+ {
1990
+ name: "ts_shortest_path",
1991
+ fn: () => {
1992
+ const rev = graph.reverse.get(testFile);
1993
+ if (rev && rev.length > 0) return shortestPath(graph, rev[0].target, testFile);
1994
+ return null;
1995
+ }
1996
+ },
1997
+ { name: "ts_subgraph", fn: () => subgraph(graph, [testFile], { depth: 2, direction: "both" }) },
1998
+ {
1999
+ name: "ts_module_boundary",
2000
+ fn: () => {
2001
+ const dir = path7.dirname(testFile);
2002
+ const siblings = [...graph.files].filter((f) => path7.dirname(f) === dir);
2003
+ return moduleBoundary(graph, siblings);
2004
+ }
2005
+ }
2006
+ ];
2007
+ for (const tool of graphTools) {
2008
+ const times = [];
2009
+ for (let i = 0; i < RUNS; i++) {
2010
+ const t0 = performance.now();
2011
+ tool.fn();
2012
+ times.push(performance.now() - t0);
2013
+ }
2014
+ times.sort((a, b) => a - b);
2015
+ results.push({
2016
+ tool: tool.name,
2017
+ runs: RUNS,
2018
+ p50: percentile(times, 50),
2019
+ p95: percentile(times, 95),
2020
+ avg: times.reduce((a, b) => a + b, 0) / times.length,
2021
+ min: times[0],
2022
+ max: times[times.length - 1]
2023
+ });
2024
+ }
2025
+ console.log("| Tool | p50 | p95 | avg | min | max |");
2026
+ console.log("|------|-----|-----|-----|-----|-----|");
2027
+ for (const r of results) {
2028
+ console.log(
2029
+ `| ${r.tool} | ${r.p50.toFixed(1)}ms | ${r.p95.toFixed(1)}ms | ${r.avg.toFixed(1)}ms | ${r.min.toFixed(1)}ms | ${r.max.toFixed(1)}ms |`
2030
+ );
2031
+ }
2032
+ console.log("");
2033
+ return results;
2034
+ }
2035
+ async function benchmarkAccuracy(client2, graph) {
2036
+ console.log("=== Benchmark 3: Accuracy (grep vs typegraph-mcp) ===");
2037
+ console.log("");
2038
+ const scenarios = [];
2039
+ const barrel = findBarrelChain(graph);
2040
+ if (barrel) {
2041
+ const symbol = barrel.specifiers[0];
2042
+ const grep = grepCount(symbol);
2043
+ const navItems = await client2.navto(symbol, 10);
2044
+ const defItem = navItems.find((i) => i.name === symbol && i.matchKind === "exact");
2045
+ let defLocation = "";
2046
+ if (defItem) {
2047
+ const defs = await client2.definition(defItem.file, defItem.start.line, defItem.start.offset);
2048
+ if (defs.length > 0) {
2049
+ defLocation = `${defs[0].file}:${defs[0].start.line}`;
2050
+ }
2051
+ }
2052
+ scenarios.push({
2053
+ name: "Barrel file resolution",
2054
+ description: `Find where \`${symbol}\` is actually defined (not re-exported)`,
2055
+ grepResult: `${grep.matches} matches across ${grep.files} files \u2014 agent must read files to distinguish definition from re-exports`,
2056
+ typegraphResult: defLocation ? `Direct: ${defLocation} (1 tool call)` : `Found ${navItems.length} declarations via navto`,
2057
+ verdict: "typegraph wins"
2058
+ });
2059
+ }
2060
+ const prefixSymbol = findPrefixSymbol(graph);
2061
+ if (prefixSymbol) {
2062
+ const grep = grepCount(prefixSymbol.base);
2063
+ const grepVariants = grepCount(`${prefixSymbol.base}[A-Z]`);
2064
+ const navItems = await client2.navto(prefixSymbol.base, 10);
2065
+ const exactMatches = navItems.filter((i) => i.name === prefixSymbol.base);
2066
+ scenarios.push({
2067
+ name: "Same-name disambiguation",
2068
+ description: `Distinguish \`${prefixSymbol.base}\` from ${prefixSymbol.variants.map((v) => `\`${v}\``).join(", ")}`,
2069
+ grepResult: `${grep.matches} total matches (includes ${grepVariants.matches} variant-name matches sharing the prefix)`,
2070
+ typegraphResult: `${exactMatches.length} exact match(es): ${exactMatches.map((i) => `${i.file}:${i.start.line} [${i.kind}]`).join(", ")}`,
2071
+ verdict: "typegraph wins"
2072
+ });
2073
+ }
2074
+ const mixedFile = findMixedImportFile(graph);
2075
+ if (mixedFile) {
2076
+ const fwdEdges = graph.forward.get(mixedFile) ?? [];
2077
+ const typeOnly = fwdEdges.filter((e) => e.isTypeOnly);
2078
+ const runtime = fwdEdges.filter((e) => !e.isTypeOnly);
2079
+ scenarios.push({
2080
+ name: "Type-only vs runtime imports",
2081
+ description: `In \`${relPath(mixedFile)}\`, distinguish type-only from runtime imports`,
2082
+ grepResult: `grep "import" shows all imports without distinguishing \`import type\` \u2014 agent must parse each line manually`,
2083
+ typegraphResult: `${typeOnly.length} type-only imports, ${runtime.length} runtime imports (module graph distinguishes automatically)`,
2084
+ verdict: "typegraph wins"
2085
+ });
2086
+ }
2087
+ const mostDepended = findMostDependedFile(graph);
2088
+ if (mostDepended) {
2089
+ const basename8 = path7.basename(mostDepended, path7.extname(mostDepended));
2090
+ const grep = grepCount(basename8);
2091
+ const deps = dependents(graph, mostDepended);
2092
+ const byPackageSummary = Object.entries(deps.byPackage).map(([pkg, files]) => `${pkg}: ${files.length}`).join(", ");
2093
+ scenarios.push({
2094
+ name: "Cross-package impact analysis",
2095
+ description: `Find everything that depends on \`${relPath(mostDepended)}\``,
2096
+ grepResult: `grep for "${basename8}" finds ${grep.matches} matches \u2014 cannot distinguish direct vs transitive, cannot follow re-exports`,
2097
+ typegraphResult: `${deps.directCount} direct dependents, ${deps.nodes} total (transitive)${byPackageSummary ? `. By package: ${byPackageSummary}` : ""}`,
2098
+ verdict: "typegraph wins"
2099
+ });
2100
+ }
2101
+ {
2102
+ const cycles = importCycles(graph);
2103
+ const cycleDetail = cycles.cycles.length > 0 ? cycles.cycles.slice(0, 3).map((c) => c.map(relPath).join(" -> ")).join("; ") : "none";
2104
+ scenarios.push({
2105
+ name: "Circular dependency detection",
2106
+ description: "Find all circular import chains in the project",
2107
+ grepResult: "Impossible with grep \u2014 requires full graph analysis",
2108
+ typegraphResult: `${cycles.count} cycle(s)${cycles.count > 0 ? `: ${cycleDetail}` : ""}`,
2109
+ verdict: "typegraph wins"
2110
+ });
2111
+ }
2112
+ for (const s of scenarios) {
2113
+ console.log(`### ${s.name}`);
2114
+ console.log(`${s.description}`);
2115
+ console.log(` grep: ${s.grepResult}`);
2116
+ console.log(` typegraph: ${s.typegraphResult}`);
2117
+ console.log(` verdict: ${s.verdict}`);
2118
+ console.log("");
2119
+ }
2120
+ return scenarios;
2121
+ }
2122
+ async function main3(config) {
2123
+ ({ projectRoot, tsconfigPath } = config ?? resolveConfig(import.meta.dirname));
2124
+ console.log("");
2125
+ console.log("typegraph-mcp Benchmark");
2126
+ console.log("=======================");
2127
+ console.log(`Project: ${projectRoot}`);
2128
+ console.log("");
2129
+ const graphStart = performance.now();
2130
+ const { graph } = await buildGraph(projectRoot, tsconfigPath);
2131
+ const graphMs = performance.now() - graphStart;
2132
+ const edgeCount = [...graph.forward.values()].reduce((s, e) => s + e.length, 0);
2133
+ console.log(`Module graph: ${graph.files.size} files, ${edgeCount} edges [${graphMs.toFixed(0)}ms]`);
2134
+ console.log("");
2135
+ const client2 = new TsServerClient(projectRoot, tsconfigPath);
2136
+ const tsStart = performance.now();
2137
+ await client2.start();
2138
+ console.log(`tsserver ready [${(performance.now() - tsStart).toFixed(0)}ms]`);
2139
+ const warmFile = [...graph.files][0];
2140
+ await client2.navbar(relPath(warmFile));
2141
+ console.log("");
2142
+ const tokenResults = await benchmarkTokens(client2, graph);
2143
+ const latencyResults = await benchmarkLatency(client2, graph);
2144
+ const accuracyResults = await benchmarkAccuracy(client2, graph);
2145
+ console.log("=== Summary ===");
2146
+ console.log("");
2147
+ if (tokenResults.length > 0) {
2148
+ const avgReduction = tokenResults.reduce((sum, s) => {
2149
+ const pct = parseFloat(s.reduction);
2150
+ return sum + (isNaN(pct) ? 0 : pct);
2151
+ }, 0) / tokenResults.length;
2152
+ console.log(`Average token reduction: ${avgReduction.toFixed(0)}%`);
2153
+ }
2154
+ const tsserverLatencies = latencyResults.filter(
2155
+ (r) => ["ts_find_symbol", "ts_definition", "ts_references", "ts_type_info", "ts_navigate_to", "ts_module_exports"].includes(r.tool)
2156
+ );
2157
+ const graphLatencies = latencyResults.filter((r) => !tsserverLatencies.includes(r));
2158
+ if (tsserverLatencies.length > 0) {
2159
+ const tsAvg = tsserverLatencies.reduce((s, r) => s + r.avg, 0) / tsserverLatencies.length;
2160
+ console.log(`Average tsserver query: ${tsAvg.toFixed(1)}ms`);
2161
+ }
2162
+ if (graphLatencies.length > 0) {
2163
+ const graphAvg = graphLatencies.reduce((s, r) => s + r.avg, 0) / graphLatencies.length;
2164
+ console.log(`Average graph query: ${graphAvg.toFixed(1)}ms`);
2165
+ }
2166
+ console.log(`Accuracy scenarios: ${accuracyResults.filter((s) => s.verdict === "typegraph wins").length}/${accuracyResults.length} typegraph wins`);
2167
+ console.log("");
2168
+ client2.shutdown();
2169
+ }
2170
+ var projectRoot, tsconfigPath;
2171
+ var init_benchmark = __esm({
2172
+ "benchmark.ts"() {
2173
+ init_tsserver_client();
2174
+ init_module_graph();
2175
+ init_graph_queries();
2176
+ init_config();
2177
+ main3().catch((err) => {
2178
+ console.error("Fatal:", err);
2179
+ process.exit(1);
2180
+ });
2181
+ }
2182
+ });
2183
+
1665
2184
  // server.ts
1666
2185
  var server_exports = {};
1667
2186
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
1668
2187
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
1669
2188
  import { z } from "zod";
1670
- import * as fs6 from "fs";
1671
- import * as path7 from "path";
2189
+ import * as fs7 from "fs";
2190
+ import * as path8 from "path";
1672
2191
  function readPreview(file, line) {
1673
2192
  try {
1674
2193
  const absPath2 = client.resolvePath(file);
1675
- const content = fs6.readFileSync(absPath2, "utf-8");
2194
+ const content = fs7.readFileSync(absPath2, "utf-8");
1676
2195
  return content.split("\n")[line - 1]?.trim() ?? "";
1677
2196
  } catch {
1678
2197
  return "";
@@ -1730,36 +2249,36 @@ async function resolveParams(params) {
1730
2249
  }
1731
2250
  return { error: "Either line+column or symbol must be provided" };
1732
2251
  }
1733
- function relPath(absPath2) {
1734
- return path7.relative(projectRoot, absPath2);
2252
+ function relPath2(absPath2) {
2253
+ return path8.relative(projectRoot2, absPath2);
1735
2254
  }
1736
2255
  function absPath(file) {
1737
- return path7.isAbsolute(file) ? file : path7.resolve(projectRoot, file);
2256
+ return path8.isAbsolute(file) ? file : path8.resolve(projectRoot2, file);
1738
2257
  }
1739
- async function main3() {
2258
+ async function main4() {
1740
2259
  log3("Starting TypeGraph MCP server...");
1741
- log3(`Project root: ${projectRoot}`);
1742
- log3(`tsconfig: ${tsconfigPath}`);
2260
+ log3(`Project root: ${projectRoot2}`);
2261
+ log3(`tsconfig: ${tsconfigPath2}`);
1743
2262
  const [, graphResult] = await Promise.all([
1744
2263
  client.start(),
1745
- buildGraph(projectRoot, tsconfigPath)
2264
+ buildGraph(projectRoot2, tsconfigPath2)
1746
2265
  ]);
1747
2266
  moduleGraph = graphResult.graph;
1748
- startWatcher(projectRoot, moduleGraph, graphResult.resolver);
2267
+ startWatcher(projectRoot2, moduleGraph, graphResult.resolver);
1749
2268
  const transport = new StdioServerTransport();
1750
2269
  await mcpServer.connect(transport);
1751
2270
  log3("MCP server connected and ready");
1752
2271
  }
1753
- var projectRoot, tsconfigPath, log3, client, moduleGraph, mcpServer, locationOrSymbol;
2272
+ var projectRoot2, tsconfigPath2, log3, client, moduleGraph, mcpServer, locationOrSymbol;
1754
2273
  var init_server = __esm({
1755
2274
  "server.ts"() {
1756
2275
  init_tsserver_client();
1757
2276
  init_module_graph();
1758
2277
  init_graph_queries();
1759
2278
  init_config();
1760
- ({ projectRoot, tsconfigPath } = resolveConfig(import.meta.dirname));
2279
+ ({ projectRoot: projectRoot2, tsconfigPath: tsconfigPath2 } = resolveConfig(import.meta.dirname));
1761
2280
  log3 = (...args2) => console.error("[typegraph]", ...args2);
1762
- client = new TsServerClient(projectRoot, tsconfigPath);
2281
+ client = new TsServerClient(projectRoot2, tsconfigPath2);
1763
2282
  mcpServer = new McpServer({
1764
2283
  name: "typegraph",
1765
2284
  version: "1.0.0"
@@ -2102,9 +2621,9 @@ var init_server = __esm({
2102
2621
  {
2103
2622
  type: "text",
2104
2623
  text: JSON.stringify({
2105
- root: relPath(result.root),
2624
+ root: relPath2(result.root),
2106
2625
  nodes: result.nodes,
2107
- files: result.files.map(relPath)
2626
+ files: result.files.map(relPath2)
2108
2627
  })
2109
2628
  }
2110
2629
  ]
@@ -2123,17 +2642,17 @@ var init_server = __esm({
2123
2642
  const result = dependents(moduleGraph, absPath(file), { depth, includeTypeOnly });
2124
2643
  const byPackageRel = {};
2125
2644
  for (const [pkg, files] of Object.entries(result.byPackage)) {
2126
- byPackageRel[pkg] = files.map(relPath);
2645
+ byPackageRel[pkg] = files.map(relPath2);
2127
2646
  }
2128
2647
  return {
2129
2648
  content: [
2130
2649
  {
2131
2650
  type: "text",
2132
2651
  text: JSON.stringify({
2133
- root: relPath(result.root),
2652
+ root: relPath2(result.root),
2134
2653
  nodes: result.nodes,
2135
2654
  directCount: result.directCount,
2136
- files: result.files.map(relPath),
2655
+ files: result.files.map(relPath2),
2137
2656
  byPackage: byPackageRel
2138
2657
  })
2139
2658
  }
@@ -2159,7 +2678,7 @@ var init_server = __esm({
2159
2678
  type: "text",
2160
2679
  text: JSON.stringify({
2161
2680
  count: result.count,
2162
- cycles: result.cycles.map((cycle) => cycle.map(relPath))
2681
+ cycles: result.cycles.map((cycle) => cycle.map(relPath2))
2163
2682
  })
2164
2683
  }
2165
2684
  ]
@@ -2181,10 +2700,10 @@ var init_server = __esm({
2181
2700
  {
2182
2701
  type: "text",
2183
2702
  text: JSON.stringify({
2184
- path: result.path?.map(relPath) ?? null,
2703
+ path: result.path?.map(relPath2) ?? null,
2185
2704
  hops: result.hops,
2186
2705
  chain: result.chain.map((c) => ({
2187
- file: relPath(c.file),
2706
+ file: relPath2(c.file),
2188
2707
  imports: c.imports
2189
2708
  }))
2190
2709
  })
@@ -2208,10 +2727,10 @@ var init_server = __esm({
2208
2727
  {
2209
2728
  type: "text",
2210
2729
  text: JSON.stringify({
2211
- nodes: result.nodes.map(relPath),
2730
+ nodes: result.nodes.map(relPath2),
2212
2731
  edges: result.edges.map((e) => ({
2213
- from: relPath(e.from),
2214
- to: relPath(e.to),
2732
+ from: relPath2(e.from),
2733
+ to: relPath2(e.to),
2215
2734
  specifiers: e.specifiers,
2216
2735
  isTypeOnly: e.isTypeOnly
2217
2736
  })),
@@ -2237,16 +2756,16 @@ var init_server = __esm({
2237
2756
  text: JSON.stringify({
2238
2757
  internalEdges: result.internalEdges,
2239
2758
  incomingEdges: result.incomingEdges.map((e) => ({
2240
- from: relPath(e.from),
2241
- to: relPath(e.to),
2759
+ from: relPath2(e.from),
2760
+ to: relPath2(e.to),
2242
2761
  specifiers: e.specifiers
2243
2762
  })),
2244
2763
  outgoingEdges: result.outgoingEdges.map((e) => ({
2245
- from: relPath(e.from),
2246
- to: relPath(e.to),
2764
+ from: relPath2(e.from),
2765
+ to: relPath2(e.to),
2247
2766
  specifiers: e.specifiers
2248
2767
  })),
2249
- sharedDependencies: result.sharedDependencies.map(relPath),
2768
+ sharedDependencies: result.sharedDependencies.map(relPath2),
2250
2769
  isolationScore: Math.round(result.isolationScore * 1e3) / 1e3
2251
2770
  })
2252
2771
  }
@@ -2263,7 +2782,7 @@ var init_server = __esm({
2263
2782
  client.shutdown();
2264
2783
  process.exit(0);
2265
2784
  });
2266
- main3().catch((err) => {
2785
+ main4().catch((err) => {
2267
2786
  log3("Fatal error:", err);
2268
2787
  process.exit(1);
2269
2788
  });
@@ -2272,9 +2791,9 @@ var init_server = __esm({
2272
2791
 
2273
2792
  // cli.ts
2274
2793
  init_config();
2275
- import * as fs7 from "fs";
2276
- import * as path8 from "path";
2277
- import { execSync } from "child_process";
2794
+ import * as fs8 from "fs";
2795
+ import * as path9 from "path";
2796
+ import { execSync as execSync2 } from "child_process";
2278
2797
  import * as p from "@clack/prompts";
2279
2798
  var AGENT_SNIPPET = `
2280
2799
  ## TypeScript Navigation (typegraph-mcp)
@@ -2300,35 +2819,35 @@ var AGENTS = {
2300
2819
  ],
2301
2820
  agentFile: "CLAUDE.md",
2302
2821
  needsAgentsSkills: false,
2303
- detect: (root) => fs7.existsSync(path8.join(root, "CLAUDE.md")) || fs7.existsSync(path8.join(root, ".claude"))
2822
+ detect: (root) => fs8.existsSync(path9.join(root, "CLAUDE.md")) || fs8.existsSync(path9.join(root, ".claude"))
2304
2823
  },
2305
2824
  cursor: {
2306
2825
  name: "Cursor",
2307
2826
  pluginFiles: [".cursor-plugin/plugin.json"],
2308
2827
  agentFile: null,
2309
2828
  needsAgentsSkills: false,
2310
- detect: (root) => fs7.existsSync(path8.join(root, ".cursor"))
2829
+ detect: (root) => fs8.existsSync(path9.join(root, ".cursor"))
2311
2830
  },
2312
2831
  codex: {
2313
2832
  name: "Codex CLI",
2314
2833
  pluginFiles: [],
2315
2834
  agentFile: "AGENTS.md",
2316
2835
  needsAgentsSkills: true,
2317
- detect: (root) => fs7.existsSync(path8.join(root, "AGENTS.md"))
2836
+ detect: (root) => fs8.existsSync(path9.join(root, "AGENTS.md"))
2318
2837
  },
2319
2838
  gemini: {
2320
2839
  name: "Gemini CLI",
2321
2840
  pluginFiles: ["gemini-extension.json"],
2322
2841
  agentFile: "GEMINI.md",
2323
2842
  needsAgentsSkills: true,
2324
- detect: (root) => fs7.existsSync(path8.join(root, "GEMINI.md"))
2843
+ detect: (root) => fs8.existsSync(path9.join(root, "GEMINI.md"))
2325
2844
  },
2326
2845
  copilot: {
2327
2846
  name: "GitHub Copilot",
2328
2847
  pluginFiles: [],
2329
2848
  agentFile: ".github/copilot-instructions.md",
2330
2849
  needsAgentsSkills: true,
2331
- detect: (root) => fs7.existsSync(path8.join(root, ".github/copilot-instructions.md"))
2850
+ detect: (root) => fs8.existsSync(path9.join(root, ".github/copilot-instructions.md"))
2332
2851
  }
2333
2852
  };
2334
2853
  var CORE_FILES = [
@@ -2366,6 +2885,7 @@ Commands:
2366
2885
  remove Uninstall typegraph-mcp from the current project
2367
2886
  check Run health checks (12 checks)
2368
2887
  test Run smoke tests (all 14 tools)
2888
+ bench Run benchmarks (token, latency, accuracy)
2369
2889
  start Start the MCP server (stdin/stdout)
2370
2890
 
2371
2891
  Options:
@@ -2373,13 +2893,13 @@ Options:
2373
2893
  --help Show this help
2374
2894
  `.trim();
2375
2895
  function copyFile(src, dest) {
2376
- const destDir = path8.dirname(dest);
2377
- if (!fs7.existsSync(destDir)) {
2378
- fs7.mkdirSync(destDir, { recursive: true });
2896
+ const destDir = path9.dirname(dest);
2897
+ if (!fs8.existsSync(destDir)) {
2898
+ fs8.mkdirSync(destDir, { recursive: true });
2379
2899
  }
2380
- fs7.copyFileSync(src, dest);
2900
+ fs8.copyFileSync(src, dest);
2381
2901
  if (src.endsWith(".sh")) {
2382
- fs7.chmodSync(dest, 493);
2902
+ fs8.chmodSync(dest, 493);
2383
2903
  }
2384
2904
  }
2385
2905
  var MCP_SERVER_ENTRY = {
@@ -2390,28 +2910,28 @@ var MCP_SERVER_ENTRY = {
2390
2910
  TYPEGRAPH_TSCONFIG: "./tsconfig.json"
2391
2911
  }
2392
2912
  };
2393
- function registerMcpServers(projectRoot2, selectedAgents) {
2913
+ function registerMcpServers(projectRoot3, selectedAgents) {
2394
2914
  if (selectedAgents.includes("cursor")) {
2395
- registerJsonMcp(projectRoot2, ".cursor/mcp.json", "mcpServers");
2915
+ registerJsonMcp(projectRoot3, ".cursor/mcp.json", "mcpServers");
2396
2916
  }
2397
2917
  if (selectedAgents.includes("codex")) {
2398
- registerCodexMcp(projectRoot2);
2918
+ registerCodexMcp(projectRoot3);
2399
2919
  }
2400
2920
  if (selectedAgents.includes("copilot")) {
2401
- registerJsonMcp(projectRoot2, ".vscode/mcp.json", "servers");
2921
+ registerJsonMcp(projectRoot3, ".vscode/mcp.json", "servers");
2402
2922
  }
2403
2923
  }
2404
- function deregisterMcpServers(projectRoot2) {
2405
- deregisterJsonMcp(projectRoot2, ".cursor/mcp.json", "mcpServers");
2406
- deregisterCodexMcp(projectRoot2);
2407
- deregisterJsonMcp(projectRoot2, ".vscode/mcp.json", "servers");
2924
+ function deregisterMcpServers(projectRoot3) {
2925
+ deregisterJsonMcp(projectRoot3, ".cursor/mcp.json", "mcpServers");
2926
+ deregisterCodexMcp(projectRoot3);
2927
+ deregisterJsonMcp(projectRoot3, ".vscode/mcp.json", "servers");
2408
2928
  }
2409
- function registerJsonMcp(projectRoot2, configPath, rootKey) {
2410
- const fullPath = path8.resolve(projectRoot2, configPath);
2929
+ function registerJsonMcp(projectRoot3, configPath, rootKey) {
2930
+ const fullPath = path9.resolve(projectRoot3, configPath);
2411
2931
  let config = {};
2412
- if (fs7.existsSync(fullPath)) {
2932
+ if (fs8.existsSync(fullPath)) {
2413
2933
  try {
2414
- config = JSON.parse(fs7.readFileSync(fullPath, "utf-8"));
2934
+ config = JSON.parse(fs8.readFileSync(fullPath, "utf-8"));
2415
2935
  } catch {
2416
2936
  p.log.warn(`Could not parse ${configPath} \u2014 skipping MCP registration`);
2417
2937
  return;
@@ -2424,18 +2944,18 @@ function registerJsonMcp(projectRoot2, configPath, rootKey) {
2424
2944
  }
2425
2945
  servers["typegraph"] = entry;
2426
2946
  config[rootKey] = servers;
2427
- const dir = path8.dirname(fullPath);
2428
- if (!fs7.existsSync(dir)) {
2429
- fs7.mkdirSync(dir, { recursive: true });
2947
+ const dir = path9.dirname(fullPath);
2948
+ if (!fs8.existsSync(dir)) {
2949
+ fs8.mkdirSync(dir, { recursive: true });
2430
2950
  }
2431
- fs7.writeFileSync(fullPath, JSON.stringify(config, null, 2) + "\n");
2951
+ fs8.writeFileSync(fullPath, JSON.stringify(config, null, 2) + "\n");
2432
2952
  p.log.success(`${configPath}: registered typegraph MCP server`);
2433
2953
  }
2434
- function deregisterJsonMcp(projectRoot2, configPath, rootKey) {
2435
- const fullPath = path8.resolve(projectRoot2, configPath);
2436
- if (!fs7.existsSync(fullPath)) return;
2954
+ function deregisterJsonMcp(projectRoot3, configPath, rootKey) {
2955
+ const fullPath = path9.resolve(projectRoot3, configPath);
2956
+ if (!fs8.existsSync(fullPath)) return;
2437
2957
  try {
2438
- const config = JSON.parse(fs7.readFileSync(fullPath, "utf-8"));
2958
+ const config = JSON.parse(fs8.readFileSync(fullPath, "utf-8"));
2439
2959
  const servers = config[rootKey];
2440
2960
  if (!servers || !servers["typegraph"]) return;
2441
2961
  delete servers["typegraph"];
@@ -2443,20 +2963,20 @@ function deregisterJsonMcp(projectRoot2, configPath, rootKey) {
2443
2963
  delete config[rootKey];
2444
2964
  }
2445
2965
  if (Object.keys(config).length === 0) {
2446
- fs7.unlinkSync(fullPath);
2966
+ fs8.unlinkSync(fullPath);
2447
2967
  } else {
2448
- fs7.writeFileSync(fullPath, JSON.stringify(config, null, 2) + "\n");
2968
+ fs8.writeFileSync(fullPath, JSON.stringify(config, null, 2) + "\n");
2449
2969
  }
2450
2970
  p.log.info(`${configPath}: removed typegraph MCP server`);
2451
2971
  } catch {
2452
2972
  }
2453
2973
  }
2454
- function registerCodexMcp(projectRoot2) {
2974
+ function registerCodexMcp(projectRoot3) {
2455
2975
  const configPath = ".codex/config.toml";
2456
- const fullPath = path8.resolve(projectRoot2, configPath);
2976
+ const fullPath = path9.resolve(projectRoot3, configPath);
2457
2977
  let content = "";
2458
- if (fs7.existsSync(fullPath)) {
2459
- content = fs7.readFileSync(fullPath, "utf-8");
2978
+ if (fs8.existsSync(fullPath)) {
2979
+ content = fs8.readFileSync(fullPath, "utf-8");
2460
2980
  if (content.includes("[mcp_servers.typegraph]")) {
2461
2981
  p.log.info(`${configPath}: typegraph MCP server already registered`);
2462
2982
  return;
@@ -2470,19 +2990,19 @@ function registerCodexMcp(projectRoot2) {
2470
2990
  'env = { TYPEGRAPH_PROJECT_ROOT = ".", TYPEGRAPH_TSCONFIG = "./tsconfig.json" }',
2471
2991
  ""
2472
2992
  ].join("\n");
2473
- const dir = path8.dirname(fullPath);
2474
- if (!fs7.existsSync(dir)) {
2475
- fs7.mkdirSync(dir, { recursive: true });
2993
+ const dir = path9.dirname(fullPath);
2994
+ if (!fs8.existsSync(dir)) {
2995
+ fs8.mkdirSync(dir, { recursive: true });
2476
2996
  }
2477
2997
  const newContent = content ? content.trimEnd() + "\n" + block : block.trimStart();
2478
- fs7.writeFileSync(fullPath, newContent);
2998
+ fs8.writeFileSync(fullPath, newContent);
2479
2999
  p.log.success(`${configPath}: registered typegraph MCP server`);
2480
3000
  }
2481
- function deregisterCodexMcp(projectRoot2) {
3001
+ function deregisterCodexMcp(projectRoot3) {
2482
3002
  const configPath = ".codex/config.toml";
2483
- const fullPath = path8.resolve(projectRoot2, configPath);
2484
- if (!fs7.existsSync(fullPath)) return;
2485
- let content = fs7.readFileSync(fullPath, "utf-8");
3003
+ const fullPath = path9.resolve(projectRoot3, configPath);
3004
+ if (!fs8.existsSync(fullPath)) return;
3005
+ let content = fs8.readFileSync(fullPath, "utf-8");
2486
3006
  if (!content.includes("[mcp_servers.typegraph]")) return;
2487
3007
  content = content.replace(
2488
3008
  /\n?\[mcp_servers\.typegraph\]\n[\s\S]*?(?=\n\[|$)/,
@@ -2490,17 +3010,17 @@ function deregisterCodexMcp(projectRoot2) {
2490
3010
  );
2491
3011
  content = content.replace(/\n{3,}/g, "\n\n").trimEnd() + "\n";
2492
3012
  if (content.trim() === "") {
2493
- fs7.unlinkSync(fullPath);
3013
+ fs8.unlinkSync(fullPath);
2494
3014
  } else {
2495
- fs7.writeFileSync(fullPath, content);
3015
+ fs8.writeFileSync(fullPath, content);
2496
3016
  }
2497
3017
  p.log.info(`${configPath}: removed typegraph MCP server`);
2498
3018
  }
2499
- function ensureTsconfigExclude(projectRoot2) {
2500
- const tsconfigPath2 = path8.resolve(projectRoot2, "tsconfig.json");
2501
- if (!fs7.existsSync(tsconfigPath2)) return;
3019
+ function ensureTsconfigExclude(projectRoot3) {
3020
+ const tsconfigPath3 = path9.resolve(projectRoot3, "tsconfig.json");
3021
+ if (!fs8.existsSync(tsconfigPath3)) return;
2502
3022
  try {
2503
- const raw = fs7.readFileSync(tsconfigPath2, "utf-8");
3023
+ const raw = fs8.readFileSync(tsconfigPath3, "utf-8");
2504
3024
  const stripped = raw.replace(/\/\/.*$/gm, "").replace(/,(\s*[}\]])/g, "$1");
2505
3025
  const tsconfig = JSON.parse(stripped);
2506
3026
  const exclude = tsconfig.exclude || [];
@@ -2517,7 +3037,7 @@ function ensureTsconfigExclude(projectRoot2) {
2517
3037
  "plugins/**"${close}`;
2518
3038
  }
2519
3039
  );
2520
- fs7.writeFileSync(tsconfigPath2, updated);
3040
+ fs8.writeFileSync(tsconfigPath3, updated);
2521
3041
  } else {
2522
3042
  const lastBrace = raw.lastIndexOf("}");
2523
3043
  if (lastBrace !== -1) {
@@ -2527,7 +3047,7 @@ function ensureTsconfigExclude(projectRoot2) {
2527
3047
  "exclude": ["plugins/**"]
2528
3048
  }
2529
3049
  `;
2530
- fs7.writeFileSync(tsconfigPath2, patched);
3050
+ fs8.writeFileSync(tsconfigPath3, patched);
2531
3051
  }
2532
3052
  }
2533
3053
  p.log.success('Added "plugins/**" to tsconfig.json exclude (prevents build errors)');
@@ -2535,11 +3055,11 @@ function ensureTsconfigExclude(projectRoot2) {
2535
3055
  p.log.warn('Could not update tsconfig.json \u2014 manually add "plugins/**" to the exclude array to prevent build errors');
2536
3056
  }
2537
3057
  }
2538
- function detectAgents(projectRoot2) {
2539
- return AGENT_IDS.filter((id) => AGENTS[id].detect(projectRoot2));
3058
+ function detectAgents(projectRoot3) {
3059
+ return AGENT_IDS.filter((id) => AGENTS[id].detect(projectRoot3));
2540
3060
  }
2541
- async function selectAgents(projectRoot2, yes2) {
2542
- const detected = detectAgents(projectRoot2);
3061
+ async function selectAgents(projectRoot3, yes2) {
3062
+ const detected = detectAgents(projectRoot3);
2543
3063
  if (yes2) {
2544
3064
  const selected2 = detected.length > 0 ? detected : [...AGENT_IDS];
2545
3065
  p.log.info(`Auto-selected: ${selected2.map((id) => AGENTS[id].name).join(", ")}`);
@@ -2565,24 +3085,24 @@ async function selectAgents(projectRoot2, yes2) {
2565
3085
  return selected;
2566
3086
  }
2567
3087
  async function setup(yes2) {
2568
- const sourceDir = path8.basename(import.meta.dirname) === "dist" ? path8.resolve(import.meta.dirname, "..") : import.meta.dirname;
2569
- const projectRoot2 = process.cwd();
3088
+ const sourceDir = path9.basename(import.meta.dirname) === "dist" ? path9.resolve(import.meta.dirname, "..") : import.meta.dirname;
3089
+ const projectRoot3 = process.cwd();
2570
3090
  process.stdout.write("\x1Bc");
2571
3091
  p.intro("TypeGraph MCP Setup");
2572
- p.log.info(`Project: ${projectRoot2}`);
2573
- const pkgJsonPath = path8.resolve(projectRoot2, "package.json");
2574
- const tsconfigPath2 = path8.resolve(projectRoot2, "tsconfig.json");
2575
- if (!fs7.existsSync(pkgJsonPath)) {
3092
+ p.log.info(`Project: ${projectRoot3}`);
3093
+ const pkgJsonPath = path9.resolve(projectRoot3, "package.json");
3094
+ const tsconfigPath3 = path9.resolve(projectRoot3, "tsconfig.json");
3095
+ if (!fs8.existsSync(pkgJsonPath)) {
2576
3096
  p.cancel("No package.json found. Run this from the root of your TypeScript project.");
2577
3097
  process.exit(1);
2578
3098
  }
2579
- if (!fs7.existsSync(tsconfigPath2)) {
3099
+ if (!fs8.existsSync(tsconfigPath3)) {
2580
3100
  p.cancel("No tsconfig.json found. typegraph-mcp requires a TypeScript project.");
2581
3101
  process.exit(1);
2582
3102
  }
2583
3103
  p.log.success("Found package.json and tsconfig.json");
2584
- const targetDir = path8.resolve(projectRoot2, PLUGIN_DIR_NAME);
2585
- const isUpdate = fs7.existsSync(targetDir);
3104
+ const targetDir = path9.resolve(projectRoot3, PLUGIN_DIR_NAME);
3105
+ const isUpdate = fs8.existsSync(targetDir);
2586
3106
  if (isUpdate && !yes2) {
2587
3107
  const action = await p.select({
2588
3108
  message: `${PLUGIN_DIR_NAME}/ already exists.`,
@@ -2597,7 +3117,7 @@ async function setup(yes2) {
2597
3117
  process.exit(0);
2598
3118
  }
2599
3119
  if (action === "remove") {
2600
- await removePlugin(projectRoot2, targetDir);
3120
+ await removePlugin(projectRoot3, targetDir);
2601
3121
  return;
2602
3122
  }
2603
3123
  if (action === "exit") {
@@ -2605,7 +3125,7 @@ async function setup(yes2) {
2605
3125
  return;
2606
3126
  }
2607
3127
  }
2608
- const selectedAgents = await selectAgents(projectRoot2, yes2);
3128
+ const selectedAgents = await selectAgents(projectRoot3, yes2);
2609
3129
  const needsPluginSkills = selectedAgents.includes("claude-code") || selectedAgents.includes("cursor");
2610
3130
  const needsAgentsSkills = selectedAgents.some((id) => AGENTS[id].needsAgentsSkills);
2611
3131
  p.log.step(`Installing to ${PLUGIN_DIR_NAME}/...`);
@@ -2620,9 +3140,9 @@ async function setup(yes2) {
2620
3140
  }
2621
3141
  let copied = 0;
2622
3142
  for (const file of filesToCopy) {
2623
- const src = path8.join(sourceDir, file);
2624
- const dest = path8.join(targetDir, file);
2625
- if (fs7.existsSync(src)) {
3143
+ const src = path9.join(sourceDir, file);
3144
+ const dest = path9.join(targetDir, file);
3145
+ if (fs8.existsSync(src)) {
2626
3146
  copyFile(src, dest);
2627
3147
  copied++;
2628
3148
  } else {
@@ -2631,7 +3151,7 @@ async function setup(yes2) {
2631
3151
  }
2632
3152
  s.message("Installing dependencies...");
2633
3153
  try {
2634
- execSync("npm install", { cwd: targetDir, stdio: "pipe" });
3154
+ execSync2("npm install", { cwd: targetDir, stdio: "pipe" });
2635
3155
  s.stop(`${isUpdate ? "Updated" : "Installed"} ${copied} files with dependencies`);
2636
3156
  } catch (err) {
2637
3157
  s.stop(`${isUpdate ? "Updated" : "Installed"} ${copied} files`);
@@ -2640,20 +3160,20 @@ async function setup(yes2) {
2640
3160
  }
2641
3161
  if (needsAgentsSkills) {
2642
3162
  const agentsNames = selectedAgents.filter((id) => AGENTS[id].needsAgentsSkills).map((id) => AGENTS[id].name);
2643
- const agentsSkillsDir = path8.resolve(projectRoot2, ".agents/skills");
3163
+ const agentsSkillsDir = path9.resolve(projectRoot3, ".agents/skills");
2644
3164
  let copiedSkills = 0;
2645
3165
  for (const skill of SKILL_NAMES) {
2646
- const src = path8.join(targetDir, "skills", skill, "SKILL.md");
2647
- const destDir = path8.join(agentsSkillsDir, skill);
2648
- const dest = path8.join(destDir, "SKILL.md");
2649
- if (!fs7.existsSync(src)) continue;
2650
- if (fs7.existsSync(dest)) {
2651
- const srcContent = fs7.readFileSync(src, "utf-8");
2652
- const destContent = fs7.readFileSync(dest, "utf-8");
3166
+ const src = path9.join(targetDir, "skills", skill, "SKILL.md");
3167
+ const destDir = path9.join(agentsSkillsDir, skill);
3168
+ const dest = path9.join(destDir, "SKILL.md");
3169
+ if (!fs8.existsSync(src)) continue;
3170
+ if (fs8.existsSync(dest)) {
3171
+ const srcContent = fs8.readFileSync(src, "utf-8");
3172
+ const destContent = fs8.readFileSync(dest, "utf-8");
2653
3173
  if (srcContent === destContent) continue;
2654
3174
  }
2655
- fs7.mkdirSync(destDir, { recursive: true });
2656
- fs7.copyFileSync(src, dest);
3175
+ fs8.mkdirSync(destDir, { recursive: true });
3176
+ fs8.copyFileSync(src, dest);
2657
3177
  copiedSkills++;
2658
3178
  }
2659
3179
  if (copiedSkills > 0) {
@@ -2663,70 +3183,70 @@ async function setup(yes2) {
2663
3183
  }
2664
3184
  }
2665
3185
  if (selectedAgents.includes("claude-code")) {
2666
- const mcpJsonPath = path8.resolve(projectRoot2, ".claude/mcp.json");
2667
- if (fs7.existsSync(mcpJsonPath)) {
3186
+ const mcpJsonPath = path9.resolve(projectRoot3, ".claude/mcp.json");
3187
+ if (fs8.existsSync(mcpJsonPath)) {
2668
3188
  try {
2669
- const mcpJson = JSON.parse(fs7.readFileSync(mcpJsonPath, "utf-8"));
3189
+ const mcpJson = JSON.parse(fs8.readFileSync(mcpJsonPath, "utf-8"));
2670
3190
  if (mcpJson.mcpServers?.["typegraph"]) {
2671
3191
  delete mcpJson.mcpServers["typegraph"];
2672
- fs7.writeFileSync(mcpJsonPath, JSON.stringify(mcpJson, null, 2) + "\n");
3192
+ fs8.writeFileSync(mcpJsonPath, JSON.stringify(mcpJson, null, 2) + "\n");
2673
3193
  p.log.info("Removed old typegraph entry from .claude/mcp.json");
2674
3194
  }
2675
3195
  } catch {
2676
3196
  }
2677
3197
  }
2678
3198
  }
2679
- await setupAgentInstructions(projectRoot2, selectedAgents);
2680
- registerMcpServers(projectRoot2, selectedAgents);
2681
- ensureTsconfigExclude(projectRoot2);
3199
+ await setupAgentInstructions(projectRoot3, selectedAgents);
3200
+ registerMcpServers(projectRoot3, selectedAgents);
3201
+ ensureTsconfigExclude(projectRoot3);
2682
3202
  await runVerification(targetDir, selectedAgents);
2683
3203
  }
2684
- async function removePlugin(projectRoot2, pluginDir) {
3204
+ async function removePlugin(projectRoot3, pluginDir) {
2685
3205
  const s = p.spinner();
2686
3206
  s.start("Removing typegraph-mcp...");
2687
- if (fs7.existsSync(pluginDir)) {
2688
- fs7.rmSync(pluginDir, { recursive: true });
3207
+ if (fs8.existsSync(pluginDir)) {
3208
+ fs8.rmSync(pluginDir, { recursive: true });
2689
3209
  }
2690
- const agentsSkillsDir = path8.resolve(projectRoot2, ".agents/skills");
3210
+ const agentsSkillsDir = path9.resolve(projectRoot3, ".agents/skills");
2691
3211
  for (const skill of SKILL_NAMES) {
2692
- const skillDir = path8.join(agentsSkillsDir, skill);
2693
- if (fs7.existsSync(skillDir)) {
2694
- fs7.rmSync(skillDir, { recursive: true });
3212
+ const skillDir = path9.join(agentsSkillsDir, skill);
3213
+ if (fs8.existsSync(skillDir)) {
3214
+ fs8.rmSync(skillDir, { recursive: true });
2695
3215
  }
2696
3216
  }
2697
- if (fs7.existsSync(agentsSkillsDir) && fs7.readdirSync(agentsSkillsDir).length === 0) {
2698
- fs7.rmSync(agentsSkillsDir, { recursive: true });
2699
- const agentsDir = path8.resolve(projectRoot2, ".agents");
2700
- if (fs7.existsSync(agentsDir) && fs7.readdirSync(agentsDir).length === 0) {
2701
- fs7.rmSync(agentsDir, { recursive: true });
3217
+ if (fs8.existsSync(agentsSkillsDir) && fs8.readdirSync(agentsSkillsDir).length === 0) {
3218
+ fs8.rmSync(agentsSkillsDir, { recursive: true });
3219
+ const agentsDir = path9.resolve(projectRoot3, ".agents");
3220
+ if (fs8.existsSync(agentsDir) && fs8.readdirSync(agentsDir).length === 0) {
3221
+ fs8.rmSync(agentsDir, { recursive: true });
2702
3222
  }
2703
3223
  }
2704
3224
  const allAgentFiles = AGENT_IDS.map((id) => AGENTS[id].agentFile).filter((f) => f !== null);
2705
3225
  const seenRealPaths = /* @__PURE__ */ new Set();
2706
3226
  for (const agentFile of allAgentFiles) {
2707
- const filePath = path8.resolve(projectRoot2, agentFile);
2708
- if (!fs7.existsSync(filePath)) continue;
2709
- const realPath = fs7.realpathSync(filePath);
3227
+ const filePath = path9.resolve(projectRoot3, agentFile);
3228
+ if (!fs8.existsSync(filePath)) continue;
3229
+ const realPath = fs8.realpathSync(filePath);
2710
3230
  if (seenRealPaths.has(realPath)) continue;
2711
3231
  seenRealPaths.add(realPath);
2712
- let content = fs7.readFileSync(realPath, "utf-8");
3232
+ let content = fs8.readFileSync(realPath, "utf-8");
2713
3233
  if (content.includes(SNIPPET_MARKER)) {
2714
3234
  content = content.replace(/\n?## TypeScript Navigation \(typegraph-mcp\)\n[\s\S]*?(?=\n## |\n# |$)/, "");
2715
3235
  content = content.replace(/\n{3,}$/, "\n");
2716
- fs7.writeFileSync(realPath, content);
3236
+ fs8.writeFileSync(realPath, content);
2717
3237
  }
2718
3238
  }
2719
- const claudeMdPath = path8.resolve(projectRoot2, "CLAUDE.md");
2720
- if (fs7.existsSync(claudeMdPath)) {
2721
- let content = fs7.readFileSync(claudeMdPath, "utf-8");
3239
+ const claudeMdPath = path9.resolve(projectRoot3, "CLAUDE.md");
3240
+ if (fs8.existsSync(claudeMdPath)) {
3241
+ let content = fs8.readFileSync(claudeMdPath, "utf-8");
2722
3242
  content = content.replace(/ --plugin-dir \.\/plugins\/typegraph-mcp/g, "");
2723
- fs7.writeFileSync(claudeMdPath, content);
3243
+ fs8.writeFileSync(claudeMdPath, content);
2724
3244
  }
2725
3245
  s.stop("Removed typegraph-mcp");
2726
- deregisterMcpServers(projectRoot2);
3246
+ deregisterMcpServers(projectRoot3);
2727
3247
  p.outro("typegraph-mcp has been uninstalled from this project.");
2728
3248
  }
2729
- async function setupAgentInstructions(projectRoot2, selectedAgents) {
3249
+ async function setupAgentInstructions(projectRoot3, selectedAgents) {
2730
3250
  const agentFiles = selectedAgents.map((id) => AGENTS[id].agentFile).filter((f) => f !== null);
2731
3251
  if (agentFiles.length === 0) {
2732
3252
  return;
@@ -2734,16 +3254,16 @@ async function setupAgentInstructions(projectRoot2, selectedAgents) {
2734
3254
  const seenRealPaths = /* @__PURE__ */ new Map();
2735
3255
  const existingFiles = [];
2736
3256
  for (const agentFile of agentFiles) {
2737
- const filePath = path8.resolve(projectRoot2, agentFile);
2738
- if (!fs7.existsSync(filePath)) continue;
2739
- const realPath = fs7.realpathSync(filePath);
3257
+ const filePath = path9.resolve(projectRoot3, agentFile);
3258
+ if (!fs8.existsSync(filePath)) continue;
3259
+ const realPath = fs8.realpathSync(filePath);
2740
3260
  const previousFile = seenRealPaths.get(realPath);
2741
3261
  if (previousFile) {
2742
3262
  p.log.info(`${agentFile}: same file as ${previousFile} (skipped)`);
2743
3263
  continue;
2744
3264
  }
2745
3265
  seenRealPaths.set(realPath, agentFile);
2746
- const content = fs7.readFileSync(filePath, "utf-8");
3266
+ const content = fs8.readFileSync(filePath, "utf-8");
2747
3267
  existingFiles.push({ file: agentFile, realPath, hasSnippet: content.includes(SNIPPET_MARKER) });
2748
3268
  }
2749
3269
  if (existingFiles.length === 0) {
@@ -2757,15 +3277,15 @@ async function setupAgentInstructions(projectRoot2, selectedAgents) {
2757
3277
  }
2758
3278
  } else {
2759
3279
  const target = existingFiles[0];
2760
- const content = fs7.readFileSync(target.realPath, "utf-8");
3280
+ const content = fs8.readFileSync(target.realPath, "utf-8");
2761
3281
  const appendContent = (content.endsWith("\n") ? "" : "\n") + "\n" + AGENT_SNIPPET;
2762
- fs7.appendFileSync(target.realPath, appendContent);
3282
+ fs8.appendFileSync(target.realPath, appendContent);
2763
3283
  p.log.success(`${target.file}: appended typegraph-mcp instructions`);
2764
3284
  }
2765
3285
  if (selectedAgents.includes("claude-code")) {
2766
- const claudeMdPath = path8.resolve(projectRoot2, "CLAUDE.md");
2767
- if (fs7.existsSync(claudeMdPath)) {
2768
- let content = fs7.readFileSync(claudeMdPath, "utf-8");
3286
+ const claudeMdPath = path9.resolve(projectRoot3, "CLAUDE.md");
3287
+ if (fs8.existsSync(claudeMdPath)) {
3288
+ let content = fs8.readFileSync(claudeMdPath, "utf-8");
2769
3289
  const pluginDirPattern = /(`claude\s+)((?:--plugin-dir\s+\S+\s*)+)(`)/;
2770
3290
  const match = content.match(pluginDirPattern);
2771
3291
  if (match && !match[2].includes("./plugins/typegraph-mcp")) {
@@ -2774,7 +3294,7 @@ async function setupAgentInstructions(projectRoot2, selectedAgents) {
2774
3294
  pluginDirPattern,
2775
3295
  `$1${existingFlags} --plugin-dir ./plugins/typegraph-mcp$3`
2776
3296
  );
2777
- fs7.writeFileSync(claudeMdPath, content);
3297
+ fs8.writeFileSync(claudeMdPath, content);
2778
3298
  p.log.success("CLAUDE.md: added --plugin-dir ./plugins/typegraph-mcp");
2779
3299
  } else if (match) {
2780
3300
  p.log.info("CLAUDE.md: --plugin-dir already includes typegraph-mcp");
@@ -2797,9 +3317,9 @@ async function runVerification(pluginDir, selectedAgents) {
2797
3317
  console.log("");
2798
3318
  if (checkResult.failed === 0 && testResult.failed === 0) {
2799
3319
  if (selectedAgents.includes("claude-code")) {
2800
- p.outro("Setup complete! Run: claude --plugin-dir ./plugins/typegraph-mcp");
3320
+ p.outro("Setup complete! Run: claude --plugin-dir ./plugins/typegraph-mcp\n Slash commands: /typegraph:check, /typegraph:test, /typegraph:bench");
2801
3321
  } else {
2802
- p.outro("Setup complete! typegraph-mcp tools are now available to your agents.");
3322
+ p.outro("Setup complete! typegraph-mcp tools are now available to your agents.\n CLI: npx typegraph-mcp check | test | bench");
2803
3323
  }
2804
3324
  } else {
2805
3325
  p.cancel("Setup completed with issues. Fix the failures above and re-run.");
@@ -2807,11 +3327,11 @@ async function runVerification(pluginDir, selectedAgents) {
2807
3327
  }
2808
3328
  }
2809
3329
  async function remove(yes2) {
2810
- const projectRoot2 = process.cwd();
2811
- const pluginDir = path8.resolve(projectRoot2, PLUGIN_DIR_NAME);
3330
+ const projectRoot3 = process.cwd();
3331
+ const pluginDir = path9.resolve(projectRoot3, PLUGIN_DIR_NAME);
2812
3332
  process.stdout.write("\x1Bc");
2813
3333
  p.intro("TypeGraph MCP Remove");
2814
- if (!fs7.existsSync(pluginDir)) {
3334
+ if (!fs8.existsSync(pluginDir)) {
2815
3335
  p.cancel("typegraph-mcp is not installed in this project.");
2816
3336
  process.exit(1);
2817
3337
  }
@@ -2822,12 +3342,12 @@ async function remove(yes2) {
2822
3342
  process.exit(0);
2823
3343
  }
2824
3344
  }
2825
- await removePlugin(projectRoot2, pluginDir);
3345
+ await removePlugin(projectRoot3, pluginDir);
2826
3346
  }
2827
3347
  function resolvePluginDir() {
2828
- const installed = path8.resolve(process.cwd(), PLUGIN_DIR_NAME);
2829
- if (fs7.existsSync(installed)) return installed;
2830
- return path8.basename(import.meta.dirname) === "dist" ? path8.resolve(import.meta.dirname, "..") : import.meta.dirname;
3348
+ const installed = path9.resolve(process.cwd(), PLUGIN_DIR_NAME);
3349
+ if (fs8.existsSync(installed)) return installed;
3350
+ return path9.basename(import.meta.dirname) === "dist" ? path9.resolve(import.meta.dirname, "..") : import.meta.dirname;
2831
3351
  }
2832
3352
  async function check() {
2833
3353
  const config = resolveConfig(resolvePluginDir());
@@ -2841,6 +3361,11 @@ async function test() {
2841
3361
  const result = await testMain(config);
2842
3362
  process.exit(result.failed > 0 ? 1 : 0);
2843
3363
  }
3364
+ async function benchmark() {
3365
+ const config = resolveConfig(resolvePluginDir());
3366
+ const { main: benchMain } = await Promise.resolve().then(() => (init_benchmark(), benchmark_exports));
3367
+ await benchMain(config);
3368
+ }
2844
3369
  async function start() {
2845
3370
  await Promise.resolve().then(() => (init_server(), server_exports));
2846
3371
  }
@@ -2877,6 +3402,13 @@ switch (command) {
2877
3402
  process.exit(1);
2878
3403
  });
2879
3404
  break;
3405
+ case "bench":
3406
+ case "benchmark":
3407
+ benchmark().catch((err) => {
3408
+ console.error("Fatal:", err);
3409
+ process.exit(1);
3410
+ });
3411
+ break;
2880
3412
  case "start":
2881
3413
  start().catch((err) => {
2882
3414
  console.error("Fatal:", err);