@adhisang/minecraft-modding-mcp 6.2.0 → 7.0.0-rc.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 +71 -0
- package/README.md +21 -5
- package/dist/cache-policy.d.ts +71 -0
- package/dist/cache-policy.js +83 -0
- package/dist/cache-registry.js +6 -6
- package/dist/cli.js +74 -3
- package/dist/compat-stdio-transport.d.ts +1 -1
- package/dist/compat-stdio-transport.js +13 -1
- package/dist/config.d.ts +3 -0
- package/dist/config.js +8 -2
- package/dist/decompiler/vineflower.d.ts +1 -0
- package/dist/decompiler/vineflower.js +8 -5
- package/dist/entry-tools/analyze-mod-service.d.ts +70 -136
- package/dist/entry-tools/analyze-symbol-service.d.ts +112 -150
- package/dist/entry-tools/compare-minecraft-service.d.ts +59 -145
- package/dist/entry-tools/entry-tool-schema.d.ts +38 -4
- package/dist/entry-tools/entry-tool-schema.js +4 -1
- package/dist/entry-tools/inspect-minecraft/internal.d.ts +235 -799
- package/dist/entry-tools/inspect-minecraft/internal.js +65 -28
- package/dist/entry-tools/inspect-minecraft-service.d.ts +372 -1736
- package/dist/entry-tools/inspect-minecraft-service.js +1 -1
- package/dist/entry-tools/manage-cache-service.d.ts +81 -91
- package/dist/entry-tools/validate-project/cases/project-summary.d.ts +7 -7
- package/dist/entry-tools/validate-project-service.d.ts +164 -592
- package/dist/entry-tools/verify-mixin-target-service.d.ts +3 -19
- package/dist/era-classifier.d.ts +161 -0
- package/dist/era-classifier.js +292 -0
- package/dist/error-mapping.js +9 -2
- package/dist/index.d.ts +42 -4
- package/dist/index.js +637 -475
- package/dist/java-process.d.ts +2 -0
- package/dist/java-process.js +22 -2
- package/dist/json-rpc-framing.d.ts +77 -1
- package/dist/json-rpc-framing.js +249 -13
- package/dist/mapping/loaders/tiny-loom-selection.d.ts +88 -0
- package/dist/mapping/loaders/tiny-loom-selection.js +223 -0
- package/dist/mapping/loaders/tiny-loom.js +45 -33
- package/dist/mapping/loaders/tiny-maven.js +6 -11
- package/dist/mapping/parsers/tiny.d.ts +57 -0
- package/dist/mapping/parsers/tiny.js +99 -22
- package/dist/mapping-service.d.ts +19 -0
- package/dist/mapping-service.js +93 -9
- package/dist/mcp-helpers.d.ts +19 -2
- package/dist/mcp-helpers.js +48 -6
- package/dist/minecraft-explorer-service.d.ts +1 -1
- package/dist/mixin/types.d.ts +8 -0
- package/dist/mod-analyzer.js +7 -7
- package/dist/mod-decompile-service.js +1 -0
- package/dist/nbt/java-nbt-codec.js +12 -2
- package/dist/nbt/json-patch.js +14 -3
- package/dist/nbt/pipeline.js +40 -3
- package/dist/nbt/typed-json.js +26 -1
- package/dist/registration-adapter.d.ts +32 -0
- package/dist/registration-adapter.js +52 -0
- package/dist/request-context.d.ts +7 -0
- package/dist/request-context.js +9 -0
- package/dist/resources.d.ts +1 -1
- package/dist/resources.js +25 -19
- package/dist/server-identity.d.ts +27 -0
- package/dist/server-identity.js +26 -0
- package/dist/source/access-validate.js +53 -0
- package/dist/source/artifact-resolver.d.ts +69 -1
- package/dist/source/artifact-resolver.js +215 -14
- package/dist/source/class-source.d.ts +22 -0
- package/dist/source/class-source.js +162 -29
- package/dist/source/did-you-mean.d.ts +12 -1
- package/dist/source/did-you-mean.js +6 -2
- package/dist/source/file-access.js +150 -46
- package/dist/source/indexer.js +1 -0
- package/dist/source/nested-jars.d.ts +19 -0
- package/dist/source/nested-jars.js +90 -21
- package/dist/source/shared-utils.d.ts +21 -0
- package/dist/source/shared-utils.js +23 -0
- package/dist/source-service.d.ts +12 -0
- package/dist/source-service.js +3 -0
- package/dist/stdio-supervisor.d.ts +357 -2
- package/dist/stdio-supervisor.js +1031 -80
- package/dist/storage/db.d.ts +2 -1
- package/dist/storage/db.js +15 -8
- package/dist/synthetic-decorator.d.ts +24 -0
- package/dist/synthetic-decorator.js +48 -0
- package/dist/tool-contract-manifest.js +1 -1
- package/dist/tool-guidance.d.ts +17 -1
- package/dist/tool-guidance.js +417 -13
- package/dist/tool-schema-registry.d.ts +2 -0
- package/dist/tool-schema-registry.js +4 -0
- package/dist/tool-schemas.d.ts +2214 -3915
- package/dist/tool-schemas.js +34 -7
- package/dist/types.d.ts +35 -0
- package/dist/v1-parity-schemas.d.ts +7 -0
- package/dist/v1-parity-schemas.js +5584 -0
- package/dist/version-diff-service.d.ts +33 -0
- package/dist/version-diff-service.js +148 -3
- package/dist/version-service.js +36 -14
- package/dist/warning-details.js +18 -1
- package/docs/README-ja.md +9 -3
- package/docs/tool-reference.md +267 -22
- package/package.json +12 -9
|
@@ -8,9 +8,9 @@ import { buildClassSourceSnippet } from "./class-source/snippet-builder.js";
|
|
|
8
8
|
import { remapAndCountMembers, sliceMembersWithLimit, projectMembersForWire, projectMembersByLevel } from "./class-source/members-builder.js";
|
|
9
9
|
import { collectDidYouMeanCandidates } from "./did-you-mean.js";
|
|
10
10
|
import { matchesMemberPattern } from "./member-pattern.js";
|
|
11
|
-
import { resolveUniqueNestedJarForClass } from "./nested-jars.js";
|
|
11
|
+
import { findNestedJarClasses, resolveUniqueNestedJarForClass } from "./nested-jars.js";
|
|
12
12
|
import { buildPageContextKey, encodeOffsetCursor, resolveCursorOffset } from "../page-cursor.js";
|
|
13
|
-
import { dedupeQualityFlags, normalizeMapping, normalizeOptionalString, normalizePathStyle } from "./shared-utils.js";
|
|
13
|
+
import { dedupeQualityFlags, inheritArtifactMapping, normalizeMapping, normalizeOptionalString, normalizePathStyle } from "./shared-utils.js";
|
|
14
14
|
import { isUnobfuscatedVersion } from "../version-service.js";
|
|
15
15
|
const MEMBERS_STATUS_LEGACY = process.env.MEMBERS_STATUS_LEGACY === "1";
|
|
16
16
|
/**
|
|
@@ -82,6 +82,14 @@ function obfuscatedNamespaceHint(className) {
|
|
|
82
82
|
function hasPartialNetMinecraftCoverage(qualityFlags) {
|
|
83
83
|
return qualityFlags.includes("partial-source-no-net-minecraft");
|
|
84
84
|
}
|
|
85
|
+
function shouldSuggestObfuscatedMapping(artifact, className) {
|
|
86
|
+
const nativeDependency = artifact.provenance?.dependencyResolution != null;
|
|
87
|
+
const shellArtifact = artifact.qualityFlags.includes("shell-jar");
|
|
88
|
+
return (artifact.mappingApplied === "obfuscated" &&
|
|
89
|
+
!nativeDependency &&
|
|
90
|
+
!shellArtifact &&
|
|
91
|
+
looksLikeDeobfuscatedClassName(className));
|
|
92
|
+
}
|
|
85
93
|
function classNameToClassPath(className) {
|
|
86
94
|
const normalized = normalizePathStyle(className.trim()).replace(/\//g, ".");
|
|
87
95
|
const segments = normalized.split(".").filter((segment) => segment.length > 0);
|
|
@@ -167,10 +175,26 @@ export function buildFallbackProvenance(svc, input) {
|
|
|
167
175
|
transformChain
|
|
168
176
|
};
|
|
169
177
|
}
|
|
178
|
+
/**
|
|
179
|
+
* Near-miss candidates from the requested artifact's index followed by any the
|
|
180
|
+
* lookup's final artifact contributes, deduplicated by FQN so a class present in
|
|
181
|
+
* both is reported once, under the requested artifact.
|
|
182
|
+
*/
|
|
183
|
+
function unionDidYouMeanCandidates(svc, requestedArtifactId, endedOnArtifactId, className) {
|
|
184
|
+
const requested = collectDidYouMeanCandidates(svc, requestedArtifactId, className);
|
|
185
|
+
if (endedOnArtifactId === requestedArtifactId) {
|
|
186
|
+
return requested;
|
|
187
|
+
}
|
|
188
|
+
const seen = new Set(requested.map((candidate) => candidate.className));
|
|
189
|
+
const fromFallback = collectDidYouMeanCandidates(svc, endedOnArtifactId, className, endedOnArtifactId).filter((candidate) => !seen.has(candidate.className));
|
|
190
|
+
return [...requested, ...fromFallback];
|
|
191
|
+
}
|
|
170
192
|
export function buildClassSourceNotFoundError(svc, input) {
|
|
171
193
|
const simpleName = input.className.split(/[.$]/).at(-1) ?? input.className;
|
|
194
|
+
const requestedArtifactId = input.requestedArtifactId ?? input.artifactId;
|
|
172
195
|
const details = {
|
|
173
|
-
artifactId:
|
|
196
|
+
artifactId: requestedArtifactId,
|
|
197
|
+
...(input.artifactId !== requestedArtifactId ? { fallbackArtifactId: input.artifactId } : {}),
|
|
174
198
|
className: input.className,
|
|
175
199
|
mapping: input.mappingApplied,
|
|
176
200
|
qualityFlags: input.qualityFlags,
|
|
@@ -183,12 +207,18 @@ export function buildClassSourceNotFoundError(svc, input) {
|
|
|
183
207
|
...(input.nestedJars && input.nestedJars.length > 0 ? { nestedJars: input.nestedJars } : {}),
|
|
184
208
|
// Candidates are hints from the symbol index, never assertions that the
|
|
185
209
|
// class exists at the suggested location; empty when nothing usable.
|
|
186
|
-
|
|
210
|
+
//
|
|
211
|
+
// Both indexes are consulted, requested artifact first. Collecting from the
|
|
212
|
+
// requested artifact ALONE is empty by construction in the scenario the
|
|
213
|
+
// partial-source fallback exists to serve: that artifact is the one without
|
|
214
|
+
// net.minecraft symbols, which is why the fallback fired and indexed the other
|
|
215
|
+
// jar. Candidates from the artifact the caller did not name carry its id.
|
|
216
|
+
didYouMean: unionDidYouMeanCandidates(svc, requestedArtifactId, input.artifactId, input.className)
|
|
187
217
|
};
|
|
188
218
|
let nextAction = `Use find-class to resolve the correct fully-qualified name for "${simpleName}".`;
|
|
189
219
|
let suggestionSpec = {
|
|
190
220
|
tool: "find-class",
|
|
191
|
-
params: { className: simpleName, artifactId:
|
|
221
|
+
params: { className: simpleName, artifactId: requestedArtifactId }
|
|
192
222
|
};
|
|
193
223
|
if (input.targetKind === "version" && input.scope && input.scope !== "merged" && !input.projectPath) {
|
|
194
224
|
nextAction +=
|
|
@@ -217,7 +247,7 @@ export function buildClassSourceNotFoundError(svc, input) {
|
|
|
217
247
|
else {
|
|
218
248
|
suggestionSpec = {
|
|
219
249
|
tool: "find-class",
|
|
220
|
-
params: { className: simpleName, artifactId:
|
|
250
|
+
params: { className: simpleName, artifactId: requestedArtifactId }
|
|
221
251
|
};
|
|
222
252
|
}
|
|
223
253
|
}
|
|
@@ -395,13 +425,14 @@ export function findClass(svc, input) {
|
|
|
395
425
|
const filteredMatches = partialVanillaLookup && matches.every((match) => !match.qualifiedName.startsWith("net.minecraft.") && !match.qualifiedName.startsWith("com.mojang."))
|
|
396
426
|
? []
|
|
397
427
|
: matches;
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
428
|
+
return finishFindClass(svc, {
|
|
429
|
+
artifact,
|
|
430
|
+
artifactId,
|
|
431
|
+
className,
|
|
432
|
+
matches: filteredMatches,
|
|
433
|
+
partialVanillaLookup,
|
|
434
|
+
warnings
|
|
435
|
+
});
|
|
405
436
|
}
|
|
406
437
|
const result = svc.symbolsRepo.findScopedSymbols({
|
|
407
438
|
artifactId,
|
|
@@ -410,32 +441,110 @@ export function findClass(svc, input) {
|
|
|
410
441
|
symbolKinds: TYPE_SYMBOL_KINDS,
|
|
411
442
|
limit: limit * 5
|
|
412
443
|
});
|
|
413
|
-
|
|
444
|
+
// The extractor stores ONE qualifiedName per FILE (the top-level type), so a
|
|
445
|
+
// nested type's row carries its OUTER type's FQN. Reporting that verbatim
|
|
446
|
+
// returned a qualifiedName that did not contain the searched token at all —
|
|
447
|
+
// feeding match[0] into get-class-source then fetched the wrong class.
|
|
448
|
+
const candidates = [];
|
|
414
449
|
for (const row of result.items) {
|
|
415
|
-
if (matches.length >= limit)
|
|
416
|
-
break;
|
|
417
450
|
const isTypeSymbol = row.symbolKind === "class" || row.symbolKind === "interface" ||
|
|
418
451
|
row.symbolKind === "enum" || row.symbolKind === "record";
|
|
419
452
|
if (!isTypeSymbol)
|
|
420
453
|
continue;
|
|
421
|
-
|
|
422
|
-
|
|
454
|
+
const enclosingQualifiedName = row.qualifiedName ?? row.filePath.replace(/\.java$/, "").replaceAll("/", ".");
|
|
455
|
+
const enclosingSimpleName = enclosingQualifiedName.split(".").at(-1) ?? enclosingQualifiedName;
|
|
456
|
+
const nested = enclosingSimpleName !== row.symbolName;
|
|
457
|
+
candidates.push({
|
|
458
|
+
qualifiedName: nested ? `${enclosingQualifiedName}.${row.symbolName}` : enclosingQualifiedName,
|
|
423
459
|
filePath: row.filePath,
|
|
424
460
|
line: row.line,
|
|
425
|
-
symbolKind: row.symbolKind
|
|
461
|
+
symbolKind: row.symbolKind,
|
|
462
|
+
...(nested ? { nested: true, enclosingClass: enclosingQualifiedName } : {})
|
|
426
463
|
});
|
|
427
464
|
}
|
|
465
|
+
// A top-level type named exactly like the query is what the caller almost
|
|
466
|
+
// always means; a nested type that merely shares the simple name comes after.
|
|
467
|
+
candidates.sort((left, right) => {
|
|
468
|
+
const leftNested = left.nested === true ? 1 : 0;
|
|
469
|
+
const rightNested = right.nested === true ? 1 : 0;
|
|
470
|
+
if (leftNested !== rightNested)
|
|
471
|
+
return leftNested - rightNested;
|
|
472
|
+
return left.qualifiedName.localeCompare(right.qualifiedName);
|
|
473
|
+
});
|
|
474
|
+
const matches = candidates.slice(0, limit);
|
|
428
475
|
const partialVanillaLookup = hasPartialNetMinecraftCoverage(artifact.qualityFlags) && looksLikeDeobfuscatedClassName(className);
|
|
429
476
|
const filteredMatches = partialVanillaLookup && matches.every((match) => !match.qualifiedName.startsWith("net.minecraft.") && !match.qualifiedName.startsWith("com.mojang."))
|
|
430
477
|
? []
|
|
431
478
|
: matches;
|
|
432
|
-
|
|
479
|
+
return finishFindClass(svc, {
|
|
480
|
+
artifact,
|
|
481
|
+
artifactId,
|
|
482
|
+
className,
|
|
483
|
+
matches: filteredMatches,
|
|
484
|
+
partialVanillaLookup,
|
|
485
|
+
warnings
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
/**
|
|
489
|
+
* Shared tail for both findClass branches: coverage/namespace warnings plus a
|
|
490
|
+
* machine-usable recovery route.
|
|
491
|
+
*
|
|
492
|
+
* An empty result on an artifact whose index excludes net.minecraft is
|
|
493
|
+
* internally consistent but externally contradictory — get-class-source answers
|
|
494
|
+
* the SAME class from the binary fallback. The caller therefore gets both the
|
|
495
|
+
* reason and the exact call that works.
|
|
496
|
+
*/
|
|
497
|
+
function finishFindClass(svc, input) {
|
|
498
|
+
const { artifact, artifactId, className, matches, partialVanillaLookup, warnings } = input;
|
|
499
|
+
let suggestedCall;
|
|
500
|
+
if (matches.length === 0 && partialVanillaLookup) {
|
|
433
501
|
warnings.push(`Artifact source coverage is partial and excludes net.minecraft; returning non-vanilla matches for "${className}" would be misleading. Use get-class-source/get-class-members for binary fallback or get-class-api-matrix for mapped API inspection.`);
|
|
502
|
+
suggestedCall = buildSuggestedCall({
|
|
503
|
+
tool: "get-class-source",
|
|
504
|
+
params: {
|
|
505
|
+
className,
|
|
506
|
+
target: { kind: "artifact", artifactId },
|
|
507
|
+
mode: "metadata"
|
|
508
|
+
}
|
|
509
|
+
}).suggestedCall;
|
|
434
510
|
}
|
|
435
|
-
if (
|
|
511
|
+
if (matches.length === 0 && shouldSuggestObfuscatedMapping(artifact, className)) {
|
|
436
512
|
warnings.push(`No exact class symbol matched "${className}". ${obfuscatedNamespaceHint(className)}`);
|
|
437
513
|
}
|
|
438
|
-
return {
|
|
514
|
+
return {
|
|
515
|
+
matches,
|
|
516
|
+
total: matches.length,
|
|
517
|
+
warnings,
|
|
518
|
+
...(suggestedCall ? { suggestedCall } : {})
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
export async function findClassIncludingNested(svc, input) {
|
|
522
|
+
const indexed = findClass(svc, input);
|
|
523
|
+
if (indexed.total > 0) {
|
|
524
|
+
return indexed;
|
|
525
|
+
}
|
|
526
|
+
const artifact = svc.getArtifact(input.artifactId.trim());
|
|
527
|
+
const inventory = artifact.provenance?.nestedJars;
|
|
528
|
+
if (!artifact.qualityFlags.includes("shell-jar") ||
|
|
529
|
+
!artifact.binaryJarPath ||
|
|
530
|
+
!inventory ||
|
|
531
|
+
inventory.length === 0) {
|
|
532
|
+
return indexed;
|
|
533
|
+
}
|
|
534
|
+
const limit = Math.max(1, Math.min(input.limit ?? 20, 200));
|
|
535
|
+
const matches = await findNestedJarClasses({
|
|
536
|
+
cacheDir: svc.config.cacheDir,
|
|
537
|
+
outerJarPath: artifact.binaryJarPath,
|
|
538
|
+
outerSignature: artifact.artifactId,
|
|
539
|
+
inventory,
|
|
540
|
+
className: input.className,
|
|
541
|
+
limit
|
|
542
|
+
});
|
|
543
|
+
return {
|
|
544
|
+
matches,
|
|
545
|
+
total: matches.length,
|
|
546
|
+
warnings: indexed.warnings
|
|
547
|
+
};
|
|
439
548
|
}
|
|
440
549
|
export async function getClassSource(svc, input) {
|
|
441
550
|
const className = input.className.trim();
|
|
@@ -514,7 +623,7 @@ export async function getClassSource(svc, input) {
|
|
|
514
623
|
const artifact = svc.getArtifact(artifactId);
|
|
515
624
|
artifactId = artifact.artifactId;
|
|
516
625
|
origin = artifact.origin;
|
|
517
|
-
requestedMapping = input.mapping
|
|
626
|
+
requestedMapping = inheritArtifactMapping(input.mapping, artifact);
|
|
518
627
|
mappingApplied = artifact.mappingApplied ?? requestedMapping;
|
|
519
628
|
provenance = artifact.provenance;
|
|
520
629
|
qualityFlags = artifact.qualityFlags;
|
|
@@ -559,7 +668,11 @@ export async function getClassSource(svc, input) {
|
|
|
559
668
|
requestedMapping,
|
|
560
669
|
mappingApplied,
|
|
561
670
|
provenance: activeProvenance,
|
|
562
|
-
qualityFlags: activeQualityFlags
|
|
671
|
+
qualityFlags: activeQualityFlags,
|
|
672
|
+
// A caller who declined decompilation must not pay for one here: the
|
|
673
|
+
// fallback used to hardcode allowDecompile:true and silently ran a full
|
|
674
|
+
// Vineflower pass on the binary jar.
|
|
675
|
+
allowDecompile: input.allowDecompile
|
|
563
676
|
});
|
|
564
677
|
if (!fallbackResolved || fallbackResolved.artifactId === activeArtifactId) {
|
|
565
678
|
return false;
|
|
@@ -653,11 +766,15 @@ export async function getClassSource(svc, input) {
|
|
|
653
766
|
if (!filePath) {
|
|
654
767
|
throw buildClassSourceNotFoundError(svc, {
|
|
655
768
|
artifactId: activeArtifactId,
|
|
769
|
+
requestedArtifactId: artifactId,
|
|
656
770
|
className,
|
|
657
771
|
lookupClassName: activeLookupClassName,
|
|
658
|
-
|
|
772
|
+
// The REQUESTED artifact's namespace and quality, matching the artifactId this
|
|
773
|
+
// error reports. The nested-jar redirect replaces the active values with the
|
|
774
|
+
// inner jar's, which would otherwise describe an artifact the caller never named.
|
|
775
|
+
mappingApplied,
|
|
659
776
|
requestedMapping,
|
|
660
|
-
qualityFlags
|
|
777
|
+
qualityFlags,
|
|
661
778
|
attemptedBinaryFallback,
|
|
662
779
|
targetKind: input.target?.kind,
|
|
663
780
|
targetValue: input.target && "value" in input.target ? input.target.value : undefined,
|
|
@@ -692,11 +809,15 @@ export async function getClassSource(svc, input) {
|
|
|
692
809
|
if (!row) {
|
|
693
810
|
throw buildClassSourceNotFoundError(svc, {
|
|
694
811
|
artifactId: activeArtifactId,
|
|
812
|
+
requestedArtifactId: artifactId,
|
|
695
813
|
className,
|
|
696
814
|
lookupClassName: activeLookupClassName,
|
|
697
|
-
|
|
815
|
+
// The REQUESTED artifact's namespace and quality, matching the artifactId this
|
|
816
|
+
// error reports. The nested-jar redirect replaces the active values with the
|
|
817
|
+
// inner jar's, which would otherwise describe an artifact the caller never named.
|
|
818
|
+
mappingApplied,
|
|
698
819
|
requestedMapping,
|
|
699
|
-
qualityFlags
|
|
820
|
+
qualityFlags,
|
|
700
821
|
attemptedBinaryFallback,
|
|
701
822
|
filePath,
|
|
702
823
|
targetKind: input.target?.kind,
|
|
@@ -874,6 +995,7 @@ export async function getClassMembers(svc, input) {
|
|
|
874
995
|
const artifact = svc.getArtifact(artifactId);
|
|
875
996
|
artifactId = artifact.artifactId;
|
|
876
997
|
origin = artifact.origin;
|
|
998
|
+
requestedMapping = inheritArtifactMapping(input.mapping, artifact);
|
|
877
999
|
mappingApplied = artifact.mappingApplied ?? requestedMapping;
|
|
878
1000
|
provenance = artifact.provenance;
|
|
879
1001
|
qualityFlags = artifact.qualityFlags;
|
|
@@ -1143,7 +1265,18 @@ export async function getClassMembers(svc, input) {
|
|
|
1143
1265
|
truncated,
|
|
1144
1266
|
...(nextCursor ? { nextCursor } : {}),
|
|
1145
1267
|
...(memberCursorIgnored ? { cursorIgnored: true } : {}),
|
|
1146
|
-
context:
|
|
1268
|
+
context: {
|
|
1269
|
+
...signatureContext,
|
|
1270
|
+
// The jar-derived context describes the BYTECODE the reader opened. The
|
|
1271
|
+
// members handed back have already been remapped into requestedMapping,
|
|
1272
|
+
// so echoing the jar namespace here contradicted `returnedNamespace` in
|
|
1273
|
+
// the same payload. Report what the response actually contains, and fill
|
|
1274
|
+
// the version the resolver established when the jar path yielded none.
|
|
1275
|
+
...(signatureContext.minecraftVersion === "unknown" && version
|
|
1276
|
+
? { minecraftVersion: version }
|
|
1277
|
+
: {}),
|
|
1278
|
+
mappingNamespace: requestedMapping
|
|
1279
|
+
},
|
|
1147
1280
|
origin,
|
|
1148
1281
|
artifactId,
|
|
1149
1282
|
requestedMapping,
|
|
@@ -2,6 +2,13 @@ import type { SourceService } from "../source-service.js";
|
|
|
2
2
|
export interface DidYouMeanCandidate {
|
|
3
3
|
className: string;
|
|
4
4
|
matchReason: string;
|
|
5
|
+
/**
|
|
6
|
+
* Set only when the candidate was found in an artifact OTHER than the one the caller
|
|
7
|
+
* named — today, the internal binary fallback or nested-jar redirect the lookup ended
|
|
8
|
+
* on. An unattributed candidate always means "in the artifact you asked about", so the
|
|
9
|
+
* field marks the exception rather than restating the common case on every entry.
|
|
10
|
+
*/
|
|
11
|
+
artifactId?: string;
|
|
5
12
|
}
|
|
6
13
|
/**
|
|
7
14
|
* Collects ranked near-miss class candidates from the artifact's symbol
|
|
@@ -10,5 +17,9 @@ export interface DidYouMeanCandidate {
|
|
|
10
17
|
* edit-distance suggestions. Candidates are hints, never assertions that the
|
|
11
18
|
* class exists at the suggested location. Returns an empty array when the
|
|
12
19
|
* index has nothing usable.
|
|
20
|
+
*
|
|
21
|
+
* `attributeTo` stamps every produced candidate with that artifact id; pass it when
|
|
22
|
+
* collecting from an artifact the caller did not name, and leave it unset for the
|
|
23
|
+
* requested artifact.
|
|
13
24
|
*/
|
|
14
|
-
export declare function collectDidYouMeanCandidates(svc: SourceService, artifactId: string, className: string): DidYouMeanCandidate[];
|
|
25
|
+
export declare function collectDidYouMeanCandidates(svc: SourceService, artifactId: string, className: string, attributeTo?: string): DidYouMeanCandidate[];
|
|
@@ -15,8 +15,12 @@ function fqnOfRow(row) {
|
|
|
15
15
|
* edit-distance suggestions. Candidates are hints, never assertions that the
|
|
16
16
|
* class exists at the suggested location. Returns an empty array when the
|
|
17
17
|
* index has nothing usable.
|
|
18
|
+
*
|
|
19
|
+
* `attributeTo` stamps every produced candidate with that artifact id; pass it when
|
|
20
|
+
* collecting from an artifact the caller did not name, and leave it unset for the
|
|
21
|
+
* requested artifact.
|
|
18
22
|
*/
|
|
19
|
-
export function collectDidYouMeanCandidates(svc, artifactId, className) {
|
|
23
|
+
export function collectDidYouMeanCandidates(svc, artifactId, className, attributeTo) {
|
|
20
24
|
try {
|
|
21
25
|
const simpleName = className.split(/[.$]/).at(-1) ?? className;
|
|
22
26
|
if (!simpleName) {
|
|
@@ -29,7 +33,7 @@ export function collectDidYouMeanCandidates(svc, artifactId, className) {
|
|
|
29
33
|
return;
|
|
30
34
|
}
|
|
31
35
|
seen.add(fqn);
|
|
32
|
-
out.push({ className: fqn, matchReason });
|
|
36
|
+
out.push({ className: fqn, matchReason, ...(attributeTo ? { artifactId: attributeTo } : {}) });
|
|
33
37
|
};
|
|
34
38
|
const exact = svc.symbolsRepo.findScopedSymbols({
|
|
35
39
|
artifactId,
|
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
import { basename } from "node:path";
|
|
2
|
+
import { buildSuggestedCall } from "../build-suggested-call.js";
|
|
2
3
|
import { ERROR_CODES, createError, isAppError } from "../errors.js";
|
|
3
4
|
import { log } from "../logger.js";
|
|
4
5
|
import { decodeJarEntryUtf8OrThrow, listJarEntries, readJarEntryCapped } from "../source-jar-reader.js";
|
|
5
6
|
import { normalizeOptionalString, normalizePathStyle } from "./shared-utils.js";
|
|
6
|
-
// Read-through delivery for non-indexed jar resources:
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
|
|
7
|
+
// Read-through delivery for non-indexed jar resources: any entry of the
|
|
8
|
+
// backing jar is served directly when the source index has no row for it.
|
|
9
|
+
//
|
|
10
|
+
// Delivery used to be gated on an `assets/`+`data/` path prefix, which made the
|
|
11
|
+
// two entries an agent reaches for FIRST when inspecting a mod jar —
|
|
12
|
+
// `fabric.mod.json` at the archive root and `META-INF/MANIFEST.MF` — permanently
|
|
13
|
+
// unreachable (ERR_FILE_NOT_FOUND) even though `assets/**` read-through proved
|
|
14
|
+
// the mechanism worked. The prefix carries no safety value: the real guards are
|
|
15
|
+
// the traversal-shaped path rejection, the per-file byte cap, and the
|
|
16
|
+
// text/binary classification below, and all three apply to every entry alike.
|
|
11
17
|
const READ_THROUGH_MAX_BYTES = 512 * 1024;
|
|
12
18
|
const READ_THROUGH_TEXT_EXTENSIONS = new Set([
|
|
13
19
|
".json",
|
|
@@ -24,23 +30,104 @@ const READ_THROUGH_TEXT_EXTENSIONS = new Set([
|
|
|
24
30
|
".md",
|
|
25
31
|
".fsh",
|
|
26
32
|
".vsh",
|
|
27
|
-
".glsl"
|
|
33
|
+
".glsl",
|
|
34
|
+
// Loader metadata that lives outside assets/ and data/.
|
|
35
|
+
".mf",
|
|
36
|
+
".accesswidener",
|
|
37
|
+
".xml"
|
|
38
|
+
]);
|
|
39
|
+
/**
|
|
40
|
+
* Extensions known to hold non-text payloads. Listing them keeps the common
|
|
41
|
+
* case a metadata-only probe: no bytes are read at all.
|
|
42
|
+
*/
|
|
43
|
+
const READ_THROUGH_BINARY_EXTENSIONS = new Set([
|
|
44
|
+
".class",
|
|
45
|
+
".png",
|
|
46
|
+
".jpg",
|
|
47
|
+
".jpeg",
|
|
48
|
+
".gif",
|
|
49
|
+
".bmp",
|
|
50
|
+
".webp",
|
|
51
|
+
".ico",
|
|
52
|
+
".jar",
|
|
53
|
+
".zip",
|
|
54
|
+
".gz",
|
|
55
|
+
".tar",
|
|
56
|
+
".ogg",
|
|
57
|
+
".wav",
|
|
58
|
+
".mp3",
|
|
59
|
+
".nbt",
|
|
60
|
+
".dat",
|
|
61
|
+
".bin",
|
|
62
|
+
".so",
|
|
63
|
+
".dll",
|
|
64
|
+
".dylib",
|
|
65
|
+
".ttf",
|
|
66
|
+
".otf",
|
|
67
|
+
".woff",
|
|
68
|
+
".woff2",
|
|
69
|
+
".pack",
|
|
70
|
+
".mca",
|
|
71
|
+
".rsa",
|
|
72
|
+
".dsa"
|
|
28
73
|
]);
|
|
29
74
|
const NEARBY_PATH_HINT_LIMIT = 5;
|
|
75
|
+
/** Ratio of C0 control bytes above which a sniffed entry counts as binary. */
|
|
76
|
+
const SNIFF_MAX_CONTROL_RATIO = 0.05;
|
|
30
77
|
function isTraversalShapedPath(filePath) {
|
|
31
78
|
return (filePath.startsWith("/") ||
|
|
32
79
|
filePath.includes("\u0000") ||
|
|
33
80
|
filePath.split(/[\\/]/).includes(".."));
|
|
34
81
|
}
|
|
35
|
-
function
|
|
36
|
-
return READ_THROUGH_PREFIXES.some((prefix) => filePath.startsWith(prefix));
|
|
37
|
-
}
|
|
38
|
-
function readThroughTextExtension(filePath) {
|
|
82
|
+
function classifyReadThroughEntry(filePath) {
|
|
39
83
|
const dot = filePath.lastIndexOf(".");
|
|
40
|
-
|
|
41
|
-
|
|
84
|
+
const slash = filePath.lastIndexOf("/");
|
|
85
|
+
if (dot < 0 || dot < slash) {
|
|
86
|
+
return "unknown";
|
|
87
|
+
}
|
|
88
|
+
const extension = filePath.slice(dot).toLowerCase();
|
|
89
|
+
if (READ_THROUGH_TEXT_EXTENSIONS.has(extension)) {
|
|
90
|
+
return "text";
|
|
91
|
+
}
|
|
92
|
+
if (READ_THROUGH_BINARY_EXTENSIONS.has(extension)) {
|
|
93
|
+
return "binary";
|
|
94
|
+
}
|
|
95
|
+
return "unknown";
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Decodes a capped buffer when it really is UTF-8 text, else reports binary.
|
|
99
|
+
* A NUL byte, a strict-UTF-8 decode failure, or a high C0 control-byte ratio
|
|
100
|
+
* all mean "do not hand this back as text".
|
|
101
|
+
*/
|
|
102
|
+
function sniffUtf8Text(buffer) {
|
|
103
|
+
if (buffer.length === 0) {
|
|
104
|
+
return "";
|
|
105
|
+
}
|
|
106
|
+
if (buffer.includes(0)) {
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
109
|
+
let decoded;
|
|
110
|
+
try {
|
|
111
|
+
decoded = new TextDecoder("utf-8", { fatal: true }).decode(buffer);
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
let control = 0;
|
|
117
|
+
for (const byte of buffer) {
|
|
118
|
+
if (byte < 0x20 && byte !== 0x09 && byte !== 0x0a && byte !== 0x0d) {
|
|
119
|
+
control += 1;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return control / buffer.length > SNIFF_MAX_CONTROL_RATIO ? undefined : decoded;
|
|
123
|
+
}
|
|
124
|
+
/** Trims a capped buffer back to a UTF-8 character boundary at `limit`. */
|
|
125
|
+
function trimToUtf8Boundary(buffer, limit) {
|
|
126
|
+
let cut = Math.min(limit, buffer.length);
|
|
127
|
+
while (cut > 0 && ((buffer[cut] ?? 0) & 0xc0) === 0x80) {
|
|
128
|
+
cut -= 1;
|
|
42
129
|
}
|
|
43
|
-
return
|
|
130
|
+
return buffer.subarray(0, cut);
|
|
44
131
|
}
|
|
45
132
|
/**
|
|
46
133
|
* Same-basename entries elsewhere in the jar, for not-found hints. Covers
|
|
@@ -52,7 +139,7 @@ async function collectNearbyPaths(binaryJarPath, missingPath) {
|
|
|
52
139
|
const wanted = basename(missingPath);
|
|
53
140
|
const entries = await listJarEntries(binaryJarPath);
|
|
54
141
|
return entries
|
|
55
|
-
.filter((entry) =>
|
|
142
|
+
.filter((entry) => basename(entry) === wanted)
|
|
56
143
|
.slice(0, NEARBY_PATH_HINT_LIMIT);
|
|
57
144
|
}
|
|
58
145
|
catch {
|
|
@@ -95,7 +182,7 @@ export async function getArtifactFile(svc, input) {
|
|
|
95
182
|
const artifact = svc.getArtifact(input.artifactId);
|
|
96
183
|
const normalizedPath = normalizePathStyle(input.filePath);
|
|
97
184
|
const row = svc.filesRepo.getFileContent(artifact.artifactId, normalizedPath);
|
|
98
|
-
if (!row &&
|
|
185
|
+
if (!row && artifact.binaryJarPath) {
|
|
99
186
|
return await readFileThroughJar(svc, {
|
|
100
187
|
artifact,
|
|
101
188
|
binaryJarPath: artifact.binaryJarPath,
|
|
@@ -108,7 +195,15 @@ export async function getArtifactFile(svc, input) {
|
|
|
108
195
|
throw createError({
|
|
109
196
|
code: ERROR_CODES.FILE_NOT_FOUND,
|
|
110
197
|
message: `Source file "${input.filePath}" was not found.`,
|
|
111
|
-
details: {
|
|
198
|
+
details: {
|
|
199
|
+
artifactId: input.artifactId,
|
|
200
|
+
filePath: input.filePath,
|
|
201
|
+
nextAction: "This artifact has no backing binary jar, so only indexed source files are reachable. List what is indexed with list-artifact-files.",
|
|
202
|
+
...buildSuggestedCall({
|
|
203
|
+
tool: "list-artifact-files",
|
|
204
|
+
params: { artifactId: input.artifactId, limit: 50 }
|
|
205
|
+
})
|
|
206
|
+
}
|
|
112
207
|
});
|
|
113
208
|
}
|
|
114
209
|
const maxBytes = clampLimit(input.maxBytes, svc.config.maxContentBytes, Number.MAX_SAFE_INTEGER);
|
|
@@ -145,16 +240,16 @@ export async function getArtifactFile(svc, input) {
|
|
|
145
240
|
}
|
|
146
241
|
async function readFileThroughJar(svc, args) {
|
|
147
242
|
const { artifact, binaryJarPath, filePath, artifactId } = args;
|
|
148
|
-
const
|
|
149
|
-
const cap =
|
|
150
|
-
?
|
|
151
|
-
:
|
|
243
|
+
const kind = classifyReadThroughEntry(filePath);
|
|
244
|
+
const cap = kind === "binary"
|
|
245
|
+
? 0
|
|
246
|
+
: Math.min(clampLimit(args.maxBytes, svc.config.maxContentBytes, Number.MAX_SAFE_INTEGER), READ_THROUGH_MAX_BYTES);
|
|
152
247
|
let capped;
|
|
153
248
|
try {
|
|
154
249
|
// Read at most the cap (+ slack to trim back to a UTF-8 boundary); an
|
|
155
|
-
// oversized entry is never fully materialized in memory.
|
|
156
|
-
// are metadata-only probes (no content read at all).
|
|
157
|
-
capped = await readJarEntryCapped(binaryJarPath, filePath,
|
|
250
|
+
// oversized entry is never fully materialized in memory. Known-binary
|
|
251
|
+
// entries are metadata-only probes (no content read at all).
|
|
252
|
+
capped = await readJarEntryCapped(binaryJarPath, filePath, kind === "binary" ? 0 : cap + 4);
|
|
158
253
|
}
|
|
159
254
|
catch (error) {
|
|
160
255
|
if (!isAppError(error) || error.code !== ERROR_CODES.SOURCE_NOT_FOUND) {
|
|
@@ -190,30 +285,37 @@ async function readFileThroughJar(svc, args) {
|
|
|
190
285
|
}),
|
|
191
286
|
deliveryMode: "jar-read-through"
|
|
192
287
|
};
|
|
193
|
-
|
|
288
|
+
const omitBinary = (reason) => ({
|
|
289
|
+
...base,
|
|
290
|
+
content: "",
|
|
291
|
+
contentBytes: capped.entrySize,
|
|
292
|
+
truncated: false,
|
|
293
|
+
contentOmittedReason: reason
|
|
294
|
+
});
|
|
295
|
+
if (kind === "binary") {
|
|
296
|
+
return omitBinary("Entry is not a known text format; binary content is not delivered. Size and existence are reported instead.");
|
|
297
|
+
}
|
|
298
|
+
const truncated = capped.entrySize > cap;
|
|
299
|
+
// Trim the capped prefix back to a UTF-8 character boundary before decoding;
|
|
300
|
+
// the tail past the cap is dropped by design.
|
|
301
|
+
const trimmed = truncated ? trimToUtf8Boundary(capped.buffer, cap) : capped.buffer;
|
|
302
|
+
if (kind === "unknown") {
|
|
303
|
+
// No extension to go on: decide from the bytes rather than refuse a text
|
|
304
|
+
// entry (META-INF/services/*, LICENSE files) on the strength of its name.
|
|
305
|
+
const sniffed = sniffUtf8Text(trimmed);
|
|
306
|
+
if (sniffed === undefined) {
|
|
307
|
+
return omitBinary("Entry has no known text extension and its bytes are not UTF-8 text; binary content is not delivered. Size and existence are reported instead.");
|
|
308
|
+
}
|
|
194
309
|
return {
|
|
195
310
|
...base,
|
|
196
|
-
content:
|
|
311
|
+
content: sniffed,
|
|
197
312
|
contentBytes: capped.entrySize,
|
|
198
|
-
truncated
|
|
199
|
-
contentOmittedReason: "Entry is not a known text format; binary content is not delivered. Size and existence are reported instead."
|
|
313
|
+
truncated
|
|
200
314
|
};
|
|
201
315
|
}
|
|
202
|
-
const
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
// Trim the capped prefix back to a UTF-8 character boundary before
|
|
206
|
-
// decoding; the tail past the cap is dropped by design.
|
|
207
|
-
const buffer = capped.buffer;
|
|
208
|
-
let cut = Math.min(cap, buffer.length);
|
|
209
|
-
while (cut > 0 && ((buffer[cut] ?? 0) & 0xc0) === 0x80) {
|
|
210
|
-
cut -= 1;
|
|
211
|
-
}
|
|
212
|
-
content = buffer.slice(0, cut).toString("utf8");
|
|
213
|
-
}
|
|
214
|
-
else {
|
|
215
|
-
content = decodeJarEntryUtf8OrThrow(capped.buffer, binaryJarPath, filePath);
|
|
216
|
-
}
|
|
316
|
+
const content = truncated
|
|
317
|
+
? trimmed.toString("utf8")
|
|
318
|
+
: decodeJarEntryUtf8OrThrow(capped.buffer, binaryJarPath, filePath);
|
|
217
319
|
return {
|
|
218
320
|
...base,
|
|
219
321
|
content,
|
|
@@ -234,10 +336,12 @@ export async function listArtifactFiles(svc, input) {
|
|
|
234
336
|
prefix
|
|
235
337
|
});
|
|
236
338
|
const normalizedPrefix = normalizeOptionalString(prefix);
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
339
|
+
// A prefix that matched nothing is the exact moment read-through is worth
|
|
340
|
+
// naming: the index holds Java source only, while EVERY jar entry —
|
|
341
|
+
// archive-root files such as fabric.mod.json and META-INF/** included — is
|
|
342
|
+
// reachable by exact path.
|
|
343
|
+
if (normalizedPrefix && page.items.length === 0 && artifact.binaryJarPath) {
|
|
344
|
+
warnings.push("Indexed artifacts currently include Java source only; non-Java resources are not indexed. Any text entry of the backing jar — including archive-root files such as fabric.mod.json and META-INF/** — is served directly by exact path with get-artifact-file (read-through delivery).");
|
|
241
345
|
}
|
|
242
346
|
return {
|
|
243
347
|
items: page.items,
|
package/dist/source/indexer.js
CHANGED
|
@@ -252,6 +252,7 @@ export async function buildRebuiltArtifactData(svc, resolved) {
|
|
|
252
252
|
vineflowerJarPath: vineflowerPath,
|
|
253
253
|
artifactIdCandidate: resolved.artifactId,
|
|
254
254
|
timeoutMs: 120_000,
|
|
255
|
+
maxMemoryMb: svc.config.decompileMaxMemoryMb,
|
|
255
256
|
signature: resolved.artifactId
|
|
256
257
|
});
|
|
257
258
|
files = decompileResult.javaFiles.map((entry) => ({
|
|
@@ -9,6 +9,25 @@ export interface NestedJarMatch {
|
|
|
9
9
|
entryName: string;
|
|
10
10
|
extractedPath: string;
|
|
11
11
|
}
|
|
12
|
+
export interface NestedClassMatch {
|
|
13
|
+
qualifiedName: string;
|
|
14
|
+
filePath: string;
|
|
15
|
+
line: number;
|
|
16
|
+
symbolKind: "class";
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Finds exact class-name matches across a shell's nested bytecode inventory.
|
|
20
|
+
* Results use the same binary-backed class representation as entry-tool search:
|
|
21
|
+
* an inferred outer Java path, line 1, and the broad class symbol kind.
|
|
22
|
+
*/
|
|
23
|
+
export declare function findNestedJarClasses(args: {
|
|
24
|
+
cacheDir: string;
|
|
25
|
+
outerJarPath: string;
|
|
26
|
+
outerSignature: string;
|
|
27
|
+
inventory: string[];
|
|
28
|
+
className: string;
|
|
29
|
+
limit: number;
|
|
30
|
+
}): Promise<NestedClassMatch[]>;
|
|
12
31
|
/**
|
|
13
32
|
* Shell detection with both required signals: near-zero own classes AND
|
|
14
33
|
* bundled nested jars (META-INF/jars scan or fabric.mod.json "jars"
|