typegraph-mcp 0.9.37 → 0.9.39

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/check.ts CHANGED
@@ -209,6 +209,44 @@ function hasDeclaredDependency(packageJson: Record<string, unknown> | null, pack
209
209
  });
210
210
  }
211
211
 
212
+ const ESLINT_CONFIG_NAMES = [
213
+ "eslint.config.mjs",
214
+ "eslint.config.js",
215
+ "eslint.config.ts",
216
+ "eslint.config.cjs",
217
+ ];
218
+ const OXLINT_CONFIG_NAMES = [
219
+ ".oxlintrc.json",
220
+ "oxlint.config.ts",
221
+ "oxlint.config.js",
222
+ "oxlint.config.mjs",
223
+ "oxlint.config.cjs",
224
+ ];
225
+
226
+ type LintConfigCheck =
227
+ | { tool: "ESLint"; fileName: string; fullPath: string; propertyName: "ignores" }
228
+ | { tool: "Oxlint"; fileName: string; fullPath: string; propertyName: "ignorePatterns" };
229
+
230
+ function findLintConfigs(projectRoot: string): LintConfigCheck[] {
231
+ const configs: LintConfigCheck[] = [];
232
+
233
+ for (const fileName of ESLINT_CONFIG_NAMES) {
234
+ const fullPath = path.resolve(projectRoot, fileName);
235
+ if (fs.existsSync(fullPath)) {
236
+ configs.push({ tool: "ESLint", fileName, fullPath, propertyName: "ignores" });
237
+ }
238
+ }
239
+
240
+ for (const fileName of OXLINT_CONFIG_NAMES) {
241
+ const fullPath = path.resolve(projectRoot, fileName);
242
+ if (fs.existsSync(fullPath)) {
243
+ configs.push({ tool: "Oxlint", fileName, fullPath, propertyName: "ignorePatterns" });
244
+ }
245
+ }
246
+
247
+ return configs;
248
+ }
249
+
212
250
  // ─── Main ────────────────────────────────────────────────────────────────────
213
251
 
214
252
  export async function main(configOverride?: TypegraphConfig): Promise<CheckResult> {
@@ -527,31 +565,32 @@ export async function main(configOverride?: TypegraphConfig): Promise<CheckResul
527
565
  );
528
566
  }
529
567
 
530
- // 11. ESLint ignores (only when typegraph-mcp is embedded inside the project)
568
+ // 11. Lint ignores (only when typegraph-mcp is embedded inside the project)
531
569
  if (toolIsEmbedded) {
532
- const eslintConfigNames = ["eslint.config.mjs", "eslint.config.js", "eslint.config.ts", "eslint.config.cjs"];
533
- const eslintConfigFile = eslintConfigNames.find((name) => fs.existsSync(path.resolve(projectRoot, name)));
534
- if (eslintConfigFile) {
535
- const eslintConfigPath = path.resolve(projectRoot, eslintConfigFile);
536
- const eslintContent = fs.readFileSync(eslintConfigPath, "utf-8");
570
+ const lintConfigs = findLintConfigs(projectRoot);
571
+ if (lintConfigs.length > 0) {
537
572
  // Determine the parent directory (e.g. "plugins") for the ignore pattern
538
573
  const parentDir = path.basename(path.dirname(toolDir));
539
574
  const parentIgnorePattern = new RegExp(`["']${parentDir}\\/\\*\\*["']`);
540
- const hasParentIgnore = parentIgnorePattern.test(eslintContent);
541
575
 
542
- if (hasParentIgnore) {
543
- pass(`ESLint ignores ${parentDir}/`);
544
- } else {
545
- fail(
546
- `ESLint missing ignore: "${parentDir}/**"`,
547
- `Add to the ignores array in ${eslintConfigFile}:\n "${parentDir}/**",`
548
- );
576
+ for (const config of lintConfigs) {
577
+ const content = fs.readFileSync(config.fullPath, "utf-8");
578
+ const hasParentIgnore = parentIgnorePattern.test(content);
579
+
580
+ if (hasParentIgnore) {
581
+ pass(`${config.tool} ignores ${parentDir}/ (${config.fileName})`);
582
+ } else {
583
+ fail(
584
+ `${config.tool} missing ignore: "${parentDir}/**" (${config.fileName})`,
585
+ `Add to ${config.propertyName} in ${config.fileName}:\n "${parentDir}/**",`
586
+ );
587
+ }
549
588
  }
550
589
  } else {
551
- skip("ESLint config check (no eslint flat config found)");
590
+ skip("Lint config check (no ESLint or Oxlint config found)");
552
591
  }
553
592
  } else {
554
- skip("ESLint config check (typegraph-mcp is external to project)");
593
+ skip("Lint config check (typegraph-mcp is external to project)");
555
594
  }
556
595
 
557
596
  // 12. .gitignore check (optional)
package/cli.ts CHANGED
@@ -59,6 +59,8 @@ Where suitable, use the \`ts_*\` MCP tools instead of grep/glob for navigating T
59
59
 
60
60
  Start with the navigation tools before reading entire files. Use direct file reads only after the MCP tools identify the exact symbols or lines that matter.
61
61
 
62
+ For quick architectural insight, prefer composition modules and entrypoints over top-level barrel files. If \`ts_module_exports\` on an \`index.ts\` or other barrel looks empty or uninformative, pivot to the app entrypoint, router, handler, service composition root, or API module that wires real behavior together.
63
+
62
64
  Use \`rg\` or \`grep\` when semantic symbol navigation is not the right tool, especially for:
63
65
 
64
66
  - docs, config, SQL, migrations, JSON, env vars, route strings, and other non-TypeScript assets
@@ -599,19 +601,13 @@ function ensureTsconfigExclude(projectRoot: string): void {
599
601
 
600
602
  try {
601
603
  const raw = fs.readFileSync(tsconfigPath, "utf-8");
602
- // Strip single-line comments (// ...) and trailing commas for JSON.parse
603
- const stripped = raw
604
- .replace(/\/\/.*$/gm, "")
605
- .replace(/,(\s*[}\]])/g, "$1");
606
- const tsconfig = JSON.parse(stripped);
607
-
608
- const exclude: string[] = tsconfig.exclude || [];
609
- if (exclude.some((e: string) => e === "plugins" || e === "plugins/**" || e === "plugins/*")) {
610
- return; // Already excluded
604
+ const excludeArrayMatch = raw.match(/("exclude"\s*:\s*\[)([\s\S]*?)(\])/);
605
+ if (excludeArrayMatch && /["']plugins(?:\/\*\*|\/\*|)["']/.test(excludeArrayMatch[2])) {
606
+ return;
611
607
  }
612
608
 
613
609
  // Insert "plugins/**" into the exclude array in the original file
614
- if (raw.includes('"exclude"')) {
610
+ if (excludeArrayMatch) {
615
611
  // Existing exclude array — append to it
616
612
  const updated = raw.replace(
617
613
  /("exclude"\s*:\s*\[)([\s\S]*?)(\])/,
@@ -640,48 +636,137 @@ function ensureTsconfigExclude(projectRoot: string): void {
640
636
  }
641
637
  }
642
638
 
643
- // ─── ESLint Ignore ───────────────────────────────────────────────────────────
639
+ // ─── Lint Ignore ─────────────────────────────────────────────────────────────
644
640
 
645
- function ensureEslintIgnore(projectRoot: string): void {
646
- const eslintConfigNames = ["eslint.config.mjs", "eslint.config.js", "eslint.config.ts", "eslint.config.cjs"];
647
- const eslintConfigFile = eslintConfigNames.find((name) => fs.existsSync(path.resolve(projectRoot, name)));
648
- if (!eslintConfigFile) return;
649
- const eslintConfigPath = path.resolve(projectRoot, eslintConfigFile);
641
+ const ESLINT_CONFIG_NAMES = [
642
+ "eslint.config.mjs",
643
+ "eslint.config.js",
644
+ "eslint.config.ts",
645
+ "eslint.config.cjs",
646
+ ];
647
+ const OXLINT_CONFIG_NAMES = [
648
+ ".oxlintrc.json",
649
+ "oxlint.config.ts",
650
+ "oxlint.config.js",
651
+ "oxlint.config.mjs",
652
+ "oxlint.config.cjs",
653
+ ];
650
654
 
651
- try {
652
- const raw = fs.readFileSync(eslintConfigPath, "utf-8");
653
- const pattern = /["']plugins\/\*\*["']/;
654
- if (pattern.test(raw)) return; // Already ignored
655
+ type LintConfig =
656
+ | { tool: "ESLint"; fileName: string; fullPath: string; format: "flat" }
657
+ | { tool: "Oxlint"; fileName: string; fullPath: string; format: "json" | "module" };
655
658
 
656
- // Strategy 1: Append to an existing ignores array
657
- const ignoresArrayRe = /(ignores\s*:\s*\[)([\s\S]*?)(\])/;
658
- const match = raw.match(ignoresArrayRe);
659
- if (match) {
660
- const updated = raw.replace(ignoresArrayRe, (_m, open, items, close) => {
661
- const trimmed = items.trimEnd();
662
- const needsComma = trimmed.length > 0 && !trimmed.endsWith(",");
663
- return `${open}${items.trimEnd()}${needsComma ? "," : ""} "plugins/**"${close}`;
664
- });
665
- fs.writeFileSync(eslintConfigPath, updated);
666
- p.log.success(`Added "plugins/**" to ${eslintConfigFile} ignores`);
667
- return;
659
+ function findLintConfigs(projectRoot: string): LintConfig[] {
660
+ const configs: LintConfig[] = [];
661
+
662
+ for (const fileName of ESLINT_CONFIG_NAMES) {
663
+ const fullPath = path.resolve(projectRoot, fileName);
664
+ if (fs.existsSync(fullPath)) {
665
+ configs.push({ tool: "ESLint", fileName, fullPath, format: "flat" });
668
666
  }
667
+ }
669
668
 
670
- // Strategy 2: Insert a new ignores object at the start of the exported array
671
- // Matches: export default [ or export default tseslint.config(
672
- const exportArrayRe = /(export\s+default\s+(?:\w+\.config\(|\[))\s*\n?/;
673
- if (exportArrayRe.test(raw)) {
674
- const updated = raw.replace(exportArrayRe, (m) => {
675
- return `${m} { ignores: ["plugins/**"] },\n`;
669
+ for (const fileName of OXLINT_CONFIG_NAMES) {
670
+ const fullPath = path.resolve(projectRoot, fileName);
671
+ if (fs.existsSync(fullPath)) {
672
+ configs.push({
673
+ tool: "Oxlint",
674
+ fileName,
675
+ fullPath,
676
+ format: fileName.endsWith(".json") ? "json" : "module",
676
677
  });
677
- fs.writeFileSync(eslintConfigPath, updated);
678
- p.log.success(`Added "plugins/**" to ${eslintConfigFile} ignores`);
679
- return;
680
678
  }
679
+ }
681
680
 
682
- p.log.warn(`Could not patch ${eslintConfigFile} — manually add "plugins/**" to the ignores array`);
683
- } catch {
684
- p.log.warn(`Could not update ${eslintConfigFile} — manually add "plugins/**" to the ignores array`);
681
+ return configs;
682
+ }
683
+
684
+ function appendToArrayLiteral(raw: string, propertyPattern: RegExp, valueLiteral: string): string | null {
685
+ if (!propertyPattern.test(raw)) return null;
686
+ return raw.replace(propertyPattern, (_match, open, items, close) => {
687
+ const trimmed = items.trimEnd();
688
+ const needsComma = trimmed.length > 0 && !trimmed.endsWith(",");
689
+ return `${open}${items.trimEnd()}${needsComma ? "," : ""} ${valueLiteral}${close}`;
690
+ });
691
+ }
692
+
693
+ function insertTopLevelJsonArrayProperty(raw: string, propertyName: string, valueLiteral: string): string | null {
694
+ const lastBrace = raw.lastIndexOf("}");
695
+ if (lastBrace === -1) return null;
696
+ const before = raw.slice(0, lastBrace).trimEnd();
697
+ const needsComma = !before.endsWith(",") && !before.endsWith("{");
698
+ return `${before}${needsComma ? "," : ""}\n "${propertyName}": [${valueLiteral}]\n}\n`;
699
+ }
700
+
701
+ function patchEslintConfig(raw: string): string | null {
702
+ const updatedIgnores = appendToArrayLiteral(raw, /(ignores\s*:\s*\[)([\s\S]*?)(\])/, '"plugins/**"');
703
+ if (updatedIgnores) return updatedIgnores;
704
+
705
+ // Matches: export default [ or export default tseslint.config(
706
+ const exportArrayRe = /(export\s+default\s+(?:\w+\.config\(|\[))\s*\n?/;
707
+ if (exportArrayRe.test(raw)) {
708
+ return raw.replace(exportArrayRe, (match) => `${match} { ignores: ["plugins/**"] },\n`);
709
+ }
710
+
711
+ return null;
712
+ }
713
+
714
+ function patchOxlintJsonConfig(raw: string): string | null {
715
+ const updatedIgnores = appendToArrayLiteral(
716
+ raw,
717
+ /("ignorePatterns"\s*:\s*\[)([\s\S]*?)(\])/,
718
+ '"plugins/**"'
719
+ );
720
+ if (updatedIgnores) return updatedIgnores;
721
+ return insertTopLevelJsonArrayProperty(raw, "ignorePatterns", '"plugins/**"');
722
+ }
723
+
724
+ function patchOxlintModuleConfig(raw: string): string | null {
725
+ const updatedIgnores = appendToArrayLiteral(
726
+ raw,
727
+ /(ignorePatterns\s*:\s*\[)([\s\S]*?)(\])/,
728
+ '"plugins/**"'
729
+ );
730
+ if (updatedIgnores) return updatedIgnores;
731
+
732
+ const exportObjectRe = /(export\s+default\s*\{)\s*\n?/;
733
+ if (exportObjectRe.test(raw)) {
734
+ return raw.replace(exportObjectRe, (match) => `${match}\n ignorePatterns: ["plugins/**"],`);
735
+ }
736
+
737
+ return null;
738
+ }
739
+
740
+ function ensureLintIgnores(projectRoot: string): void {
741
+ const configs = findLintConfigs(projectRoot);
742
+ for (const config of configs) {
743
+ try {
744
+ const raw = fs.readFileSync(config.fullPath, "utf-8");
745
+ if (/["']plugins\/\*\*["']/.test(raw)) continue;
746
+
747
+ const updated =
748
+ config.tool === "ESLint"
749
+ ? patchEslintConfig(raw)
750
+ : config.format === "json"
751
+ ? patchOxlintJsonConfig(raw)
752
+ : patchOxlintModuleConfig(raw);
753
+
754
+ if (updated) {
755
+ fs.writeFileSync(config.fullPath, updated);
756
+ const propertyName = config.tool === "ESLint" ? "ignores" : "ignorePatterns";
757
+ p.log.success(`Added "plugins/**" to ${config.fileName} ${propertyName}`);
758
+ } else {
759
+ const propertyName = config.tool === "ESLint" ? "ignores" : "ignorePatterns";
760
+ p.log.warn(
761
+ `Could not patch ${config.fileName} — manually add "plugins/**" to ${propertyName}`
762
+ );
763
+ }
764
+ } catch {
765
+ const propertyName = config.tool === "ESLint" ? "ignores" : "ignorePatterns";
766
+ p.log.warn(
767
+ `Could not update ${config.fileName} — manually add "plugins/**" to ${propertyName}`
768
+ );
769
+ }
685
770
  }
686
771
  }
687
772
 
@@ -918,8 +1003,8 @@ async function setup(yes: boolean): Promise<void> {
918
1003
  // 8. Ensure plugins/ is excluded from tsconfig
919
1004
  ensureTsconfigExclude(projectRoot);
920
1005
 
921
- // 9. Ensure plugins/ is ignored by ESLint
922
- ensureEslintIgnore(projectRoot);
1006
+ // 9. Ensure plugins/ is ignored by supported lint configs
1007
+ ensureLintIgnores(projectRoot);
923
1008
 
924
1009
  // 10. Verification
925
1010
  await runVerification(targetDir, selectedAgents);
package/dist/benchmark.js CHANGED
@@ -387,7 +387,7 @@ function distToSource(resolvedPath, projectRoot2) {
387
387
  }
388
388
  return resolvedPath;
389
389
  }
390
- function resolveImport(resolver, fromDir, specifier, projectRoot2) {
390
+ function resolveProjectImport(resolver, fromDir, specifier, projectRoot2) {
391
391
  try {
392
392
  const result = resolver.sync(fromDir, specifier);
393
393
  if (result.path && !result.path.includes("node_modules")) {
@@ -438,7 +438,7 @@ function buildForwardEdges(files, resolver, projectRoot2) {
438
438
  const edges = [];
439
439
  const fromDir = path2.dirname(filePath);
440
440
  for (const raw of rawImports) {
441
- const target = resolveImport(resolver, fromDir, raw.specifier, projectRoot2);
441
+ const target = resolveProjectImport(resolver, fromDir, raw.specifier, projectRoot2);
442
442
  if (target) {
443
443
  edges.push({
444
444
  target,
package/dist/check.js CHANGED
@@ -15,6 +15,7 @@ __export(module_graph_exports, {
15
15
  createResolver: () => createResolver,
16
16
  discoverFiles: () => discoverFiles,
17
17
  removeFile: () => removeFile,
18
+ resolveProjectImport: () => resolveProjectImport,
18
19
  startWatcher: () => startWatcher,
19
20
  updateFile: () => updateFile
20
21
  });
@@ -120,7 +121,7 @@ function distToSource(resolvedPath, projectRoot) {
120
121
  }
121
122
  return resolvedPath;
122
123
  }
123
- function resolveImport(resolver, fromDir, specifier, projectRoot) {
124
+ function resolveProjectImport(resolver, fromDir, specifier, projectRoot) {
124
125
  try {
125
126
  const result = resolver.sync(fromDir, specifier);
126
127
  if (result.path && !result.path.includes("node_modules")) {
@@ -171,7 +172,7 @@ function buildForwardEdges(files, resolver, projectRoot) {
171
172
  const edges = [];
172
173
  const fromDir = path2.dirname(filePath);
173
174
  for (const raw of rawImports) {
174
- const target = resolveImport(resolver, fromDir, raw.specifier, projectRoot);
175
+ const target = resolveProjectImport(resolver, fromDir, raw.specifier, projectRoot);
175
176
  if (target) {
176
177
  edges.push({
177
178
  target,
@@ -252,7 +253,7 @@ function updateFile(graph, filePath, resolver, projectRoot) {
252
253
  const fromDir = path2.dirname(filePath);
253
254
  const newEdges = [];
254
255
  for (const raw of rawImports) {
255
- const target = resolveImport(resolver, fromDir, raw.specifier, projectRoot);
256
+ const target = resolveProjectImport(resolver, fromDir, raw.specifier, projectRoot);
256
257
  if (target) {
257
258
  newEdges.push({
258
259
  target,
@@ -506,6 +507,35 @@ function hasDeclaredDependency(packageJson, packageName) {
506
507
  return typeof deps === "object" && deps !== null && packageName in deps;
507
508
  });
508
509
  }
510
+ var ESLINT_CONFIG_NAMES = [
511
+ "eslint.config.mjs",
512
+ "eslint.config.js",
513
+ "eslint.config.ts",
514
+ "eslint.config.cjs"
515
+ ];
516
+ var OXLINT_CONFIG_NAMES = [
517
+ ".oxlintrc.json",
518
+ "oxlint.config.ts",
519
+ "oxlint.config.js",
520
+ "oxlint.config.mjs",
521
+ "oxlint.config.cjs"
522
+ ];
523
+ function findLintConfigs(projectRoot) {
524
+ const configs = [];
525
+ for (const fileName of ESLINT_CONFIG_NAMES) {
526
+ const fullPath = path3.resolve(projectRoot, fileName);
527
+ if (fs2.existsSync(fullPath)) {
528
+ configs.push({ tool: "ESLint", fileName, fullPath, propertyName: "ignores" });
529
+ }
530
+ }
531
+ for (const fileName of OXLINT_CONFIG_NAMES) {
532
+ const fullPath = path3.resolve(projectRoot, fileName);
533
+ if (fs2.existsSync(fullPath)) {
534
+ configs.push({ tool: "Oxlint", fileName, fullPath, propertyName: "ignorePatterns" });
535
+ }
536
+ }
537
+ return configs;
538
+ }
509
539
  async function main(configOverride) {
510
540
  const { projectRoot, tsconfigPath, toolDir, toolIsEmbedded, toolRelPath } = configOverride ?? resolveConfig(import.meta.dirname);
511
541
  let passed = 0;
@@ -783,28 +813,28 @@ async function main(configOverride) {
783
813
  );
784
814
  }
785
815
  if (toolIsEmbedded) {
786
- const eslintConfigNames = ["eslint.config.mjs", "eslint.config.js", "eslint.config.ts", "eslint.config.cjs"];
787
- const eslintConfigFile = eslintConfigNames.find((name) => fs2.existsSync(path3.resolve(projectRoot, name)));
788
- if (eslintConfigFile) {
789
- const eslintConfigPath = path3.resolve(projectRoot, eslintConfigFile);
790
- const eslintContent = fs2.readFileSync(eslintConfigPath, "utf-8");
816
+ const lintConfigs = findLintConfigs(projectRoot);
817
+ if (lintConfigs.length > 0) {
791
818
  const parentDir = path3.basename(path3.dirname(toolDir));
792
819
  const parentIgnorePattern = new RegExp(`["']${parentDir}\\/\\*\\*["']`);
793
- const hasParentIgnore = parentIgnorePattern.test(eslintContent);
794
- if (hasParentIgnore) {
795
- pass(`ESLint ignores ${parentDir}/`);
796
- } else {
797
- fail(
798
- `ESLint missing ignore: "${parentDir}/**"`,
799
- `Add to the ignores array in ${eslintConfigFile}:
820
+ for (const config of lintConfigs) {
821
+ const content = fs2.readFileSync(config.fullPath, "utf-8");
822
+ const hasParentIgnore = parentIgnorePattern.test(content);
823
+ if (hasParentIgnore) {
824
+ pass(`${config.tool} ignores ${parentDir}/ (${config.fileName})`);
825
+ } else {
826
+ fail(
827
+ `${config.tool} missing ignore: "${parentDir}/**" (${config.fileName})`,
828
+ `Add to ${config.propertyName} in ${config.fileName}:
800
829
  "${parentDir}/**",`
801
- );
830
+ );
831
+ }
802
832
  }
803
833
  } else {
804
- skip("ESLint config check (no eslint flat config found)");
834
+ skip("Lint config check (no ESLint or Oxlint config found)");
805
835
  }
806
836
  } else {
807
- skip("ESLint config check (typegraph-mcp is external to project)");
837
+ skip("Lint config check (typegraph-mcp is external to project)");
808
838
  }
809
839
  const gitignorePath = path3.resolve(projectRoot, ".gitignore");
810
840
  if (fs2.existsSync(gitignorePath)) {