@adhisang/minecraft-modding-mcp 4.2.0 → 5.0.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.
- package/CHANGELOG.md +40 -0
- package/README.md +6 -4
- package/dist/access-transformer-parser.d.ts +2 -1
- package/dist/access-transformer-parser.js +10 -1
- package/dist/build-suggested-call.js +31 -10
- package/dist/cache-registry.d.ts +8 -0
- package/dist/cache-registry.js +99 -6
- package/dist/config.d.ts +1 -0
- package/dist/config.js +4 -0
- package/dist/entry-tools/analyze-mod-service.d.ts +24 -24
- package/dist/entry-tools/analyze-symbol-service.d.ts +57 -22
- package/dist/entry-tools/analyze-symbol-service.js +124 -28
- package/dist/entry-tools/batch-class-members-service.d.ts +3 -1
- package/dist/entry-tools/batch-class-members-service.js +6 -6
- package/dist/entry-tools/batch-class-source-service.d.ts +3 -1
- package/dist/entry-tools/batch-class-source-service.js +5 -6
- package/dist/entry-tools/batch-mappings-service.d.ts +3 -1
- package/dist/entry-tools/batch-mappings-service.js +4 -5
- package/dist/entry-tools/batch-symbol-exists-service.d.ts +3 -1
- package/dist/entry-tools/batch-symbol-exists-service.js +4 -5
- package/dist/entry-tools/compare-minecraft-service.d.ts +10 -10
- package/dist/entry-tools/entry-tool-schema.d.ts +1 -1
- package/dist/entry-tools/inspect-minecraft/handlers/artifact.js +1 -3
- package/dist/entry-tools/inspect-minecraft/handlers/class-members.js +2 -2
- package/dist/entry-tools/inspect-minecraft/handlers/class-overview.js +0 -5
- package/dist/entry-tools/inspect-minecraft/handlers/class-source.js +0 -1
- package/dist/entry-tools/inspect-minecraft/handlers/file.js +0 -1
- package/dist/entry-tools/inspect-minecraft/handlers/list-files.js +0 -1
- package/dist/entry-tools/inspect-minecraft/handlers/search.d.ts +8 -0
- package/dist/entry-tools/inspect-minecraft/handlers/search.js +13 -2
- package/dist/entry-tools/inspect-minecraft/handlers/versions.d.ts +1 -1
- package/dist/entry-tools/inspect-minecraft/handlers/versions.js +1 -1
- package/dist/entry-tools/inspect-minecraft/internal.d.ts +90 -89
- package/dist/entry-tools/inspect-minecraft/internal.js +30 -14
- package/dist/entry-tools/inspect-minecraft-service.d.ts +218 -218
- package/dist/entry-tools/inspect-minecraft-service.js +1 -1
- package/dist/entry-tools/manage-cache-service.d.ts +4 -4
- package/dist/entry-tools/response-contract.d.ts +1 -1
- package/dist/entry-tools/response-contract.js +1 -0
- package/dist/entry-tools/validate-project/cases/mixin.js +2 -1
- package/dist/entry-tools/validate-project/cases/project-summary.js +5 -3
- package/dist/entry-tools/validate-project-service.d.ts +42 -36
- package/dist/entry-tools/validate-project-service.js +3 -1
- package/dist/entry-tools/verify-mixin-target-service.d.ts +30 -0
- package/dist/entry-tools/verify-mixin-target-service.js +113 -22
- package/dist/error-mapping.d.ts +51 -0
- package/dist/error-mapping.js +157 -2
- package/dist/errors.js +4 -1
- package/dist/index.js +138 -71
- package/dist/java-process.d.ts +9 -0
- package/dist/java-process.js +11 -5
- package/dist/mapping/internal-types.d.ts +7 -0
- package/dist/mapping/lookup.d.ts +1 -1
- package/dist/mapping/lookup.js +7 -1
- package/dist/mapping/types.d.ts +6 -0
- package/dist/mapping-service.d.ts +26 -2
- package/dist/mapping-service.js +178 -36
- package/dist/mcp-helpers.d.ts +1 -0
- package/dist/mcp-helpers.js +25 -3
- package/dist/minecraft-explorer-service.js +24 -11
- package/dist/mixin/access-validators.js +33 -1
- package/dist/mixin/annotation-validators.js +53 -7
- package/dist/mixin/parsed-validator.js +2 -2
- package/dist/mixin-parser.js +48 -4
- package/dist/mod-decompile-service.js +2 -1
- package/dist/mod-remap-service.d.ts +20 -1
- package/dist/mod-remap-service.js +95 -40
- package/dist/nbt/java-nbt-codec.js +87 -2
- package/dist/observability.d.ts +6 -0
- package/dist/observability.js +10 -0
- package/dist/page-cursor.d.ts +17 -0
- package/dist/page-cursor.js +0 -0
- package/dist/registry-service.js +25 -1
- package/dist/resources.js +45 -9
- package/dist/response-utils.d.ts +29 -4
- package/dist/response-utils.js +95 -5
- package/dist/source/access-validate.js +12 -2
- package/dist/source/artifact-resolver.js +1 -1
- package/dist/source/cache-metrics.js +2 -2
- package/dist/source/class-source/members-builder.d.ts +44 -1
- package/dist/source/class-source/members-builder.js +51 -4
- package/dist/source/class-source/snippet-builder.d.ts +13 -0
- package/dist/source/class-source/snippet-builder.js +48 -4
- package/dist/source/class-source-helpers.js +67 -11
- package/dist/source/class-source.js +97 -20
- package/dist/source/indexer.d.ts +31 -6
- package/dist/source/indexer.js +95 -25
- package/dist/source/search.d.ts +1 -1
- package/dist/source/search.js +94 -22
- package/dist/source/validate-mixin.d.ts +2 -1
- package/dist/source/validate-mixin.js +45 -4
- package/dist/source/workspace-target.js +15 -9
- package/dist/source-jar-reader.d.ts +54 -1
- package/dist/source-jar-reader.js +121 -10
- package/dist/source-service.d.ts +28 -7
- package/dist/storage/files-repo.d.ts +12 -0
- package/dist/storage/files-repo.js +24 -0
- package/dist/storage/symbols-repo.d.ts +6 -0
- package/dist/storage/symbols-repo.js +6 -0
- package/dist/symbols/symbol-extractor.js +35 -6
- package/dist/text-truncate.d.ts +7 -0
- package/dist/text-truncate.js +21 -0
- package/dist/tiny-remapper-resolver.js +10 -0
- package/dist/tiny-remapper-service.js +3 -3
- package/dist/tool-contract-manifest.js +2 -2
- package/dist/tool-execution-gate.js +12 -1
- package/dist/tool-guidance.d.ts +4 -1
- package/dist/tool-guidance.js +37 -12
- package/dist/tool-schemas.d.ts +559 -494
- package/dist/tool-schemas.js +99 -63
- package/dist/types.d.ts +1 -0
- package/dist/version-diff-service.js +5 -0
- package/dist/version-service.d.ts +3 -0
- package/dist/version-service.js +18 -2
- package/dist/warning-details.d.ts +18 -0
- package/dist/warning-details.js +63 -0
- package/docs/README-ja.md +2 -2
- package/docs/examples.md +26 -2
- package/docs/tool-reference.md +23 -17
- package/package.json +1 -1
|
@@ -67,17 +67,73 @@ export function extractDecompiledMembers(className, filePath, content) {
|
|
|
67
67
|
}
|
|
68
68
|
return { constructors, fields, methods };
|
|
69
69
|
}
|
|
70
|
+
/**
|
|
71
|
+
* Remove everything that must not count toward brace depth — line comments,
|
|
72
|
+
* (possibly multi-line) block comments, string literals, and char literals —
|
|
73
|
+
* in a single stateful pass so that block comments spanning lines and char
|
|
74
|
+
* literals like '{' / '}' never skew brace accounting. Line indices are
|
|
75
|
+
* preserved (one output entry per input line). Text blocks (""" … """) are not
|
|
76
|
+
* special-cased; they are vanishingly rare in decompiled output.
|
|
77
|
+
*/
|
|
78
|
+
function stripBraceNoise(lines) {
|
|
79
|
+
const out = new Array(lines.length);
|
|
80
|
+
let inBlockComment = false;
|
|
81
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
82
|
+
const line = lines[i] ?? "";
|
|
83
|
+
let result = "";
|
|
84
|
+
let j = 0;
|
|
85
|
+
while (j < line.length) {
|
|
86
|
+
const ch = line[j];
|
|
87
|
+
const next = line[j + 1];
|
|
88
|
+
if (inBlockComment) {
|
|
89
|
+
if (ch === "*" && next === "/") {
|
|
90
|
+
inBlockComment = false;
|
|
91
|
+
j += 2;
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
j += 1;
|
|
95
|
+
}
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (ch === "/" && next === "/") {
|
|
99
|
+
break; // line comment: ignore the rest of the line
|
|
100
|
+
}
|
|
101
|
+
if (ch === "/" && next === "*") {
|
|
102
|
+
inBlockComment = true;
|
|
103
|
+
j += 2;
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if (ch === '"' || ch === "'") {
|
|
107
|
+
const quote = ch;
|
|
108
|
+
j += 1;
|
|
109
|
+
while (j < line.length) {
|
|
110
|
+
if (line[j] === "\\") {
|
|
111
|
+
j += 2;
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (line[j] === quote) {
|
|
115
|
+
j += 1;
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
j += 1;
|
|
119
|
+
}
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
result += ch;
|
|
123
|
+
j += 1;
|
|
124
|
+
}
|
|
125
|
+
out[i] = result;
|
|
126
|
+
}
|
|
127
|
+
return out;
|
|
128
|
+
}
|
|
70
129
|
export function computeLineBraceDepths(lines) {
|
|
130
|
+
const stripped = stripBraceNoise(lines);
|
|
71
131
|
const depths = new Array(lines.length).fill(0);
|
|
72
132
|
let depth = 0;
|
|
73
|
-
for (let i = 0; i <
|
|
133
|
+
for (let i = 0; i < stripped.length; i += 1) {
|
|
74
134
|
// Entry depth for this line = depth observed before any brace on it.
|
|
75
135
|
depths[i] = depth;
|
|
76
|
-
const
|
|
77
|
-
.replace(/\/\/.*/g, "")
|
|
78
|
-
.replace(/"(?:\\.|[^"\\])*"/g, "\"\"")
|
|
79
|
-
.replace(/'(?:\\.|[^'\\])*'/g, "''");
|
|
80
|
-
for (const char of stripped) {
|
|
136
|
+
for (const char of stripped[i] ?? "") {
|
|
81
137
|
if (char === "{") {
|
|
82
138
|
depth += 1;
|
|
83
139
|
}
|
|
@@ -98,13 +154,13 @@ export function computeBraceRange(lines, symbols, simpleName) {
|
|
|
98
154
|
return scanBraceRange(lines, classSymbol.line);
|
|
99
155
|
}
|
|
100
156
|
export function scanBraceRange(lines, declarationLine) {
|
|
157
|
+
// Strip from the start of the file so multi-line block-comment state is correct
|
|
158
|
+
// by the time we reach declarationLine; indices stay aligned with `lines`.
|
|
159
|
+
const stripped = stripBraceNoise(lines);
|
|
101
160
|
let depth = 0;
|
|
102
161
|
let started = false;
|
|
103
|
-
for (let i = declarationLine - 1; i <
|
|
104
|
-
const
|
|
105
|
-
.replace(/\/\/.*/g, "")
|
|
106
|
-
.replace(/"(?:\\.|[^"\\])*"/g, "\"\"");
|
|
107
|
-
for (const char of stripped) {
|
|
162
|
+
for (let i = declarationLine - 1; i < stripped.length; i += 1) {
|
|
163
|
+
for (const char of stripped[i] ?? "") {
|
|
108
164
|
if (char === "{") {
|
|
109
165
|
depth += 1;
|
|
110
166
|
started = true;
|
|
@@ -5,7 +5,8 @@ import { ERROR_CODES, createError, isAppError } from "../errors.js";
|
|
|
5
5
|
import * as artifactResolver from "./artifact-resolver.js";
|
|
6
6
|
import * as classSourceHelpers from "./class-source-helpers.js";
|
|
7
7
|
import { buildClassSourceSnippet } from "./class-source/snippet-builder.js";
|
|
8
|
-
import { remapAndCountMembers, sliceMembersWithLimit } from "./class-source/members-builder.js";
|
|
8
|
+
import { remapAndCountMembers, sliceMembersWithLimit, projectMembersForWire } from "./class-source/members-builder.js";
|
|
9
|
+
import { buildPageContextKey, encodeOffsetCursor, resolveCursorOffset } from "../page-cursor.js";
|
|
9
10
|
import { dedupeQualityFlags, normalizeMapping, normalizeOptionalString, normalizePathStyle } from "./shared-utils.js";
|
|
10
11
|
const MEMBERS_STATUS_LEGACY = process.env.MEMBERS_STATUS_LEGACY === "1";
|
|
11
12
|
function normalizeStrictPositiveInt(value, field) {
|
|
@@ -257,6 +258,9 @@ export function buildDecompiledFallback(svc, artifactId, lookupClassName, member
|
|
|
257
258
|
}
|
|
258
259
|
};
|
|
259
260
|
}
|
|
261
|
+
// The class-like symbol kinds findClass returns. MUST stay in sync with the JS-side
|
|
262
|
+
// isTypeSymbol checks below; pushed down to SQL so non-type rows are never fetched.
|
|
263
|
+
const TYPE_SYMBOL_KINDS = ["class", "interface", "enum", "record"];
|
|
260
264
|
export function findClass(svc, input) {
|
|
261
265
|
const className = input.className.trim();
|
|
262
266
|
if (!className) {
|
|
@@ -278,12 +282,20 @@ export function findClass(svc, input) {
|
|
|
278
282
|
const warnings = [];
|
|
279
283
|
const isQualified = className.includes(".");
|
|
280
284
|
if (isQualified) {
|
|
281
|
-
|
|
285
|
+
// The innermost simple name (handles both dot- and $-separated inner types).
|
|
286
|
+
const simpleName = className.split(/[.$]/).at(-1) ?? className;
|
|
287
|
+
const directPath = `${classNameToClassPath(className)}.java`;
|
|
288
|
+
// The extractor stores ONE (outer) qualifiedName per file, so a nested type's
|
|
289
|
+
// FQCN never equals any stored qualifiedName and its synthetic file path
|
|
290
|
+
// (pkg/Outer/Inner.java) does not exist. resolveClassFilePath maps the FQCN —
|
|
291
|
+
// inner classes included — to the real outer file, which we match on.
|
|
292
|
+
const resolvedFilePath = resolveClassFilePath(svc, artifactId, className);
|
|
282
293
|
const result = svc.symbolsRepo.findScopedSymbols({
|
|
283
294
|
artifactId,
|
|
284
|
-
query:
|
|
295
|
+
query: simpleName,
|
|
285
296
|
match: "exact",
|
|
286
|
-
|
|
297
|
+
symbolKinds: TYPE_SYMBOL_KINDS,
|
|
298
|
+
limit: limit * 5
|
|
287
299
|
});
|
|
288
300
|
const matches = result.items
|
|
289
301
|
.filter((row) => {
|
|
@@ -292,14 +304,22 @@ export function findClass(svc, input) {
|
|
|
292
304
|
if (!isTypeSymbol)
|
|
293
305
|
return false;
|
|
294
306
|
const rowQualified = row.qualifiedName ?? row.filePath.replace(/\.java$/, "").replaceAll("/", ".");
|
|
295
|
-
return rowQualified === className ||
|
|
307
|
+
return (rowQualified === className ||
|
|
308
|
+
row.filePath === directPath ||
|
|
309
|
+
(resolvedFilePath != null && row.filePath === resolvedFilePath));
|
|
310
|
+
})
|
|
311
|
+
.map((row) => {
|
|
312
|
+
const rowQualified = row.qualifiedName ?? row.filePath.replace(/\.java$/, "").replaceAll("/", ".");
|
|
313
|
+
// For an inner-class match the stored qualifiedName is the outer type; the
|
|
314
|
+
// caller asked for the full nested FQCN, so report that.
|
|
315
|
+
const isInnerMatch = rowQualified !== className && row.filePath !== directPath;
|
|
316
|
+
return {
|
|
317
|
+
qualifiedName: isInnerMatch ? className : rowQualified,
|
|
318
|
+
filePath: row.filePath,
|
|
319
|
+
line: row.line,
|
|
320
|
+
symbolKind: row.symbolKind
|
|
321
|
+
};
|
|
296
322
|
})
|
|
297
|
-
.map((row) => ({
|
|
298
|
-
qualifiedName: row.qualifiedName ?? row.filePath.replace(/\.java$/, "").replaceAll("/", "."),
|
|
299
|
-
filePath: row.filePath,
|
|
300
|
-
line: row.line,
|
|
301
|
-
symbolKind: row.symbolKind
|
|
302
|
-
}))
|
|
303
323
|
.slice(0, limit);
|
|
304
324
|
const partialVanillaLookup = hasPartialNetMinecraftCoverage(artifact.qualityFlags) && looksLikeDeobfuscatedClassName(className);
|
|
305
325
|
const filteredMatches = partialVanillaLookup && matches.every((match) => !match.qualifiedName.startsWith("net.minecraft.") && !match.qualifiedName.startsWith("com.mojang."))
|
|
@@ -317,6 +337,7 @@ export function findClass(svc, input) {
|
|
|
317
337
|
artifactId,
|
|
318
338
|
query: className,
|
|
319
339
|
match: "exact",
|
|
340
|
+
symbolKinds: TYPE_SYMBOL_KINDS,
|
|
320
341
|
limit: limit * 5
|
|
321
342
|
});
|
|
322
343
|
const matches = [];
|
|
@@ -587,6 +608,27 @@ export async function getClassSource(svc, input) {
|
|
|
587
608
|
requestedMapping,
|
|
588
609
|
mappingApplied: activeMappingApplied
|
|
589
610
|
});
|
|
611
|
+
const nextStartLine = snippet.nextStartLine;
|
|
612
|
+
// Continuation guidance: when output was truncated and was not redirected to
|
|
613
|
+
// a file, hand the caller the next line to read plus a replayable call that
|
|
614
|
+
// re-reads from the already-resolved artifact (no re-resolution needed).
|
|
615
|
+
// nextStartLine is only set for snippet/full mode, so `mode` here is never
|
|
616
|
+
// "metadata". The caller's original endLine window is preserved so the
|
|
617
|
+
// continuation never reads past the requested range.
|
|
618
|
+
const continuation = nextStartLine != null && !resolvedOutputFile
|
|
619
|
+
? buildSuggestedCall({
|
|
620
|
+
tool: "get-class-source",
|
|
621
|
+
params: {
|
|
622
|
+
className,
|
|
623
|
+
target: { kind: "artifact", artifactId: activeArtifactId },
|
|
624
|
+
mode,
|
|
625
|
+
startLine: nextStartLine,
|
|
626
|
+
...(input.endLine != null ? { endLine: input.endLine } : {}),
|
|
627
|
+
...(input.maxLines != null ? { maxLines: input.maxLines } : {}),
|
|
628
|
+
...(input.maxChars != null ? { maxChars: input.maxChars } : {})
|
|
629
|
+
}
|
|
630
|
+
})
|
|
631
|
+
: undefined;
|
|
590
632
|
return {
|
|
591
633
|
className,
|
|
592
634
|
mode,
|
|
@@ -598,6 +640,8 @@ export async function getClassSource(svc, input) {
|
|
|
598
640
|
},
|
|
599
641
|
truncated,
|
|
600
642
|
...(charsTruncated ? { charsTruncated } : {}),
|
|
643
|
+
...(snippet.outOfRange ? { outOfRange: true } : {}),
|
|
644
|
+
...(nextStartLine != null ? { nextStartLine } : {}),
|
|
601
645
|
origin: activeOrigin,
|
|
602
646
|
artifactId: activeArtifactId,
|
|
603
647
|
requestedMapping,
|
|
@@ -611,6 +655,7 @@ export async function getClassSource(svc, input) {
|
|
|
611
655
|
isDecompiled: activeOrigin === "decompiled",
|
|
612
656
|
qualityFlags: activeQualityFlags
|
|
613
657
|
}),
|
|
658
|
+
...(continuation?.suggestedCall ? { suggestedCall: continuation.suggestedCall } : {}),
|
|
614
659
|
...(resolvedOutputFile ? { outputFile: resolvedOutputFile } : {}),
|
|
615
660
|
warnings
|
|
616
661
|
};
|
|
@@ -629,7 +674,7 @@ export async function getClassMembers(svc, input) {
|
|
|
629
674
|
const includeInherited = input.includeInherited ?? false;
|
|
630
675
|
const memberPattern = normalizeOptionalString(input.memberPattern);
|
|
631
676
|
const parsedMaxMembers = normalizeStrictPositiveInt(input.maxMembers, "maxMembers");
|
|
632
|
-
const maxMembers = parsedMaxMembers == null ?
|
|
677
|
+
const maxMembers = parsedMaxMembers == null ? 150 : Math.min(parsedMaxMembers, 5000);
|
|
633
678
|
const normalizedArtifactId = normalizeOptionalString(input.artifactId);
|
|
634
679
|
if (normalizedArtifactId && input.target) {
|
|
635
680
|
throw createError({
|
|
@@ -759,7 +804,23 @@ export async function getClassMembers(svc, input) {
|
|
|
759
804
|
}
|
|
760
805
|
catch (error) {
|
|
761
806
|
if (isAppError(error) && error.code === ERROR_CODES.CLASS_NOT_FOUND) {
|
|
762
|
-
|
|
807
|
+
// Re-raise with the shared recovery shape (find-class/api-matrix
|
|
808
|
+
// suggestedCall, namespace + scope hints) instead of the sparse bytecode
|
|
809
|
+
// error, so members and source agree on CLASS_NOT_FOUND guidance.
|
|
810
|
+
throw buildClassSourceNotFoundError(svc, {
|
|
811
|
+
artifactId,
|
|
812
|
+
className,
|
|
813
|
+
lookupClassName,
|
|
814
|
+
mappingApplied,
|
|
815
|
+
requestedMapping,
|
|
816
|
+
qualityFlags,
|
|
817
|
+
attemptedBinaryFallback: true,
|
|
818
|
+
targetKind: input.target?.kind,
|
|
819
|
+
targetValue: input.target && "value" in input.target ? input.target.value : undefined,
|
|
820
|
+
scope: input.scope,
|
|
821
|
+
projectPath: input.projectPath,
|
|
822
|
+
version
|
|
823
|
+
});
|
|
763
824
|
}
|
|
764
825
|
binaryExtractionFailed = true;
|
|
765
826
|
binaryExtractionFailureReason = error instanceof Error ? error.message : String(error);
|
|
@@ -787,11 +848,29 @@ export async function getClassMembers(svc, input) {
|
|
|
787
848
|
warnings
|
|
788
849
|
});
|
|
789
850
|
const counts = remapped.counts;
|
|
790
|
-
|
|
851
|
+
// Offset cursor over the flat [constructors, fields, methods] member sequence.
|
|
852
|
+
// The context key ties a cursor to this exact query so a stale cursor restarts.
|
|
853
|
+
const memberCursorContext = buildPageContextKey([
|
|
854
|
+
artifactId,
|
|
855
|
+
lookupClassName,
|
|
856
|
+
requestedMapping,
|
|
857
|
+
mappingApplied,
|
|
858
|
+
access,
|
|
859
|
+
includeSynthetic,
|
|
860
|
+
includeInherited,
|
|
861
|
+
memberPattern
|
|
862
|
+
]);
|
|
863
|
+
const { offset: memberOffset, cursorIgnored: memberCursorIgnored } = resolveCursorOffset(input.cursor, memberCursorContext);
|
|
864
|
+
const sliced = sliceMembersWithLimit(remapped, counts.total, maxMembers, warnings, memberOffset);
|
|
791
865
|
const constructors = sliced.constructors;
|
|
792
866
|
const fields = sliced.fields;
|
|
793
867
|
const methods = sliced.methods;
|
|
868
|
+
// Slim the wire member shape: hoist a shared ownerFqn, drop accessFlags, omit
|
|
869
|
+
// isSynthetic:false, and drop FIELD jvmDescriptor unless includeDescriptors.
|
|
870
|
+
// Internal SignatureMember arrays above stay intact.
|
|
871
|
+
const projectedMembers = projectMembersForWire({ constructors, fields, methods }, includeInherited, input.includeDescriptors ?? false);
|
|
794
872
|
const truncated = sliced.truncated;
|
|
873
|
+
const nextCursor = sliced.nextOffset != null ? encodeOffsetCursor(sliced.nextOffset, memberCursorContext) : undefined;
|
|
795
874
|
const normalizedProvenance = provenance ??
|
|
796
875
|
buildFallbackProvenance(svc, {
|
|
797
876
|
artifactId,
|
|
@@ -843,7 +922,7 @@ export async function getClassMembers(svc, input) {
|
|
|
843
922
|
suggestedCall = buildSuggestedCall({
|
|
844
923
|
tool: "get-class-source",
|
|
845
924
|
params: {
|
|
846
|
-
target: {
|
|
925
|
+
target: { kind: "artifact", artifactId },
|
|
847
926
|
className,
|
|
848
927
|
mode: "snippet",
|
|
849
928
|
mapping: requestedMapping
|
|
@@ -861,13 +940,11 @@ export async function getClassMembers(svc, input) {
|
|
|
861
940
|
}
|
|
862
941
|
return {
|
|
863
942
|
className,
|
|
864
|
-
members:
|
|
865
|
-
constructors,
|
|
866
|
-
fields,
|
|
867
|
-
methods
|
|
868
|
-
},
|
|
943
|
+
members: projectedMembers,
|
|
869
944
|
counts,
|
|
870
945
|
truncated,
|
|
946
|
+
...(nextCursor ? { nextCursor } : {}),
|
|
947
|
+
...(memberCursorIgnored ? { cursorIgnored: true } : {}),
|
|
871
948
|
context: signatureContext,
|
|
872
949
|
origin,
|
|
873
950
|
artifactId,
|
package/dist/source/indexer.d.ts
CHANGED
|
@@ -1,7 +1,22 @@
|
|
|
1
|
+
import { resolveMojangTinyFile } from "../mojang-tiny-mapping-service.js";
|
|
1
2
|
import type { SourceService } from "../source-service.js";
|
|
2
3
|
import type { ArtifactIndexMetaRow } from "../storage/index-meta-repo.js";
|
|
3
4
|
import type { ArtifactRow, ResolvedSourceArtifact } from "../types.js";
|
|
5
|
+
import { remapJar } from "../tiny-remapper-service.js";
|
|
6
|
+
import { resolveTinyRemapperJar } from "../tiny-remapper-resolver.js";
|
|
4
7
|
export declare const INDEX_SCHEMA_VERSION = 1;
|
|
8
|
+
/**
|
|
9
|
+
* Injectable dependencies let remap-cache regression tests run without Java.
|
|
10
|
+
* Production callers use `defaultBinaryRemapDeps`; this is not a public MCP
|
|
11
|
+
* extension point.
|
|
12
|
+
*/
|
|
13
|
+
export type BinaryRemapDeps = {
|
|
14
|
+
resolveTinyRemapperJar: typeof resolveTinyRemapperJar;
|
|
15
|
+
resolveMojangTinyFile: typeof resolveMojangTinyFile;
|
|
16
|
+
remapJar: typeof remapJar;
|
|
17
|
+
now: () => number;
|
|
18
|
+
randomSuffix: () => string;
|
|
19
|
+
};
|
|
5
20
|
export type IndexRebuildReason = "force" | "missing_meta" | "schema_mismatch" | "signature_mismatch" | "already_current";
|
|
6
21
|
export interface IndexedFileRecord {
|
|
7
22
|
filePath: string;
|
|
@@ -57,13 +72,12 @@ export declare function ingestIfNeeded(svc: SourceService, resolved: ResolvedSou
|
|
|
57
72
|
* binary remap, run tiny-remapper now and return the remapped jar path.
|
|
58
73
|
* Otherwise return the original binaryJarPath unchanged.
|
|
59
74
|
*
|
|
60
|
-
* Cache safety: writes to a per-attempt temp
|
|
61
|
-
* <cacheDir>/remapped/<artifactId>.jar. A per-target
|
|
62
|
-
* collapses concurrent calls so
|
|
63
|
-
* the same artifactId share one tiny-remapper run
|
|
64
|
-
* same output path.
|
|
75
|
+
* Cache safety: writes to a per-attempt `.jar` temp path, validates ZIP magic,
|
|
76
|
+
* then atomic-renames into <cacheDir>/remapped/<artifactId>.jar. A per-target
|
|
77
|
+
* inflight Promise map collapses concurrent calls so simultaneous
|
|
78
|
+
* resolveArtifact calls for the same artifactId share one tiny-remapper run.
|
|
65
79
|
*/
|
|
66
|
-
export declare function maybeRemapBinaryForMojang(svc: SourceService, resolved: ResolvedSourceArtifact): Promise<string>;
|
|
80
|
+
export declare function maybeRemapBinaryForMojang(svc: SourceService, resolved: ResolvedSourceArtifact, deps?: BinaryRemapDeps): Promise<string>;
|
|
67
81
|
export declare function recordRemappedJarBytesFromDisk(svc: SourceService, artifactId: string, path: string): Promise<void>;
|
|
68
82
|
/**
|
|
69
83
|
* Best-effort structural check that `path` is a non-empty file beginning with
|
|
@@ -73,10 +87,21 @@ export declare function recordRemappedJarBytesFromDisk(svc: SourceService, artif
|
|
|
73
87
|
* are not (a corrupt cache hit must be evicted).
|
|
74
88
|
*/
|
|
75
89
|
export declare function isUsableJarFile(path: string): Promise<boolean>;
|
|
90
|
+
export declare function buildBinaryRemapTempPath(remappedJarPath: string, input: {
|
|
91
|
+
pid: number;
|
|
92
|
+
now: number;
|
|
93
|
+
randomSuffix: string;
|
|
94
|
+
}): string;
|
|
76
95
|
export declare function runBinaryRemap(svc: SourceService, input: {
|
|
77
96
|
version: string;
|
|
78
97
|
inputJar: string;
|
|
79
98
|
remappedDir: string;
|
|
80
99
|
remappedJarPath: string;
|
|
81
100
|
}): Promise<string>;
|
|
101
|
+
export declare function runBinaryRemapWithDeps(svc: SourceService, input: {
|
|
102
|
+
version: string;
|
|
103
|
+
inputJar: string;
|
|
104
|
+
remappedDir: string;
|
|
105
|
+
remappedJarPath: string;
|
|
106
|
+
}, deps: BinaryRemapDeps): Promise<string>;
|
|
82
107
|
export declare function loadFromSourceJar(svc: SourceService, sourceJarPath: string): Promise<IndexedFileRecord[]>;
|
package/dist/source/indexer.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
|
-
import { mkdir, open, rename,
|
|
4
|
-
import { join } from "node:path";
|
|
3
|
+
import { mkdir, open, rename, rm, stat } from "node:fs/promises";
|
|
4
|
+
import { basename, dirname, join } from "node:path";
|
|
5
5
|
import { buildSuggestedCall } from "../build-suggested-call.js";
|
|
6
6
|
import { decompileBinaryJar } from "../decompiler/vineflower.js";
|
|
7
7
|
import { ERROR_CODES, createError, isAppError } from "../errors.js";
|
|
@@ -15,6 +15,13 @@ import { resolveVineflowerJar } from "../vineflower-resolver.js";
|
|
|
15
15
|
import { enforceCacheLimits, recordRemappedJarBytes, releaseRemappedJarBytes, touchCacheMetrics, upsertCacheMetrics } from "./cache-metrics.js";
|
|
16
16
|
import { normalizePathStyle } from "./shared-utils.js";
|
|
17
17
|
export const INDEX_SCHEMA_VERSION = 1;
|
|
18
|
+
const defaultBinaryRemapDeps = {
|
|
19
|
+
resolveTinyRemapperJar,
|
|
20
|
+
resolveMojangTinyFile,
|
|
21
|
+
remapJar,
|
|
22
|
+
now: () => Date.now(),
|
|
23
|
+
randomSuffix: () => Math.random().toString(36).slice(2, 8) || "0"
|
|
24
|
+
};
|
|
18
25
|
function chunkArray(items, chunkSize) {
|
|
19
26
|
const size = Math.max(1, Math.trunc(chunkSize));
|
|
20
27
|
if (items.length === 0) {
|
|
@@ -36,8 +43,8 @@ export async function indexArtifact(svc, input) {
|
|
|
36
43
|
}
|
|
37
44
|
const artifact = svc.getArtifact(artifactId);
|
|
38
45
|
const force = input.force ?? false;
|
|
39
|
-
const hasFiles = svc.filesRepo.listFiles(artifact.artifactId, { limit: 1 }).items.length > 0;
|
|
40
46
|
const meta = svc.indexMetaRepo.get(artifact.artifactId);
|
|
47
|
+
const hasFiles = meta ? meta.filesCount > 0 : false;
|
|
41
48
|
const expectedSignature = artifact.artifactSignature ?? fallbackArtifactSignature(artifact.artifactId);
|
|
42
49
|
const reason = resolveIndexRebuildReason({
|
|
43
50
|
force,
|
|
@@ -284,8 +291,11 @@ export function getArtifact(svc, artifactId) {
|
|
|
284
291
|
}
|
|
285
292
|
export async function ingestIfNeeded(svc, resolved) {
|
|
286
293
|
const existing = svc.artifactsRepo.getArtifact(resolved.artifactId);
|
|
287
|
-
const hasFiles = svc.filesRepo.listFiles(resolved.artifactId, { limit: 1 }).items.length > 0;
|
|
288
294
|
const meta = svc.indexMetaRepo.get(resolved.artifactId);
|
|
295
|
+
// Derive hasFiles from meta instead of a separate listFiles probe: when meta is
|
|
296
|
+
// absent the reason is "missing_meta" regardless of hasFiles, and when present
|
|
297
|
+
// meta.filesCount is the authoritative count written alongside the file rows.
|
|
298
|
+
const hasFiles = meta ? meta.filesCount > 0 : false;
|
|
289
299
|
const reason = resolveIndexRebuildReason({
|
|
290
300
|
force: false,
|
|
291
301
|
expectedSignature: resolved.artifactSignature,
|
|
@@ -354,13 +364,12 @@ async function rebuildMissingArtifactIndex(svc, resolved, reason) {
|
|
|
354
364
|
* binary remap, run tiny-remapper now and return the remapped jar path.
|
|
355
365
|
* Otherwise return the original binaryJarPath unchanged.
|
|
356
366
|
*
|
|
357
|
-
* Cache safety: writes to a per-attempt temp
|
|
358
|
-
* <cacheDir>/remapped/<artifactId>.jar. A per-target
|
|
359
|
-
* collapses concurrent calls so
|
|
360
|
-
* the same artifactId share one tiny-remapper run
|
|
361
|
-
* same output path.
|
|
367
|
+
* Cache safety: writes to a per-attempt `.jar` temp path, validates ZIP magic,
|
|
368
|
+
* then atomic-renames into <cacheDir>/remapped/<artifactId>.jar. A per-target
|
|
369
|
+
* inflight Promise map collapses concurrent calls so simultaneous
|
|
370
|
+
* resolveArtifact calls for the same artifactId share one tiny-remapper run.
|
|
362
371
|
*/
|
|
363
|
-
export async function maybeRemapBinaryForMojang(svc, resolved) {
|
|
372
|
+
export async function maybeRemapBinaryForMojang(svc, resolved, deps = defaultBinaryRemapDeps) {
|
|
364
373
|
const binaryJarPath = resolved.binaryJarPath;
|
|
365
374
|
if (!binaryJarPath) {
|
|
366
375
|
throw createError({
|
|
@@ -400,10 +409,19 @@ export async function maybeRemapBinaryForMojang(svc, resolved) {
|
|
|
400
409
|
remappedJarPath
|
|
401
410
|
});
|
|
402
411
|
try {
|
|
403
|
-
await
|
|
412
|
+
await rm(remappedJarPath, { recursive: true, force: true });
|
|
404
413
|
}
|
|
405
|
-
catch {
|
|
406
|
-
|
|
414
|
+
catch (caughtError) {
|
|
415
|
+
releaseRemappedJarBytes(svc, resolved.artifactId);
|
|
416
|
+
throw createError({
|
|
417
|
+
code: ERROR_CODES.REMAP_FAILED,
|
|
418
|
+
message: "Failed to remove corrupt binary remap cache entry.",
|
|
419
|
+
details: {
|
|
420
|
+
artifactId: resolved.artifactId,
|
|
421
|
+
remappedJarPath,
|
|
422
|
+
cause: caughtError instanceof Error ? caughtError.message : String(caughtError)
|
|
423
|
+
}
|
|
424
|
+
});
|
|
407
425
|
}
|
|
408
426
|
releaseRemappedJarBytes(svc, resolved.artifactId);
|
|
409
427
|
}
|
|
@@ -411,12 +429,12 @@ export async function maybeRemapBinaryForMojang(svc, resolved) {
|
|
|
411
429
|
if (inflight) {
|
|
412
430
|
return inflight;
|
|
413
431
|
}
|
|
414
|
-
const remapPromise =
|
|
432
|
+
const remapPromise = runBinaryRemapWithDeps(svc, {
|
|
415
433
|
version: resolved.version,
|
|
416
434
|
inputJar: binaryJarPath,
|
|
417
435
|
remappedDir,
|
|
418
436
|
remappedJarPath
|
|
419
|
-
});
|
|
437
|
+
}, deps);
|
|
420
438
|
svc.state.inflightRemaps.set(remappedJarPath, remapPromise);
|
|
421
439
|
try {
|
|
422
440
|
const path = await remapPromise;
|
|
@@ -430,6 +448,9 @@ export async function maybeRemapBinaryForMojang(svc, resolved) {
|
|
|
430
448
|
export async function recordRemappedJarBytesFromDisk(svc, artifactId, path) {
|
|
431
449
|
try {
|
|
432
450
|
const fileStat = await stat(path);
|
|
451
|
+
if (!fileStat.isFile()) {
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
433
454
|
recordRemappedJarBytes(svc, artifactId, fileStat.size);
|
|
434
455
|
}
|
|
435
456
|
catch {
|
|
@@ -467,14 +488,45 @@ export async function isUsableJarFile(path) {
|
|
|
467
488
|
await handle?.close().catch(() => undefined);
|
|
468
489
|
}
|
|
469
490
|
}
|
|
491
|
+
export function buildBinaryRemapTempPath(remappedJarPath, input) {
|
|
492
|
+
// Keep the final extension as `.jar`; the remap output is rejected unless it
|
|
493
|
+
// is a regular ZIP/JAR file, and manage-cache recognizes this temp shape.
|
|
494
|
+
const fileName = basename(remappedJarPath);
|
|
495
|
+
const artifactId = fileName.endsWith(".jar")
|
|
496
|
+
? fileName.slice(0, -".jar".length)
|
|
497
|
+
: fileName;
|
|
498
|
+
return join(dirname(remappedJarPath), `${artifactId}.tmp.${input.pid}.${input.now}.${input.randomSuffix}.jar`);
|
|
499
|
+
}
|
|
500
|
+
async function describePathKind(path) {
|
|
501
|
+
try {
|
|
502
|
+
const stats = await stat(path);
|
|
503
|
+
if (stats.isFile()) {
|
|
504
|
+
return "file";
|
|
505
|
+
}
|
|
506
|
+
if (stats.isDirectory()) {
|
|
507
|
+
return "directory";
|
|
508
|
+
}
|
|
509
|
+
return "other";
|
|
510
|
+
}
|
|
511
|
+
catch {
|
|
512
|
+
return "missing";
|
|
513
|
+
}
|
|
514
|
+
}
|
|
470
515
|
export async function runBinaryRemap(svc, input) {
|
|
471
|
-
|
|
472
|
-
|
|
516
|
+
return runBinaryRemapWithDeps(svc, input, defaultBinaryRemapDeps);
|
|
517
|
+
}
|
|
518
|
+
export async function runBinaryRemapWithDeps(svc, input, deps) {
|
|
519
|
+
const tinyRemapperJarPath = await deps.resolveTinyRemapperJar(svc.config.cacheDir, svc.config.tinyRemapperJarPath);
|
|
520
|
+
const mojangTiny = await deps.resolveMojangTinyFile(input.version, svc.config);
|
|
473
521
|
await mkdir(input.remappedDir, { recursive: true });
|
|
474
|
-
const tempPath =
|
|
522
|
+
const tempPath = buildBinaryRemapTempPath(input.remappedJarPath, {
|
|
523
|
+
pid: process.pid,
|
|
524
|
+
now: deps.now(),
|
|
525
|
+
randomSuffix: deps.randomSuffix()
|
|
526
|
+
});
|
|
475
527
|
const remapStartedAt = Date.now();
|
|
476
528
|
try {
|
|
477
|
-
await remapJar(tinyRemapperJarPath, {
|
|
529
|
+
await deps.remapJar(tinyRemapperJarPath, {
|
|
478
530
|
inputJar: input.inputJar,
|
|
479
531
|
outputJar: tempPath,
|
|
480
532
|
mappingsFile: mojangTiny.path,
|
|
@@ -483,20 +535,38 @@ export async function runBinaryRemap(svc, input) {
|
|
|
483
535
|
timeoutMs: svc.config.remapTimeoutMs,
|
|
484
536
|
maxMemoryMb: svc.config.remapMaxMemoryMb
|
|
485
537
|
});
|
|
486
|
-
|
|
487
|
-
|
|
538
|
+
if (!(await isUsableJarFile(tempPath))) {
|
|
539
|
+
throw createError({
|
|
540
|
+
code: ERROR_CODES.REMAP_FAILED,
|
|
541
|
+
message: "tiny-remapper produced an invalid output jar.",
|
|
542
|
+
details: {
|
|
543
|
+
inputJar: input.inputJar,
|
|
544
|
+
tempPath,
|
|
545
|
+
outputKind: await describePathKind(tempPath)
|
|
546
|
+
}
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
try {
|
|
550
|
+
await rename(tempPath, input.remappedJarPath);
|
|
551
|
+
}
|
|
552
|
+
catch (caughtError) {
|
|
488
553
|
throw createError({
|
|
489
554
|
code: ERROR_CODES.REMAP_FAILED,
|
|
490
|
-
message: "
|
|
491
|
-
details: {
|
|
555
|
+
message: "Failed to finalize binary remap cache entry.",
|
|
556
|
+
details: {
|
|
557
|
+
inputJar: input.inputJar,
|
|
558
|
+
tempPath,
|
|
559
|
+
remappedJarPath: input.remappedJarPath,
|
|
560
|
+
outputKind: await describePathKind(input.remappedJarPath),
|
|
561
|
+
cause: caughtError instanceof Error ? caughtError.message : String(caughtError)
|
|
562
|
+
}
|
|
492
563
|
});
|
|
493
564
|
}
|
|
494
|
-
await rename(tempPath, input.remappedJarPath);
|
|
495
565
|
return input.remappedJarPath;
|
|
496
566
|
}
|
|
497
567
|
catch (caughtError) {
|
|
498
568
|
try {
|
|
499
|
-
await
|
|
569
|
+
await rm(tempPath, { recursive: true, force: true });
|
|
500
570
|
}
|
|
501
571
|
catch {
|
|
502
572
|
// tempPath may not exist if remapJar failed before writing anything; ignore.
|
package/dist/source/search.d.ts
CHANGED
|
@@ -44,7 +44,7 @@ export declare function searchClassSource(svc: SourceService, input: SearchClass
|
|
|
44
44
|
export declare function searchSymbolIntent(svc: SourceService, artifactId: string, query: string, match: SearchMatch, scope: SearchScope | undefined, regexPattern: RegExp | undefined, onHit: (hit: SearchSourceHit) => void): void;
|
|
45
45
|
export declare function searchTextIntentIndexed(svc: SourceService, artifactId: string, query: string, match: SearchMatch, scope: SearchScope | undefined, onHit: (hit: SearchSourceHit) => void): void;
|
|
46
46
|
export declare function searchPathIntentIndexed(svc: SourceService, artifactId: string, query: string, match: SearchMatch, scope: SearchScope | undefined, onHit: (hit: SearchSourceHit) => void): void;
|
|
47
|
-
export declare function searchTextIntent(svc: SourceService, artifactId: string, query: string, match: SearchMatch, scope: SearchScope | undefined, regexPattern: RegExp | undefined, onHit: (hit: SearchSourceHit) => void): void;
|
|
47
|
+
export declare function searchTextIntent(svc: SourceService, artifactId: string, query: string, match: SearchMatch, scope: SearchScope | undefined, regexPattern: RegExp | undefined, onHit: (hit: SearchSourceHit) => void, onWarning?: (warning: string) => void): void;
|
|
48
48
|
export declare function searchPathIntent(svc: SourceService, artifactId: string, query: string, match: SearchMatch, scope: SearchScope | undefined, regexPattern: RegExp | undefined, onHit: (hit: SearchSourceHit) => void): void;
|
|
49
49
|
export declare function findSymbolHits(svc: SourceService, artifactId: string, query: string, match: SearchMatch, scope: SearchScope | undefined, regexPattern: RegExp | undefined): IndexedSymbolHit[];
|
|
50
50
|
export declare function indexedCandidateLimit(svc: SourceService): number;
|