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/README.md +117 -338
- package/benchmark.ts +4 -2
- package/cli.ts +18 -2
- package/commands/bench.md +24 -0
- package/dist/benchmark.js +1284 -0
- package/dist/cli.js +761 -229
- package/package.json +1 -1
- package/tsup.config.ts +1 -0
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
|
|
17
|
-
const
|
|
18
|
-
const toolIsEmbedded = toolDir.startsWith(
|
|
19
|
-
const toolRelPath = toolIsEmbedded ? path.relative(
|
|
20
|
-
return { projectRoot:
|
|
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,
|
|
115
|
-
if (!resolvedPath.startsWith(
|
|
116
|
-
const rel2 = path2.relative(
|
|
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(
|
|
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(
|
|
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(
|
|
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,
|
|
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,
|
|
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(
|
|
153
|
+
function createResolver(projectRoot3, tsconfigPath3) {
|
|
154
154
|
return new ResolverFactory({
|
|
155
155
|
tsconfig: {
|
|
156
|
-
configFile: path2.resolve(
|
|
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,
|
|
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,
|
|
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(
|
|
224
|
+
async function buildGraph(projectRoot3, tsconfigPath3) {
|
|
225
225
|
const startTime = performance.now();
|
|
226
|
-
const resolver = createResolver(
|
|
227
|
-
const fileList = discoverFiles(
|
|
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,
|
|
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,
|
|
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,
|
|
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(
|
|
319
|
+
function startWatcher(projectRoot3, graph, resolver) {
|
|
320
320
|
try {
|
|
321
321
|
const watcher = fs.watch(
|
|
322
|
-
|
|
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(
|
|
333
|
+
const absPath2 = path2.resolve(projectRoot3, filename);
|
|
334
334
|
if (fs.existsSync(absPath2)) {
|
|
335
|
-
updateFile(graph, absPath2, resolver,
|
|
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(
|
|
398
|
-
return new Promise((
|
|
397
|
+
function testTsserver(projectRoot3) {
|
|
398
|
+
return new Promise((resolve9) => {
|
|
399
399
|
let tsserverPath;
|
|
400
400
|
try {
|
|
401
|
-
const require2 = createRequire(path3.resolve(
|
|
401
|
+
const require2 = createRequire(path3.resolve(projectRoot3, "package.json"));
|
|
402
402
|
tsserverPath = require2.resolve("typescript/lib/tsserver.js");
|
|
403
403
|
} catch {
|
|
404
|
-
|
|
404
|
+
resolve9(false);
|
|
405
405
|
return;
|
|
406
406
|
}
|
|
407
407
|
const child = spawn("node", [tsserverPath, "--disableAutomaticTypingAcquisition"], {
|
|
408
|
-
cwd:
|
|
408
|
+
cwd: projectRoot3,
|
|
409
409
|
stdio: ["pipe", "pipe", "pipe"]
|
|
410
410
|
});
|
|
411
411
|
const timeout = setTimeout(() => {
|
|
412
412
|
child.kill();
|
|
413
|
-
|
|
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
|
-
|
|
421
|
+
resolve9(true);
|
|
422
422
|
}
|
|
423
423
|
});
|
|
424
424
|
child.on("error", () => {
|
|
425
425
|
clearTimeout(timeout);
|
|
426
|
-
|
|
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:
|
|
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: ${
|
|
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(
|
|
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(
|
|
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(
|
|
497
|
+
const tsconfigAbs = path3.resolve(projectRoot3, tsconfigPath3);
|
|
498
498
|
if (fs2.existsSync(tsconfigAbs)) {
|
|
499
|
-
pass(`tsconfig.json exists at ${
|
|
499
|
+
pass(`tsconfig.json exists at ${tsconfigPath3}`);
|
|
500
500
|
} else {
|
|
501
|
-
fail(`tsconfig.json not found at ${
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
749
|
-
this.projectRoot =
|
|
750
|
-
this.tsconfigPath =
|
|
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((
|
|
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:
|
|
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,
|
|
1291
|
-
return path6.relative(
|
|
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:
|
|
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: ${
|
|
1369
|
+
console.log(`Project root: ${projectRoot3}`);
|
|
1370
1370
|
console.log("");
|
|
1371
|
-
const testFile = findTestFile(
|
|
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,
|
|
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(
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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(
|
|
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
|
|
1671
|
-
import * as
|
|
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 =
|
|
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
|
|
1734
|
-
return
|
|
2252
|
+
function relPath2(absPath2) {
|
|
2253
|
+
return path8.relative(projectRoot2, absPath2);
|
|
1735
2254
|
}
|
|
1736
2255
|
function absPath(file) {
|
|
1737
|
-
return
|
|
2256
|
+
return path8.isAbsolute(file) ? file : path8.resolve(projectRoot2, file);
|
|
1738
2257
|
}
|
|
1739
|
-
async function
|
|
2258
|
+
async function main4() {
|
|
1740
2259
|
log3("Starting TypeGraph MCP server...");
|
|
1741
|
-
log3(`Project root: ${
|
|
1742
|
-
log3(`tsconfig: ${
|
|
2260
|
+
log3(`Project root: ${projectRoot2}`);
|
|
2261
|
+
log3(`tsconfig: ${tsconfigPath2}`);
|
|
1743
2262
|
const [, graphResult] = await Promise.all([
|
|
1744
2263
|
client.start(),
|
|
1745
|
-
buildGraph(
|
|
2264
|
+
buildGraph(projectRoot2, tsconfigPath2)
|
|
1746
2265
|
]);
|
|
1747
2266
|
moduleGraph = graphResult.graph;
|
|
1748
|
-
startWatcher(
|
|
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
|
|
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(
|
|
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:
|
|
2624
|
+
root: relPath2(result.root),
|
|
2106
2625
|
nodes: result.nodes,
|
|
2107
|
-
files: result.files.map(
|
|
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(
|
|
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:
|
|
2652
|
+
root: relPath2(result.root),
|
|
2134
2653
|
nodes: result.nodes,
|
|
2135
2654
|
directCount: result.directCount,
|
|
2136
|
-
files: result.files.map(
|
|
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(
|
|
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(
|
|
2703
|
+
path: result.path?.map(relPath2) ?? null,
|
|
2185
2704
|
hops: result.hops,
|
|
2186
2705
|
chain: result.chain.map((c) => ({
|
|
2187
|
-
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(
|
|
2730
|
+
nodes: result.nodes.map(relPath2),
|
|
2212
2731
|
edges: result.edges.map((e) => ({
|
|
2213
|
-
from:
|
|
2214
|
-
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:
|
|
2241
|
-
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:
|
|
2246
|
-
to:
|
|
2764
|
+
from: relPath2(e.from),
|
|
2765
|
+
to: relPath2(e.to),
|
|
2247
2766
|
specifiers: e.specifiers
|
|
2248
2767
|
})),
|
|
2249
|
-
sharedDependencies: result.sharedDependencies.map(
|
|
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
|
-
|
|
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
|
|
2276
|
-
import * as
|
|
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) =>
|
|
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) =>
|
|
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) =>
|
|
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) =>
|
|
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) =>
|
|
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 =
|
|
2377
|
-
if (!
|
|
2378
|
-
|
|
2896
|
+
const destDir = path9.dirname(dest);
|
|
2897
|
+
if (!fs8.existsSync(destDir)) {
|
|
2898
|
+
fs8.mkdirSync(destDir, { recursive: true });
|
|
2379
2899
|
}
|
|
2380
|
-
|
|
2900
|
+
fs8.copyFileSync(src, dest);
|
|
2381
2901
|
if (src.endsWith(".sh")) {
|
|
2382
|
-
|
|
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(
|
|
2913
|
+
function registerMcpServers(projectRoot3, selectedAgents) {
|
|
2394
2914
|
if (selectedAgents.includes("cursor")) {
|
|
2395
|
-
registerJsonMcp(
|
|
2915
|
+
registerJsonMcp(projectRoot3, ".cursor/mcp.json", "mcpServers");
|
|
2396
2916
|
}
|
|
2397
2917
|
if (selectedAgents.includes("codex")) {
|
|
2398
|
-
registerCodexMcp(
|
|
2918
|
+
registerCodexMcp(projectRoot3);
|
|
2399
2919
|
}
|
|
2400
2920
|
if (selectedAgents.includes("copilot")) {
|
|
2401
|
-
registerJsonMcp(
|
|
2921
|
+
registerJsonMcp(projectRoot3, ".vscode/mcp.json", "servers");
|
|
2402
2922
|
}
|
|
2403
2923
|
}
|
|
2404
|
-
function deregisterMcpServers(
|
|
2405
|
-
deregisterJsonMcp(
|
|
2406
|
-
deregisterCodexMcp(
|
|
2407
|
-
deregisterJsonMcp(
|
|
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(
|
|
2410
|
-
const fullPath =
|
|
2929
|
+
function registerJsonMcp(projectRoot3, configPath, rootKey) {
|
|
2930
|
+
const fullPath = path9.resolve(projectRoot3, configPath);
|
|
2411
2931
|
let config = {};
|
|
2412
|
-
if (
|
|
2932
|
+
if (fs8.existsSync(fullPath)) {
|
|
2413
2933
|
try {
|
|
2414
|
-
config = JSON.parse(
|
|
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 =
|
|
2428
|
-
if (!
|
|
2429
|
-
|
|
2947
|
+
const dir = path9.dirname(fullPath);
|
|
2948
|
+
if (!fs8.existsSync(dir)) {
|
|
2949
|
+
fs8.mkdirSync(dir, { recursive: true });
|
|
2430
2950
|
}
|
|
2431
|
-
|
|
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(
|
|
2435
|
-
const fullPath =
|
|
2436
|
-
if (!
|
|
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(
|
|
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
|
-
|
|
2966
|
+
fs8.unlinkSync(fullPath);
|
|
2447
2967
|
} else {
|
|
2448
|
-
|
|
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(
|
|
2974
|
+
function registerCodexMcp(projectRoot3) {
|
|
2455
2975
|
const configPath = ".codex/config.toml";
|
|
2456
|
-
const fullPath =
|
|
2976
|
+
const fullPath = path9.resolve(projectRoot3, configPath);
|
|
2457
2977
|
let content = "";
|
|
2458
|
-
if (
|
|
2459
|
-
content =
|
|
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 =
|
|
2474
|
-
if (!
|
|
2475
|
-
|
|
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
|
-
|
|
2998
|
+
fs8.writeFileSync(fullPath, newContent);
|
|
2479
2999
|
p.log.success(`${configPath}: registered typegraph MCP server`);
|
|
2480
3000
|
}
|
|
2481
|
-
function deregisterCodexMcp(
|
|
3001
|
+
function deregisterCodexMcp(projectRoot3) {
|
|
2482
3002
|
const configPath = ".codex/config.toml";
|
|
2483
|
-
const fullPath =
|
|
2484
|
-
if (!
|
|
2485
|
-
let content =
|
|
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
|
-
|
|
3013
|
+
fs8.unlinkSync(fullPath);
|
|
2494
3014
|
} else {
|
|
2495
|
-
|
|
3015
|
+
fs8.writeFileSync(fullPath, content);
|
|
2496
3016
|
}
|
|
2497
3017
|
p.log.info(`${configPath}: removed typegraph MCP server`);
|
|
2498
3018
|
}
|
|
2499
|
-
function ensureTsconfigExclude(
|
|
2500
|
-
const
|
|
2501
|
-
if (!
|
|
3019
|
+
function ensureTsconfigExclude(projectRoot3) {
|
|
3020
|
+
const tsconfigPath3 = path9.resolve(projectRoot3, "tsconfig.json");
|
|
3021
|
+
if (!fs8.existsSync(tsconfigPath3)) return;
|
|
2502
3022
|
try {
|
|
2503
|
-
const raw =
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
2539
|
-
return AGENT_IDS.filter((id) => AGENTS[id].detect(
|
|
3058
|
+
function detectAgents(projectRoot3) {
|
|
3059
|
+
return AGENT_IDS.filter((id) => AGENTS[id].detect(projectRoot3));
|
|
2540
3060
|
}
|
|
2541
|
-
async function selectAgents(
|
|
2542
|
-
const detected = detectAgents(
|
|
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 =
|
|
2569
|
-
const
|
|
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: ${
|
|
2573
|
-
const pkgJsonPath =
|
|
2574
|
-
const
|
|
2575
|
-
if (!
|
|
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 (!
|
|
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 =
|
|
2585
|
-
const isUpdate =
|
|
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(
|
|
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(
|
|
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 =
|
|
2624
|
-
const dest =
|
|
2625
|
-
if (
|
|
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
|
-
|
|
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 =
|
|
3163
|
+
const agentsSkillsDir = path9.resolve(projectRoot3, ".agents/skills");
|
|
2644
3164
|
let copiedSkills = 0;
|
|
2645
3165
|
for (const skill of SKILL_NAMES) {
|
|
2646
|
-
const src =
|
|
2647
|
-
const destDir =
|
|
2648
|
-
const dest =
|
|
2649
|
-
if (!
|
|
2650
|
-
if (
|
|
2651
|
-
const srcContent =
|
|
2652
|
-
const destContent =
|
|
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
|
-
|
|
2656
|
-
|
|
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 =
|
|
2667
|
-
if (
|
|
3186
|
+
const mcpJsonPath = path9.resolve(projectRoot3, ".claude/mcp.json");
|
|
3187
|
+
if (fs8.existsSync(mcpJsonPath)) {
|
|
2668
3188
|
try {
|
|
2669
|
-
const mcpJson = JSON.parse(
|
|
3189
|
+
const mcpJson = JSON.parse(fs8.readFileSync(mcpJsonPath, "utf-8"));
|
|
2670
3190
|
if (mcpJson.mcpServers?.["typegraph"]) {
|
|
2671
3191
|
delete mcpJson.mcpServers["typegraph"];
|
|
2672
|
-
|
|
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(
|
|
2680
|
-
registerMcpServers(
|
|
2681
|
-
ensureTsconfigExclude(
|
|
3199
|
+
await setupAgentInstructions(projectRoot3, selectedAgents);
|
|
3200
|
+
registerMcpServers(projectRoot3, selectedAgents);
|
|
3201
|
+
ensureTsconfigExclude(projectRoot3);
|
|
2682
3202
|
await runVerification(targetDir, selectedAgents);
|
|
2683
3203
|
}
|
|
2684
|
-
async function removePlugin(
|
|
3204
|
+
async function removePlugin(projectRoot3, pluginDir) {
|
|
2685
3205
|
const s = p.spinner();
|
|
2686
3206
|
s.start("Removing typegraph-mcp...");
|
|
2687
|
-
if (
|
|
2688
|
-
|
|
3207
|
+
if (fs8.existsSync(pluginDir)) {
|
|
3208
|
+
fs8.rmSync(pluginDir, { recursive: true });
|
|
2689
3209
|
}
|
|
2690
|
-
const agentsSkillsDir =
|
|
3210
|
+
const agentsSkillsDir = path9.resolve(projectRoot3, ".agents/skills");
|
|
2691
3211
|
for (const skill of SKILL_NAMES) {
|
|
2692
|
-
const skillDir =
|
|
2693
|
-
if (
|
|
2694
|
-
|
|
3212
|
+
const skillDir = path9.join(agentsSkillsDir, skill);
|
|
3213
|
+
if (fs8.existsSync(skillDir)) {
|
|
3214
|
+
fs8.rmSync(skillDir, { recursive: true });
|
|
2695
3215
|
}
|
|
2696
3216
|
}
|
|
2697
|
-
if (
|
|
2698
|
-
|
|
2699
|
-
const agentsDir =
|
|
2700
|
-
if (
|
|
2701
|
-
|
|
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 =
|
|
2708
|
-
if (!
|
|
2709
|
-
const realPath =
|
|
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 =
|
|
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
|
-
|
|
3236
|
+
fs8.writeFileSync(realPath, content);
|
|
2717
3237
|
}
|
|
2718
3238
|
}
|
|
2719
|
-
const claudeMdPath =
|
|
2720
|
-
if (
|
|
2721
|
-
let content =
|
|
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
|
-
|
|
3243
|
+
fs8.writeFileSync(claudeMdPath, content);
|
|
2724
3244
|
}
|
|
2725
3245
|
s.stop("Removed typegraph-mcp");
|
|
2726
|
-
deregisterMcpServers(
|
|
3246
|
+
deregisterMcpServers(projectRoot3);
|
|
2727
3247
|
p.outro("typegraph-mcp has been uninstalled from this project.");
|
|
2728
3248
|
}
|
|
2729
|
-
async function setupAgentInstructions(
|
|
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 =
|
|
2738
|
-
if (!
|
|
2739
|
-
const realPath =
|
|
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 =
|
|
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 =
|
|
3280
|
+
const content = fs8.readFileSync(target.realPath, "utf-8");
|
|
2761
3281
|
const appendContent = (content.endsWith("\n") ? "" : "\n") + "\n" + AGENT_SNIPPET;
|
|
2762
|
-
|
|
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 =
|
|
2767
|
-
if (
|
|
2768
|
-
let content =
|
|
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
|
-
|
|
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
|
|
2811
|
-
const pluginDir =
|
|
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 (!
|
|
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(
|
|
3345
|
+
await removePlugin(projectRoot3, pluginDir);
|
|
2826
3346
|
}
|
|
2827
3347
|
function resolvePluginDir() {
|
|
2828
|
-
const installed =
|
|
2829
|
-
if (
|
|
2830
|
-
return
|
|
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);
|