@adhisang/minecraft-modding-mcp 3.1.0 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/CHANGELOG.md +62 -34
  2. package/README.md +79 -100
  3. package/dist/access-transformer-parser.d.ts +17 -0
  4. package/dist/access-transformer-parser.js +97 -0
  5. package/dist/concurrency.d.ts +1 -0
  6. package/dist/concurrency.js +24 -0
  7. package/dist/config.js +19 -11
  8. package/dist/decompiler/vineflower.js +22 -21
  9. package/dist/entry-tools/analyze-mod-service.d.ts +4 -4
  10. package/dist/entry-tools/analyze-symbol-service.d.ts +22 -20
  11. package/dist/entry-tools/analyze-symbol-service.js +6 -3
  12. package/dist/entry-tools/inspect-minecraft-service.d.ts +166 -149
  13. package/dist/entry-tools/inspect-minecraft-service.js +318 -55
  14. package/dist/entry-tools/validate-project-service.d.ts +153 -16
  15. package/dist/entry-tools/validate-project-service.js +360 -23
  16. package/dist/gradle-paths.d.ts +4 -0
  17. package/dist/gradle-paths.js +57 -0
  18. package/dist/index.js +274 -13
  19. package/dist/mapping-pipeline-service.d.ts +3 -1
  20. package/dist/mapping-pipeline-service.js +16 -1
  21. package/dist/mapping-service.d.ts +5 -0
  22. package/dist/mapping-service.js +200 -84
  23. package/dist/minecraft-explorer-service.d.ts +13 -0
  24. package/dist/minecraft-explorer-service.js +8 -4
  25. package/dist/mixin-validator.d.ts +33 -2
  26. package/dist/mixin-validator.js +197 -11
  27. package/dist/mod-analyzer.d.ts +1 -0
  28. package/dist/mod-analyzer.js +17 -1
  29. package/dist/mod-decompile-service.js +4 -4
  30. package/dist/mod-remap-service.js +1 -54
  31. package/dist/mod-search-service.d.ts +1 -0
  32. package/dist/mod-search-service.js +84 -51
  33. package/dist/response-utils.d.ts +35 -0
  34. package/dist/response-utils.js +113 -0
  35. package/dist/source-jar-reader.d.ts +16 -0
  36. package/dist/source-jar-reader.js +103 -1
  37. package/dist/source-resolver.js +9 -10
  38. package/dist/source-service.d.ts +24 -2
  39. package/dist/source-service.js +1052 -139
  40. package/dist/tool-contract-manifest.js +74 -74
  41. package/dist/types.d.ts +17 -0
  42. package/dist/workspace-mapping-service.d.ts +13 -0
  43. package/dist/workspace-mapping-service.js +146 -14
  44. package/package.json +1 -1
@@ -133,6 +133,21 @@ function allMethodNames(members) {
133
133
  function allFieldNames(members) {
134
134
  return members.fields.map((m) => m.name);
135
135
  }
136
+ function accessLevelFromFlags(accessFlags) {
137
+ if (accessFlags == null) {
138
+ return undefined;
139
+ }
140
+ if ((accessFlags & 0x0001) !== 0) {
141
+ return "public";
142
+ }
143
+ if ((accessFlags & 0x0004) !== 0) {
144
+ return "protected";
145
+ }
146
+ if ((accessFlags & 0x0002) !== 0) {
147
+ return "private";
148
+ }
149
+ return "package-private";
150
+ }
136
151
  function computeFalsePositiveRisk(healthReport, resolutionPath, issueConfidence) {
137
152
  if (!healthReport)
138
153
  return undefined;
@@ -735,7 +750,7 @@ export function validateParsedMixin(parsed, targetMembers, warnings, provenance,
735
750
  /* ------------------------------------------------------------------ */
736
751
  /* Access Widener validation */
737
752
  /* ------------------------------------------------------------------ */
738
- export function validateParsedAccessWidener(parsed, membersByClass, warnings) {
753
+ export function validateParsedAccessWidener(parsed, membersByClass, warnings, options) {
739
754
  warnings.push(...parsed.parseWarnings);
740
755
  const validatedEntries = [];
741
756
  let validCount = 0;
@@ -743,15 +758,29 @@ export function validateParsedAccessWidener(parsed, membersByClass, warnings) {
743
758
  for (const entry of parsed.entries) {
744
759
  const ownerFqn = entry.target.replace(/\//g, ".");
745
760
  if (entry.targetKind === "class") {
746
- if (membersByClass.has(ownerFqn)) {
747
- validatedEntries.push({ ...entry, valid: true });
761
+ const members = membersByClass.get(ownerFqn);
762
+ if (members) {
763
+ const runtimeAccess = accessLevelFromFlags(members.classAccessFlags);
764
+ validatedEntries.push({
765
+ ...entry,
766
+ valid: true,
767
+ ...(options?.includeRuntimeEvidence
768
+ ? {
769
+ resolvedInRuntime: true,
770
+ ...(runtimeAccess
771
+ ? { resolvedRuntimeAccess: runtimeAccess }
772
+ : {})
773
+ }
774
+ : {})
775
+ });
748
776
  validCount++;
749
777
  }
750
778
  else {
751
779
  validatedEntries.push({
752
780
  ...entry,
753
781
  valid: false,
754
- issue: `Class "${ownerFqn}" not found in game jar.`
782
+ issue: `Class "${ownerFqn}" not found in game jar.`,
783
+ ...(options?.includeRuntimeEvidence ? { resolvedInRuntime: false } : {})
755
784
  });
756
785
  invalidCount++;
757
786
  }
@@ -763,16 +792,37 @@ export function validateParsedAccessWidener(parsed, membersByClass, warnings) {
763
792
  validatedEntries.push({
764
793
  ...entry,
765
794
  valid: false,
766
- issue: `Owner class "${ownerFqn}" not found in game jar.`
795
+ issue: `Owner class "${ownerFqn}" not found in game jar.`,
796
+ ...(options?.includeRuntimeEvidence ? { resolvedInRuntime: false } : {})
767
797
  });
768
798
  invalidCount++;
769
799
  continue;
770
800
  }
771
801
  if (entry.targetKind === "method") {
772
802
  const methodNames = allMethodNames(members);
773
- const found = members.methods.some((m) => m.name === entry.name && (!entry.descriptor || m.jvmDescriptor === entry.descriptor)) || members.constructors.some((m) => m.name === entry.name && (!entry.descriptor || m.jvmDescriptor === entry.descriptor));
803
+ const matchedMember = members.methods.find((m) => m.name === entry.name && (!entry.descriptor || m.jvmDescriptor === entry.descriptor)) ?? members.constructors.find((m) => m.name === entry.name && (!entry.descriptor || m.jvmDescriptor === entry.descriptor));
804
+ const found = matchedMember != null;
774
805
  if (found) {
775
- validatedEntries.push({ ...entry, valid: true });
806
+ const runtimeMember = matchedMember;
807
+ const runtimeAccess = accessLevelFromFlags(runtimeMember.accessFlags);
808
+ validatedEntries.push({
809
+ ...entry,
810
+ valid: true,
811
+ ...(options?.includeRuntimeEvidence
812
+ ? {
813
+ resolvedInRuntime: true,
814
+ ...(runtimeAccess
815
+ ? { resolvedRuntimeAccess: runtimeAccess }
816
+ : {}),
817
+ ...(runtimeMember.jvmDescriptor
818
+ ? { resolvedRuntimeJvmDescriptor: runtimeMember.jvmDescriptor }
819
+ : {}),
820
+ ...(runtimeMember.javaSignature
821
+ ? { resolvedRuntimeJavaSignature: runtimeMember.javaSignature }
822
+ : {})
823
+ }
824
+ : {})
825
+ });
776
826
  validCount++;
777
827
  }
778
828
  else {
@@ -781,7 +831,8 @@ export function validateParsedAccessWidener(parsed, membersByClass, warnings) {
781
831
  ...entry,
782
832
  valid: false,
783
833
  issue: `Method "${entry.name}" not found in class "${ownerFqn}".`,
784
- suggestions: suggestions.length > 0 ? suggestions : undefined
834
+ suggestions: suggestions.length > 0 ? suggestions : undefined,
835
+ ...(options?.includeRuntimeEvidence ? { resolvedInRuntime: false } : {})
785
836
  });
786
837
  invalidCount++;
787
838
  }
@@ -789,9 +840,29 @@ export function validateParsedAccessWidener(parsed, membersByClass, warnings) {
789
840
  else {
790
841
  // field
791
842
  const fieldNames = allFieldNames(members);
792
- const found = members.fields.some((m) => m.name === entry.name && (!entry.descriptor || m.jvmDescriptor === entry.descriptor));
843
+ const matchedMember = members.fields.find((m) => m.name === entry.name && (!entry.descriptor || m.jvmDescriptor === entry.descriptor));
844
+ const found = matchedMember != null;
793
845
  if (found) {
794
- validatedEntries.push({ ...entry, valid: true });
846
+ const runtimeMember = matchedMember;
847
+ const runtimeAccess = accessLevelFromFlags(runtimeMember.accessFlags);
848
+ validatedEntries.push({
849
+ ...entry,
850
+ valid: true,
851
+ ...(options?.includeRuntimeEvidence
852
+ ? {
853
+ resolvedInRuntime: true,
854
+ ...(runtimeAccess
855
+ ? { resolvedRuntimeAccess: runtimeAccess }
856
+ : {}),
857
+ ...(runtimeMember.jvmDescriptor
858
+ ? { resolvedRuntimeJvmDescriptor: runtimeMember.jvmDescriptor }
859
+ : {}),
860
+ ...(runtimeMember.javaSignature
861
+ ? { resolvedRuntimeJavaSignature: runtimeMember.javaSignature }
862
+ : {})
863
+ }
864
+ : {})
865
+ });
795
866
  validCount++;
796
867
  }
797
868
  else {
@@ -800,7 +871,8 @@ export function validateParsedAccessWidener(parsed, membersByClass, warnings) {
800
871
  ...entry,
801
872
  valid: false,
802
873
  issue: `Field "${entry.name}" not found in class "${ownerFqn}".`,
803
- suggestions: suggestions.length > 0 ? suggestions : undefined
874
+ suggestions: suggestions.length > 0 ? suggestions : undefined,
875
+ ...(options?.includeRuntimeEvidence ? { resolvedInRuntime: false } : {})
804
876
  });
805
877
  invalidCount++;
806
878
  }
@@ -819,4 +891,118 @@ export function validateParsedAccessWidener(parsed, membersByClass, warnings) {
819
891
  warnings
820
892
  };
821
893
  }
894
+ export function validateParsedAccessTransformer(parsed, membersByClass, warnings, options) {
895
+ warnings.push(...parsed.parseWarnings);
896
+ const validatedEntries = [];
897
+ let validCount = 0;
898
+ let invalidCount = 0;
899
+ for (const entry of parsed.entries) {
900
+ const ownerFqn = entry.owner;
901
+ const members = membersByClass.get(ownerFqn);
902
+ if (entry.targetKind === "class") {
903
+ if (!members) {
904
+ validatedEntries.push({
905
+ ...entry,
906
+ valid: false,
907
+ issue: `Class "${ownerFqn}" not found in runtime jar.`,
908
+ ...(options?.includeRuntimeEvidence ? { resolvedInRuntime: false } : {})
909
+ });
910
+ invalidCount++;
911
+ continue;
912
+ }
913
+ const runtimeAccess = accessLevelFromFlags(members.classAccessFlags);
914
+ validatedEntries.push({
915
+ ...entry,
916
+ valid: true,
917
+ ...(options?.includeRuntimeEvidence
918
+ ? {
919
+ resolvedInRuntime: true,
920
+ ...(runtimeAccess ? { resolvedRuntimeAccess: runtimeAccess } : {})
921
+ }
922
+ : {})
923
+ });
924
+ validCount++;
925
+ continue;
926
+ }
927
+ if (!members) {
928
+ validatedEntries.push({
929
+ ...entry,
930
+ valid: false,
931
+ issue: `Owner class "${ownerFqn}" not found in runtime jar.`,
932
+ ...(options?.includeRuntimeEvidence ? { resolvedInRuntime: false } : {})
933
+ });
934
+ invalidCount++;
935
+ continue;
936
+ }
937
+ if (entry.targetKind === "field") {
938
+ const fieldNames = allFieldNames(members);
939
+ const matchedField = members.fields.find((member) => member.name === entry.name);
940
+ if (!matchedField) {
941
+ const suggestions = entry.name ? suggestSimilar(entry.name, fieldNames) : [];
942
+ validatedEntries.push({
943
+ ...entry,
944
+ valid: false,
945
+ issue: `Field "${entry.name}" not found in class "${ownerFqn}".`,
946
+ ...(suggestions.length > 0 ? { suggestions } : {}),
947
+ ...(options?.includeRuntimeEvidence ? { resolvedInRuntime: false } : {})
948
+ });
949
+ invalidCount++;
950
+ continue;
951
+ }
952
+ const runtimeAccess = accessLevelFromFlags(matchedField.accessFlags);
953
+ validatedEntries.push({
954
+ ...entry,
955
+ valid: true,
956
+ ...(options?.includeRuntimeEvidence
957
+ ? {
958
+ resolvedInRuntime: true,
959
+ ...(runtimeAccess ? { resolvedRuntimeAccess: runtimeAccess } : {}),
960
+ ...(matchedField.jvmDescriptor ? { resolvedRuntimeJvmDescriptor: matchedField.jvmDescriptor } : {}),
961
+ ...(matchedField.javaSignature ? { resolvedRuntimeJavaSignature: matchedField.javaSignature } : {})
962
+ }
963
+ : {})
964
+ });
965
+ validCount++;
966
+ continue;
967
+ }
968
+ const methodNames = allMethodNames(members);
969
+ const matchedMethod = members.methods.find((member) => member.name === entry.name && member.jvmDescriptor === entry.descriptor) ?? members.constructors.find((member) => member.name === entry.name && member.jvmDescriptor === entry.descriptor);
970
+ if (!matchedMethod) {
971
+ const suggestions = entry.name ? suggestSimilar(entry.name, methodNames) : [];
972
+ validatedEntries.push({
973
+ ...entry,
974
+ valid: false,
975
+ issue: `Method "${entry.name}" not found in class "${ownerFqn}".`,
976
+ ...(suggestions.length > 0 ? { suggestions } : {}),
977
+ ...(options?.includeRuntimeEvidence ? { resolvedInRuntime: false } : {})
978
+ });
979
+ invalidCount++;
980
+ continue;
981
+ }
982
+ const runtimeAccess = accessLevelFromFlags(matchedMethod.accessFlags);
983
+ validatedEntries.push({
984
+ ...entry,
985
+ valid: true,
986
+ ...(options?.includeRuntimeEvidence
987
+ ? {
988
+ resolvedInRuntime: true,
989
+ ...(runtimeAccess ? { resolvedRuntimeAccess: runtimeAccess } : {}),
990
+ ...(matchedMethod.jvmDescriptor ? { resolvedRuntimeJvmDescriptor: matchedMethod.jvmDescriptor } : {}),
991
+ ...(matchedMethod.javaSignature ? { resolvedRuntimeJavaSignature: matchedMethod.javaSignature } : {})
992
+ }
993
+ : {})
994
+ });
995
+ validCount++;
996
+ }
997
+ return {
998
+ valid: invalidCount === 0,
999
+ entries: validatedEntries,
1000
+ summary: {
1001
+ total: parsed.entries.length,
1002
+ valid: validCount,
1003
+ invalid: invalidCount
1004
+ },
1005
+ warnings
1006
+ };
1007
+ }
822
1008
  //# sourceMappingURL=mixin-validator.js.map
@@ -15,6 +15,7 @@ export interface ModAnalysisResult {
15
15
  entrypoints?: Record<string, string[]>;
16
16
  mixinConfigs?: string[];
17
17
  accessWidener?: string;
18
+ accessTransformers?: string[];
18
19
  dependencies?: ModDependency[];
19
20
  classCount: number;
20
21
  classes?: string[];
@@ -72,7 +72,8 @@ const forgeModsTomlSchema = z
72
72
  .passthrough())
73
73
  .optional(),
74
74
  dependencies: z.record(z.array(z.unknown())).optional(),
75
- mixins: z.array(z.object({ config: z.string() }).passthrough()).optional()
75
+ mixins: z.array(z.object({ config: z.string() }).passthrough()).optional(),
76
+ accessTransformers: z.array(z.object({ file: z.string().optional() }).passthrough()).optional()
76
77
  })
77
78
  .passthrough();
78
79
  const legacyForgeSchema = z.array(z
@@ -214,6 +215,9 @@ function parseForgeMod(content, entries) {
214
215
  }
215
216
  // Mixin configs
216
217
  const mixinConfigs = toml.mixins?.map((m) => m.config);
218
+ const accessTransformers = toml.accessTransformers
219
+ ?.map((entry) => entry.file)
220
+ .filter((file) => typeof file === "string" && file.trim().length > 0);
217
221
  return {
218
222
  detectedLoader,
219
223
  modId: firstMod?.modId,
@@ -221,6 +225,7 @@ function parseForgeMod(content, entries) {
221
225
  modVersion: firstMod?.version,
222
226
  description: firstMod?.description,
223
227
  mixinConfigs: mixinConfigs && mixinConfigs.length > 0 ? mixinConfigs : undefined,
228
+ accessTransformers: accessTransformers && accessTransformers.length > 0 ? accessTransformers : undefined,
224
229
  dependencies: dependencies.length > 0 ? dependencies : undefined
225
230
  };
226
231
  }
@@ -288,6 +293,9 @@ export async function analyzeModJar(jarPath, options) {
288
293
  // Detect loader and parse metadata
289
294
  let loader = "unknown";
290
295
  let metadata = {};
296
+ const packagedAccessTransformers = [...new Set(entries.filter((entry) => /(^|\/)META-INF\/accesstransformer\.cfg$/i.test(entry) ||
297
+ /(^|\/)[^/]+_at\.cfg$/i.test(entry) ||
298
+ /(^|\/)accesstransformer[^/]*\.cfg$/i.test(entry)))].sort((left, right) => left.localeCompare(right));
291
299
  if (entries.includes("fabric.mod.json")) {
292
300
  loader = "fabric";
293
301
  try {
@@ -346,6 +354,14 @@ export async function analyzeModJar(jarPath, options) {
346
354
  loader,
347
355
  jarKind,
348
356
  ...metadata,
357
+ ...(packagedAccessTransformers.length > 0 || metadata.accessTransformers
358
+ ? {
359
+ accessTransformers: [...new Set([
360
+ ...(metadata.accessTransformers ?? []),
361
+ ...packagedAccessTransformers
362
+ ])]
363
+ }
364
+ : {}),
349
365
  classCount,
350
366
  ...(classes !== undefined ? { classes } : {})
351
367
  };
@@ -1,5 +1,5 @@
1
1
  import { createHash } from "node:crypto";
2
- import { readFileSync, writeFileSync } from "node:fs";
2
+ import { readFile, writeFile } from "node:fs/promises";
3
3
  import { isAbsolute, join, resolve as resolvePath } from "node:path";
4
4
  import { createError, ERROR_CODES } from "./errors.js";
5
5
  import { log } from "./logger.js";
@@ -41,7 +41,7 @@ export class ModDecompileService {
41
41
  const targetFile = classNameToFilePath(input.className);
42
42
  const matched = files.find((f) => f === targetFile || f.endsWith(`/${targetFile}`) || f === input.className);
43
43
  if (matched) {
44
- const content = readFileSync(join(outputDir, matched), "utf8");
44
+ const content = await readFile(join(outputDir, matched), "utf8");
45
45
  sourceResult = {
46
46
  className: filePathToClassName(matched),
47
47
  content,
@@ -106,7 +106,7 @@ export class ModDecompileService {
106
106
  details: { className, jarPath, availableCount: files.length }
107
107
  });
108
108
  }
109
- const fullContent = readFileSync(join(outputDir, matched), "utf8");
109
+ const fullContent = await readFile(join(outputDir, matched), "utf8");
110
110
  const totalLines = fullContent.split("\n").length;
111
111
  let content = fullContent;
112
112
  let truncated;
@@ -131,7 +131,7 @@ export class ModDecompileService {
131
131
  const outPath = isAbsolute(input.outputFile)
132
132
  ? input.outputFile
133
133
  : resolvePath(input.outputFile);
134
- writeFileSync(outPath, content, "utf8");
134
+ await writeFile(outPath, content, "utf8");
135
135
  outputFilePath = outPath;
136
136
  content = `[Written to ${outPath}]`;
137
137
  }
@@ -8,7 +8,7 @@ import { resolveTinyMappingFile } from "./mapping-service.js";
8
8
  import { resolveMojangTinyFile } from "./mojang-tiny-mapping-service.js";
9
9
  import { analyzeModJar } from "./mod-analyzer.js";
10
10
  import { normalizePathForHost } from "./path-converter.js";
11
- import { listJarEntries, readJarEntryAsBuffer } from "./source-jar-reader.js";
11
+ import { detectFabricLikeInputNamespace } from "./source-jar-reader.js";
12
12
  import { remapJar } from "./tiny-remapper-service.js";
13
13
  import { resolveTinyRemapperJar } from "./tiny-remapper-resolver.js";
14
14
  function normalizeTargetNamespace(target) {
@@ -36,59 +36,6 @@ function extractMinecraftVersion(dependencies) {
36
36
  const match = mcDep.versionRange.match(/(\d+\.\d+(?:\.\d+)?)/);
37
37
  return match?.[1];
38
38
  }
39
- function countMatches(input, pattern) {
40
- const flags = pattern.flags.includes("g") ? pattern.flags : `${pattern.flags}g`;
41
- const globalPattern = new RegExp(pattern.source, flags);
42
- let count = 0;
43
- while (globalPattern.exec(input)) {
44
- count += 1;
45
- }
46
- return count;
47
- }
48
- async function detectFabricLikeInputNamespace(inputJar) {
49
- const warnings = [];
50
- const classEntries = (await listJarEntries(inputJar))
51
- .filter((entry) => entry.endsWith(".class"))
52
- .slice(0, 24);
53
- if (classEntries.length === 0) {
54
- warnings.push("Could not inspect class entries to detect input mapping; assuming intermediary.");
55
- return {
56
- fromNamespace: "intermediary",
57
- warnings
58
- };
59
- }
60
- let mojangScore = 0;
61
- let intermediaryScore = 0;
62
- for (const entry of classEntries) {
63
- let text = "";
64
- try {
65
- text = (await readJarEntryAsBuffer(inputJar, entry)).toString("latin1");
66
- }
67
- catch {
68
- continue;
69
- }
70
- mojangScore += countMatches(text, /net\/minecraft\/(?:advancements|client|commands|core|data|gametest|nbt|network|recipe|resources|server|sounds|stats|tags|util|world)\//g) * 3;
71
- intermediaryScore += countMatches(text, /net\/minecraft\/class_\d+/g) * 3;
72
- intermediaryScore += countMatches(text, /\b(?:method|field)_\d+\b/g);
73
- }
74
- if (mojangScore > intermediaryScore && mojangScore > 0) {
75
- return {
76
- fromNamespace: "mojang",
77
- warnings
78
- };
79
- }
80
- if (intermediaryScore > mojangScore && intermediaryScore > 0) {
81
- return {
82
- fromNamespace: "intermediary",
83
- warnings
84
- };
85
- }
86
- warnings.push("Could not confidently detect whether the input jar uses intermediary or mojang names; assuming intermediary.");
87
- return {
88
- fromNamespace: "intermediary",
89
- warnings
90
- };
91
- }
92
39
  async function detectInputNamespaceForLoader(inputJar, loader) {
93
40
  if (loader === "fabric" || loader === "quilt") {
94
41
  return detectFabricLikeInputNamespace(inputJar);
@@ -26,4 +26,5 @@ export declare class ModSearchService {
26
26
  constructor(modDecompileService: ModDecompileService);
27
27
  searchModSource(input: SearchModSourceInput): Promise<SearchModSourceOutput>;
28
28
  private searchSourceJarEntriesDirect;
29
+ private searchDecompiledClassFile;
29
30
  }
@@ -1,5 +1,6 @@
1
- import { readFileSync } from "node:fs";
1
+ import { readFile } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
+ import { mapWithConcurrencyLimit } from "./concurrency.js";
3
4
  import { createError, ERROR_CODES } from "./errors.js";
4
5
  import { log } from "./logger.js";
5
6
  import { validateAndNormalizeJarPath } from "./path-resolver.js";
@@ -9,6 +10,7 @@ const DEFAULT_LIMIT = 50;
9
10
  const MAX_LIMIT = 200;
10
11
  const MAX_QUERY_LENGTH = 200;
11
12
  const CONTEXT_LINES = 1;
13
+ const DECOMPILED_JAVA_READ_CONCURRENCY = 8;
12
14
  const METHOD_PATTERN = /^\s*(public|private|protected)\s+.*\(/;
13
15
  const FIELD_PATTERN = /^\s*(public|private|protected)\s+(?:static\s+)?(?:final\s+)?[\w<>,\[\]?]+\s+\w+\s*[;=]/;
14
16
  function buildRegex(query) {
@@ -41,6 +43,9 @@ function extractContext(lines, lineIndex) {
41
43
  function filePathToClassName(filePath) {
42
44
  return filePath.replace(/\.java$/, "").replaceAll("/", ".");
43
45
  }
46
+ function cloneRegex(regex) {
47
+ return new RegExp(regex.source, regex.flags);
48
+ }
44
49
  export class ModSearchService {
45
50
  modDecompileService;
46
51
  constructor(modDecompileService) {
@@ -112,65 +117,40 @@ export class ModSearchService {
112
117
  const hits = [];
113
118
  let totalHits = 0;
114
119
  let reachedLimit = false;
115
- for (const className of classNames) {
120
+ for (let batchStartIndex = 0; batchStartIndex < classNames.length; batchStartIndex += DECOMPILED_JAVA_READ_CONCURRENCY) {
116
121
  if (hits.length >= limit) {
117
122
  reachedLimit = true;
118
123
  break;
119
124
  }
120
- const filePath = className.replaceAll(".", "/") + ".java";
121
- // Class name search: check if the simple class name matches
122
- if (searchType === "class" || searchType === "all") {
123
- const simpleClassName = className.split(".").pop() ?? className;
124
- regex.lastIndex = 0;
125
- if (regex.test(simpleClassName)) {
126
- totalHits++;
127
- if (hits.length < limit) {
128
- hits.push({
129
- type: "class",
130
- name: className,
131
- file: filePath
132
- });
133
- }
134
- // If searching only classes, skip content search for this file
135
- if (searchType === "class")
136
- continue;
137
- }
138
- }
139
- // Content/method/field search: read and scan the file
140
- if (searchType === "method" || searchType === "field" || searchType === "content" || searchType === "all") {
141
- let content;
142
- try {
143
- content = readFileSync(join(outputDir, filePath), "utf8");
125
+ const batchClassNames = classNames.slice(batchStartIndex, batchStartIndex + DECOMPILED_JAVA_READ_CONCURRENCY);
126
+ const batchHitLimit = Math.max(1, limit - hits.length);
127
+ const fileResults = await mapWithConcurrencyLimit(batchClassNames, DECOMPILED_JAVA_READ_CONCURRENCY, async (className) => this.searchDecompiledClassFile({
128
+ className,
129
+ outputDir,
130
+ searchType,
131
+ regex: cloneRegex(regex),
132
+ maxHits: batchHitLimit
133
+ }));
134
+ for (const fileResult of fileResults) {
135
+ if (hits.length >= limit) {
136
+ reachedLimit = true;
137
+ break;
144
138
  }
145
- catch {
146
- // File might not exist at the expected path, skip
139
+ if (fileResult.hits.length === 0) {
147
140
  continue;
148
141
  }
149
- const lines = content.split("\n");
150
- for (let i = 0; i < lines.length; i++) {
151
- if (hits.length >= limit) {
152
- reachedLimit = true;
153
- break;
154
- }
155
- regex.lastIndex = 0;
156
- if (!regex.test(lines[i]))
157
- continue;
158
- const lineType = classifyLine(lines[i]);
159
- // Filter by search type
160
- if (searchType !== "all" && searchType !== lineType)
161
- continue;
162
- totalHits++;
163
- if (hits.length < limit) {
164
- hits.push({
165
- type: lineType,
166
- name: lineType === "content" ? className : extractSymbolName(lines[i], lineType),
167
- file: filePath,
168
- line: i + 1,
169
- context: extractContext(lines, i)
170
- });
171
- }
142
+ const remaining = limit - hits.length;
143
+ const acceptedHits = fileResult.hits.slice(0, remaining);
144
+ hits.push(...acceptedHits);
145
+ totalHits += acceptedHits.length;
146
+ if (hits.length >= limit || fileResult.hits.length > remaining) {
147
+ reachedLimit = true;
148
+ break;
172
149
  }
173
150
  }
151
+ if (reachedLimit) {
152
+ break;
153
+ }
174
154
  }
175
155
  log("info", "mod-search.done", {
176
156
  jarPath,
@@ -255,6 +235,59 @@ export class ModSearchService {
255
235
  warnings
256
236
  };
257
237
  }
238
+ async searchDecompiledClassFile(input) {
239
+ const hits = [];
240
+ const filePath = input.className.replaceAll(".", "/") + ".java";
241
+ if (input.searchType === "class" || input.searchType === "all") {
242
+ const simpleClassName = input.className.split(".").pop() ?? input.className;
243
+ input.regex.lastIndex = 0;
244
+ if (input.regex.test(simpleClassName)) {
245
+ hits.push({
246
+ type: "class",
247
+ name: input.className,
248
+ file: filePath
249
+ });
250
+ if (input.searchType === "class" || hits.length >= input.maxHits) {
251
+ return { hits };
252
+ }
253
+ }
254
+ }
255
+ if (input.searchType !== "method" &&
256
+ input.searchType !== "field" &&
257
+ input.searchType !== "content" &&
258
+ input.searchType !== "all") {
259
+ return { hits };
260
+ }
261
+ let content;
262
+ try {
263
+ content = await readFile(join(input.outputDir, filePath), "utf8");
264
+ }
265
+ catch {
266
+ return { hits };
267
+ }
268
+ const lines = content.split("\n");
269
+ for (let i = 0; i < lines.length; i++) {
270
+ if (hits.length >= input.maxHits) {
271
+ break;
272
+ }
273
+ input.regex.lastIndex = 0;
274
+ if (!input.regex.test(lines[i])) {
275
+ continue;
276
+ }
277
+ const lineType = classifyLine(lines[i]);
278
+ if (input.searchType !== "all" && input.searchType !== lineType) {
279
+ continue;
280
+ }
281
+ hits.push({
282
+ type: lineType,
283
+ name: lineType === "content" ? input.className : extractSymbolName(lines[i], lineType),
284
+ file: filePath,
285
+ line: i + 1,
286
+ context: extractContext(lines, i)
287
+ });
288
+ }
289
+ return { hits };
290
+ }
258
291
  }
259
292
  function extractSymbolName(line, type) {
260
293
  const trimmed = line.trim();
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Compact-mode response utilities.
3
+ *
4
+ * Applied at the public boundary (runTool) only — internal service types are never modified.
5
+ */
6
+ /** Tools that accept the `compact` parameter. */
7
+ export declare const COMPACT_ENABLED_TOOL_NAMES: Set<string>;
8
+ /** Mapping-oriented tools that get additional field projection via compactMappingResponse. */
9
+ export declare const COMPACT_MAPPING_TOOL_NAMES: Set<string>;
10
+ /**
11
+ * Double-gated compact check: tool must be in the allowlist AND parsedInput.compact must be true.
12
+ * Prevents activation on passthrough schemas where Zod doesn't strip unknown keys.
13
+ */
14
+ export declare function isCompactEnabled(tool: string, parsedInput: unknown): boolean;
15
+ /**
16
+ * Shallow-strip empty values from a response object.
17
+ * Only operates on the top level — nested structures are preserved as-is.
18
+ * Non-plain objects (Date, Map, class instances) are never treated as empty.
19
+ */
20
+ export declare function compactResponse(obj: Record<string, unknown>): Record<string, unknown>;
21
+ /** resolve-artifact compact: omit debug/diagnostic fields. */
22
+ export declare function compactArtifactResponse(obj: Record<string, unknown>): Record<string, unknown>;
23
+ /**
24
+ * Mapping tool compact: omit candidates only when provably redundant.
25
+ *
26
+ * Candidates are omitted when ALL of:
27
+ * 1. resolved === true
28
+ * 2. resolvedSymbol exists
29
+ * 3. candidates is an array of length 1
30
+ * 4. candidateCount === 1
31
+ * 5. candidatesTruncated is falsy
32
+ * 6. candidates[0].matchKind === "exact"
33
+ * 7. candidates[0].confidence is undefined or 1
34
+ */
35
+ export declare function compactMappingResponse(obj: Record<string, unknown>): Record<string, unknown>;