@adhisang/minecraft-modding-mcp 5.0.0 → 6.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.
Files changed (72) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/dist/access-widener-parser.d.ts +1 -0
  3. package/dist/access-widener-parser.js +6 -3
  4. package/dist/cache-registry.js +35 -31
  5. package/dist/decompiler/vineflower.js +8 -2
  6. package/dist/entry-tools/analyze-mod-service.js +2 -1
  7. package/dist/entry-tools/analyze-symbol-service.js +18 -9
  8. package/dist/entry-tools/compare-minecraft-service.js +1 -1
  9. package/dist/entry-tools/inspect-minecraft/handlers/class-members.js +2 -1
  10. package/dist/entry-tools/inspect-minecraft/handlers/file.js +26 -1
  11. package/dist/entry-tools/inspect-minecraft/handlers/versions.js +2 -1
  12. package/dist/entry-tools/inspect-minecraft/internal.js +2 -0
  13. package/dist/entry-tools/response-contract.d.ts +2 -0
  14. package/dist/entry-tools/response-contract.js +2 -2
  15. package/dist/entry-tools/validate-project-service.d.ts +4 -4
  16. package/dist/entry-tools/verify-mixin-target-service.js +7 -7
  17. package/dist/index.js +20 -16
  18. package/dist/java-process.js +1 -1
  19. package/dist/json-rpc-framing.d.ts +4 -0
  20. package/dist/json-rpc-framing.js +23 -1
  21. package/dist/mapping/internal-types.d.ts +17 -0
  22. package/dist/mapping/loaders/tiny-maven.js +46 -20
  23. package/dist/mapping/lookup.d.ts +12 -1
  24. package/dist/mapping/lookup.js +53 -1
  25. package/dist/mapping-service.js +36 -21
  26. package/dist/mixin/annotation-validators.js +21 -3
  27. package/dist/mixin/parsed-validator.js +1 -0
  28. package/dist/mixin-parser.js +71 -3
  29. package/dist/mod-decompile-service.d.ts +2 -0
  30. package/dist/mod-decompile-service.js +19 -2
  31. package/dist/mod-remap-service.js +6 -5
  32. package/dist/mojang-tiny-mapping-service.js +5 -2
  33. package/dist/nbt/java-nbt-codec.js +40 -16
  34. package/dist/nbt/json-patch.js +51 -12
  35. package/dist/nbt/typed-json.d.ts +1 -0
  36. package/dist/nbt/typed-json.js +12 -2
  37. package/dist/nbt/types.d.ts +2 -2
  38. package/dist/path-converter.js +10 -3
  39. package/dist/registry-service.d.ts +2 -0
  40. package/dist/registry-service.js +16 -1
  41. package/dist/repo-downloader.js +4 -3
  42. package/dist/source/artifact-resolver.js +1 -1
  43. package/dist/source/class-source/snippet-builder.js +6 -0
  44. package/dist/source/class-source-helpers.d.ts +4 -4
  45. package/dist/source/class-source-helpers.js +12 -11
  46. package/dist/source/class-source.d.ts +19 -0
  47. package/dist/source/class-source.js +48 -21
  48. package/dist/source/file-access.js +3 -2
  49. package/dist/source/indexer.js +24 -7
  50. package/dist/source/lifecycle/mapping-helpers.js +28 -3
  51. package/dist/source/lifecycle/runtime-check.js +20 -6
  52. package/dist/source/search.js +23 -8
  53. package/dist/source/validate-mixin/pipeline/resolve.js +23 -3
  54. package/dist/source/workspace-target.js +2 -1
  55. package/dist/source-resolver.js +2 -2
  56. package/dist/source-service.js +9 -1
  57. package/dist/stdio-supervisor.d.ts +2 -2
  58. package/dist/stdio-supervisor.js +29 -9
  59. package/dist/storage/db.js +24 -1
  60. package/dist/storage/files-repo.d.ts +3 -0
  61. package/dist/storage/files-repo.js +43 -37
  62. package/dist/storage/symbols-repo.d.ts +9 -0
  63. package/dist/storage/symbols-repo.js +47 -19
  64. package/dist/symbols/symbol-extractor.js +1 -1
  65. package/dist/tool-guidance.d.ts +2 -1
  66. package/dist/tool-guidance.js +26 -1
  67. package/dist/tool-schemas.d.ts +4 -4
  68. package/dist/tool-schemas.js +91 -91
  69. package/dist/warning-details.d.ts +12 -0
  70. package/dist/warning-details.js +17 -0
  71. package/dist/workspace-mapping-service.js +9 -0
  72. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -7,6 +7,42 @@ and this project aims to follow [Semantic Versioning](https://semver.org/spec/v2
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [6.0.0] - 2026-06-25
11
+
12
+ ### Changed
13
+
14
+ - **Breaking:** tool responses omit `meta.warnings` entirely when there are no warnings (previously always emitted as `[]`), and omit `meta.detailApplied` when it equals the tool's default detail level (entry tools: `summary`; expert/batch tools: their per-tool default). Clients must treat a missing `meta.warnings` as "no warnings" and a missing `meta.detailApplied` as "the default detail was applied". `meta.includeApplied` semantics are unchanged (already omitted when empty).
15
+ - `tools/list` payload slimmed by ~12% (93.2 KB → 82.2 KB, roughly 2,700 fewer tokens held in an agent's context): parameter descriptions that merely echoed enum values (e.g. `"obfuscated | mojang | intermediary | yarn"`) are removed since the JSON Schema `enum` already carries them, the shared `detail`/`include`/`target`/`scope`/`gradleUserHome` descriptions are tightened, the expert-tool steering note is shortened, and the longest tool descriptions (`resolve-method-mapping-exact`, `get-class-source`, `get-class-members`, the `batch-*` family) no longer repeat information already present in their input schemas. Wording-only — no input shapes, defaults, or validation rules changed.
16
+ - Per-member remap-failure warnings are aggregated into a single line per namespace direction (e.g. `Could not remap 12 methods from obfuscated to mojang (foo, bar, baz, +9 more).`) instead of one line per member, and the structured `meta.warningDetails[]` companion is capped to a representative set (5 entries) at `detail: "summary"` (the full text still lives in `meta.warnings[]`, so each kept entry's `index` still dereferences it). On unobfuscated versions a single core class could previously emit hundreds of near-identical remap-warning lines — the dominant token sink for those responses.
17
+
18
+ ### Fixed
19
+
20
+ - `validate-mixin` / mixin parsing: `@Mixin(My$GeneratedClass.class)` class-literal targets containing `$` (a legal Java identifier character) are now captured as the full class name; previously the target regex started the capture mid-name and produced a wrong target.
21
+ - **Unobfuscated Minecraft versions (26.1+):** `get-class-members` / `get-class-source` no longer mislabel the already-deobfuscated runtime namespace as `obfuscated` for a `mapping: "mojang"` request when reusing a previously-resolved artifact. The mislabel made `requestedMapping ("mojang") !== mappingApplied ("obfuscated")`, which triggered a doomed mojang↔obfuscated remap that dropped every member (forcing a spurious `decompiledFallback`), disabled `memberPattern`, and flooded the response with per-member remap warnings. The namespace is now reconciled to `mojang` (identity), so members come back from bytecode with correct names, `memberPattern` applies, and the warning flood disappears.
22
+ - **`batch-symbol-exists` / `check-symbol-exists` on unobfuscated versions:** method existence checks returned `not_found` for methods that genuinely exist, because the empty mapping graph reported `not_found` and skipped the runtime-bytecode fallback. The fallback now runs for `not_found` method queries; name-only lookups resolve when any overload matches (no longer reported as `ambiguous` for an existence check), and inherited methods are included (`level.getGameTime()` resolves on `Level`).
23
+ - `check-symbol-exists`: a class genuinely absent from the runtime jar now yields a clear `… was not found in the Minecraft <version> runtime jar` message instead of a generic load-failure note.
24
+ - `resolve-artifact`: a decompilation failure (`ERR_DECOMPILER_FAILED`) no longer advertises an `artifactId` for an artifact that was never persisted — the leaked id previously caused a follow-up `find-class` / `get-class-*` to fail with "Artifact not found. Resolve context first."
25
+ - Workspace dependency version detection falls back to the umbrella package's version property (e.g. `fabricApiVersion` / `fabric_api_version`) for submodules that don't declare their own (e.g. `net.fabricmc.fabric-api:fabric-screen-handler-api-v1`), instead of failing with `ERR_DEPENDENCY_VERSION_UNRESOLVED`. The submodule's own property still takes precedence when present.
26
+ - `get-class-source` now honors an explicit `mapping` argument instead of forcing the artifact's stored namespace (matching `get-class-members`), so a `className` supplied in a declared namespace no longer fails with `CLASS_NOT_FOUND`. Several recovery `suggestedCall`s are corrected: the snippet continuation no longer drops the default `maxLines` (which produced an unbounded follow-up read), the `decompiledFallback` truncation flag is reported accurately, and a hardcoded `"latest"` version that `resolve-artifact` rejects is replaced with the concrete resolved version. A method query with an omitted `signatureMode` is treated as a non-exact match in the runtime check (descriptorless methods are no longer guaranteed `not_found`), binary jars are remapped from the source jar (not the absent output) after an index loss, the `globToSqlLike` `**` push-down no longer drops direct children, the vanilla-jar fallback no longer retains the named-signature mapping, and the workspace-context write-back is serialized so it stops racing dependency-target synthesis.
27
+ - `resolve-method-mapping-exact` mirrors `find-mapping`'s obfuscated-namespace acceptance set, so a single-hop `intermediary → yarn` lookup whose candidate carries an obfuscated-namespace descriptor no longer returns a false `not_found`. Loading yarn Tiny pairs from Maven no longer unions three builds (or fetches unusable v1 jars), removing the spurious `ambiguous` results that produced. Tiny caches (`extractTinyFromJar` / `resolveMojangTinyFile`) are now written atomically (temp file + `rename`) so a crash mid-write cannot leave a truncated `.tiny` behind a permanent `existsSync` fast path, and `get-class-api-matrix` keys its cursor context on `gradleUserHome` so a cursor minted against one dependency graph cannot replay against another.
28
+ - `validate-mixin` / mixin parsing no longer matches the word `class` inside comments or javadoc (which yielded a wrong `className`) and no longer parses annotations inside block comments (which produced phantom `@Shadow` / `@Accessor` members and false definite errors); both passes now skip comment regions. The injection-point wildcard quantifier is anchored so an exact-match target no longer triggers a false positive.
29
+ - `search-class-source` / file search no longer re-emits `both`-tier hits as duplicate `content`-tier hits on later pages (they are deduped across the cursor), escapes the `LIKE` needle in path search, push-down, and count queries so the declared `ESCAPE` clause actually applies, and stops returning a `nextCursor` on the final partial page (which previously forced an extra empty-page fetch).
30
+ - `inspect-minecraft`: `task: "versions"` no longer drops the entire `versions` block at the default detail level, and `task: "file"` content is reachable now that `file` is a valid `include` group. `task: "api-overview"` honors `maxRows` and flags truncation instead of silently capping the matrix at 25 rows while reporting `rowsTruncated: false`. The `file` / `search` / `list-files` tasks return the friendly "blocked" summary (carrying the version-inference warning) instead of forwarding an empty `artifactId`, `task: "workspace"` no longer passes the literal `"unknown"` as the Minecraft version, the `get-class-members` truncation `nextAction` raises the member limit so the suggested retry no longer reproduces the same cut, and the `remap-mod-jar` preview `nextAction` preserves an explicit `outputJar`.
31
+ - stdio supervisor: a supervisor-replayed `initialize` no longer emits a stray error response when the worker crashes again after the client is already initialized; a worker spawn error that previously stalled the supervisor permanently now recovers; and a stale worker exit no longer detaches and orphans the replacement worker. The Java subprocess runner drains trailing stdout/stderr before resolving on child exit, so the final output is no longer lost.
32
+ - NBT JSON Patch: an `add` / `replace` op with key `__proto__` is assigned as an own enumerable property instead of silently mutating the prototype and dropping the member. Non-finite floats round-trip correctly — decode emits a sentinel string rather than a raw number, and the typed-JSON layer no longer rejects otherwise-valid NBT containing `NaN` / `Infinity`.
33
+ - Data generation no longer deadlocks once the child's stdout pipe buffer fills (the output is now drained), the download timeout now covers the response body stream instead of being cleared after headers (removing an indefinite hang), and concurrent identical `remap-mod-jar` calls no longer race on a non-atomic cache copy.
34
+ - Access Widener parsing accepts valid `transitive-` kinds — the prefix is stripped, the base kind validated, and transitivity recorded on the entry — instead of rejecting them as unknown. Path conversion recognizes the `wsl.localhost` UNC form (not only `wsl$`) and rejects a UNC WSL path that targets a different distro than the server runs in, instead of silently mapping it.
35
+ - `validate-mixin` recovery suggestions no longer read `input.path` before checking the mode (which turned a project-mode call into an executable path-mode `suggestedCall`), and the access-transformer suggestion no longer hardcodes the access-widener subject kind. The `batch-class-source` `outputFile` dedupe guard canonicalizes paths unconditionally so absolute-path aliases (`/a/../b`, `//a`) collide as intended and cannot race `writeFile` on the same inode.
36
+
37
+ ### Performance
38
+
39
+ - SQLite repositories cache prepared statements for dynamically built queries (cursor-paginated symbol/file lookups, scoped symbol search, count queries, `IN (...)`-list lookups) in a small per-repo LRU instead of re-preparing the SQL on every call.
40
+ - Symbol search with regex/glob/package filters now streams rows from SQLite instead of materializing every symbol of the artifact in memory first; only matching rows are retained.
41
+ - Cache-registry operations no longer read and JSON-parse every registry file from disk on each call; the parsed registry state is cached and invalidated on write.
42
+ - `get-class-members` / `get-class-source` no longer re-strip the whole decompiled file once per nested type when extracting members (`extractDecompiledMembers` was quadratic in the nested-type count).
43
+ - NBT patch validation revalidates only the touched subtree per patch op instead of the whole document (previously quadratic), and the array encoders reuse a single buffer cursor instead of allocating a `Buffer` per scalar write.
44
+ - `check-symbol-exists` indexes its lookups instead of scanning the full target namespace on every call.
45
+
10
46
  ## [5.0.0] - 2026-06-04
11
47
 
12
48
  ### Changed
@@ -9,6 +9,7 @@
9
9
  export type AccessWidenerEntry = {
10
10
  line: number;
11
11
  kind: "accessible" | "extendable" | "mutable";
12
+ transitive?: boolean;
12
13
  targetKind: "class" | "method" | "field";
13
14
  target: string;
14
15
  owner?: string;
@@ -40,7 +40,9 @@ export function parseAccessWidener(content) {
40
40
  }
41
41
  const kind = parts[0];
42
42
  const targetKind = parts[1];
43
- if (!VALID_KINDS.has(kind)) {
43
+ const transitive = kind.startsWith("transitive-");
44
+ const baseKind = transitive ? kind.slice("transitive-".length) : kind;
45
+ if (!VALID_KINDS.has(baseKind)) {
44
46
  parseWarnings.push(`Line ${lineNum}: Unknown access kind "${kind}".`);
45
47
  continue;
46
48
  }
@@ -48,16 +50,17 @@ export function parseAccessWidener(content) {
48
50
  parseWarnings.push(`Line ${lineNum}: Unknown target kind "${targetKind}".`);
49
51
  continue;
50
52
  }
51
- const validKind = kind;
53
+ const validKind = baseKind;
52
54
  const validTargetKind = targetKind;
53
55
  if (validTargetKind === "class") {
54
- entries.push({ line: lineNum, kind: validKind, targetKind: validTargetKind, target: parts[2] });
56
+ entries.push({ line: lineNum, kind: validKind, transitive, targetKind: validTargetKind, target: parts[2] });
55
57
  }
56
58
  else if (parts.length >= 5) {
57
59
  // method/field: <kind> <targetKind> <owner> <name> <descriptor>
58
60
  entries.push({
59
61
  line: lineNum,
60
62
  kind: validKind,
63
+ transitive,
61
64
  targetKind: validTargetKind,
62
65
  target: parts[2],
63
66
  owner: parts[2],
@@ -1,6 +1,7 @@
1
- import { existsSync, readFileSync } from "node:fs";
2
- import { readdir, rm, stat } from "node:fs/promises";
1
+ import { existsSync } from "node:fs";
2
+ import { readFile, readdir, rm, stat } from "node:fs/promises";
3
3
  import { join, resolve } from "node:path";
4
+ import { mapWithConcurrencyLimit } from "./concurrency.js";
4
5
  import { createError, ERROR_CODES } from "./errors.js";
5
6
  import { normalizeOptionalPathForHost } from "./path-converter.js";
6
7
  import Database from "./storage/sqlite.js";
@@ -25,6 +26,7 @@ export const CACHE_HEALTH_STATES = [
25
26
  ];
26
27
  const STALE_ENTRY_AGE_MS = 30 * 24 * 60 * 60 * 1000;
27
28
  const CURSOR_VERSION = 1;
29
+ const CACHE_STAT_CONCURRENCY = 8;
28
30
  const STATUS_PRIORITY = ["in_use", "corrupt", "orphaned", "stale", "partial", "healthy"];
29
31
  function kindRoot(config, cacheKind) {
30
32
  switch (cacheKind) {
@@ -165,12 +167,12 @@ function inferProjectPath(pathValue, runtimeInfo) {
165
167
  }
166
168
  return undefined;
167
169
  }
168
- function isCorruptRegistryJson(filePath) {
170
+ async function isCorruptRegistryJson(filePath) {
169
171
  if (!filePath.endsWith(".json")) {
170
172
  return false;
171
173
  }
172
174
  try {
173
- JSON.parse(readFileSync(filePath, "utf8"));
175
+ JSON.parse(await readFile(filePath, "utf8"));
174
176
  return false;
175
177
  }
176
178
  catch {
@@ -242,7 +244,7 @@ function candidatePathsForEntry(entry) {
242
244
  function entryUpdatedAt(entry) {
243
245
  return typeof entry.meta?.updatedAt === "string" ? entry.meta.updatedAt : undefined;
244
246
  }
245
- function deriveEntryStatus(entry, config, now) {
247
+ function deriveEntryStatus(entry, config, now, entryPathExists = false) {
246
248
  const maybeMeta = entry.meta ?? {};
247
249
  if (maybeMeta.inUse === true) {
248
250
  return "in_use";
@@ -251,7 +253,7 @@ function deriveEntryStatus(entry, config, now) {
251
253
  return "corrupt";
252
254
  }
253
255
  const candidatePaths = candidatePathsForEntry(entry);
254
- const existingPaths = candidatePaths.filter((candidate) => existsSync(candidate));
256
+ const existingPaths = candidatePaths.filter((candidate) => (entryPathExists && candidate === entry.path) || existsSync(candidate));
255
257
  if (entry.cacheKind === "artifact-index" && !existsSync(config.sqlitePath)) {
256
258
  return "orphaned";
257
259
  }
@@ -275,10 +277,9 @@ function deriveEntryStatus(entry, config, now) {
275
277
  }
276
278
  function sortEntries(entries) {
277
279
  return [...entries].sort((left, right) => {
278
- if (left.cacheKind !== right.cacheKind) {
279
- return left.cacheKind.localeCompare(right.cacheKind);
280
- }
281
- return left.entryId.localeCompare(right.entryId);
280
+ const leftKey = entrySortKey(left);
281
+ const rightKey = entrySortKey(right);
282
+ return leftKey < rightKey ? -1 : leftKey > rightKey ? 1 : 0;
282
283
  });
283
284
  }
284
285
  function entrySortKey(entry) {
@@ -367,17 +368,19 @@ function matchesSelector(entry, selector, runtimeInfo) {
367
368
  return false;
368
369
  }
369
370
  }
370
- const normalizedPaths = candidatePathsForEntry(entry)
371
- .map((candidate) => normalizePathKey(candidate, runtimeInfo))
372
- .filter((candidate) => Boolean(candidate));
373
- if (selector.normalizedJarPath && !normalizedPaths.includes(selector.normalizedJarPath)) {
374
- return false;
375
- }
376
- if (selector.normalizedProjectPath) {
377
- const projectMatch = normalizedPaths.some((candidate) => candidate === selector.normalizedProjectPath || candidate.startsWith(`${selector.normalizedProjectPath}/`));
378
- if (!projectMatch) {
371
+ if (selector.normalizedJarPath || selector.normalizedProjectPath) {
372
+ const normalizedPaths = candidatePathsForEntry(entry)
373
+ .map((candidate) => normalizePathKey(candidate, runtimeInfo))
374
+ .filter((candidate) => Boolean(candidate));
375
+ if (selector.normalizedJarPath && !normalizedPaths.includes(selector.normalizedJarPath)) {
379
376
  return false;
380
377
  }
378
+ if (selector.normalizedProjectPath) {
379
+ const projectMatch = normalizedPaths.some((candidate) => candidate === selector.normalizedProjectPath || candidate.startsWith(`${selector.normalizedProjectPath}/`));
380
+ if (!projectMatch) {
381
+ return false;
382
+ }
383
+ }
381
384
  }
382
385
  return true;
383
386
  }
@@ -455,18 +458,17 @@ function workspaceCacheEntries(workspaceCache) {
455
458
  }
456
459
  }));
457
460
  }
458
- async function fileBackedEntries(config, cacheKind) {
461
+ async function fileBackedEntries(config, cacheKind, detectCorruption) {
459
462
  if (cacheKind === "binary-remap") {
460
463
  return binaryRemapEntries(config);
461
464
  }
462
465
  const root = kindRoot(config, cacheKind);
463
466
  const files = await listFilesRecursive(root);
464
- const entries = [];
465
- for (const filePath of files) {
467
+ return mapWithConcurrencyLimit(files, CACHE_STAT_CONCURRENCY, async (filePath) => {
466
468
  const fileStat = await stat(filePath);
467
469
  const normalizedEntryId = filePath.slice(root.length + 1);
468
470
  const inferredScope = inferScope(filePath, normalizedEntryId) ?? (cacheKind === "decompiled-source" ? "vanilla" : undefined);
469
- entries.push({
471
+ return {
470
472
  cacheKind,
471
473
  entryId: normalizedEntryId,
472
474
  path: filePath,
@@ -479,7 +481,7 @@ async function fileBackedEntries(config, cacheKind) {
479
481
  scope: inferredScope,
480
482
  projectPath: inferProjectPath(filePath, config.pathRuntimeInfo),
481
483
  partial: fileStat.size === 0,
482
- corrupt: cacheKind === "registry" ? isCorruptRegistryJson(filePath) : false,
484
+ corrupt: cacheKind === "registry" && detectCorruption ? await isCorruptRegistryJson(filePath) : false,
483
485
  inUse: filePath.endsWith(".lock") ||
484
486
  filePath.endsWith(".wal") ||
485
487
  filePath.endsWith(".journal"),
@@ -487,9 +489,8 @@ async function fileBackedEntries(config, cacheKind) {
487
489
  ? { jarPath: filePath }
488
490
  : {})
489
491
  }
490
- });
491
- }
492
- return entries;
492
+ };
493
+ });
493
494
  }
494
495
  /**
495
496
  * Binary-remap cache entries are keyed by the final artifact id even when the
@@ -570,9 +571,10 @@ async function directoryFileSizeBytes(root) {
570
571
  }
571
572
  export function createCacheRegistry(config) {
572
573
  const workspaceCache = config.workspaceContextCache ?? getProcessWorkspaceContextCache();
573
- async function collectEntries(cacheKinds, selector) {
574
+ async function collectEntries(cacheKinds, selector, detectCorruption = false) {
574
575
  const selectedKinds = cacheKinds?.length ? cacheKinds : [...PUBLIC_CACHE_KINDS];
575
576
  const preparedSelector = prepareSelector(selector, config.pathRuntimeInfo);
577
+ const detectCorruptionForKinds = detectCorruption || selector?.status === "corrupt";
576
578
  const now = Date.now();
577
579
  const entries = await Promise.all(selectedKinds.map((cacheKind) => {
578
580
  if (cacheKind === "artifact-index") {
@@ -581,13 +583,15 @@ export function createCacheRegistry(config) {
581
583
  if (cacheKind === "workspace") {
582
584
  return Promise.resolve(workspaceCacheEntries(workspaceCache));
583
585
  }
584
- return fileBackedEntries(config, cacheKind);
586
+ return fileBackedEntries(config, cacheKind, detectCorruptionForKinds);
585
587
  }));
586
588
  const enriched = entries
587
589
  .flat()
588
590
  .map((entry) => ({
589
591
  ...entry,
590
- status: entry.cacheKind === "workspace" ? entry.status : deriveEntryStatus(entry, config, now)
592
+ status: entry.cacheKind === "workspace"
593
+ ? entry.status
594
+ : deriveEntryStatus(entry, config, now, entry.cacheKind !== "artifact-index")
591
595
  }));
592
596
  return sortEntries(enriched.filter((entry) => matchesSelector(entry, preparedSelector, config.pathRuntimeInfo)));
593
597
  }
@@ -628,7 +632,7 @@ export function createCacheRegistry(config) {
628
632
  return entries.slice(0, limit);
629
633
  },
630
634
  async verifyEntries(input) {
631
- const entries = await collectEntries(input.cacheKinds, input.selector);
635
+ const entries = await collectEntries(input.cacheKinds, input.selector, true);
632
636
  const unhealthy = entries.filter((entry) => entry.status !== "healthy");
633
637
  const warningStatuses = [...new Set(unhealthy.map((entry) => entry.status))];
634
638
  return {
@@ -1,4 +1,4 @@
1
- import { access, constants, mkdir, readFile, readdir, stat } from "node:fs/promises";
1
+ import { access, constants, mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
2
2
  import { mkdirSync, rmSync } from "node:fs";
3
3
  import { createHash } from "node:crypto";
4
4
  import { basename, join, relative, sep } from "node:path";
@@ -8,6 +8,7 @@ import { assertJavaAvailable, runJavaProcess } from "../java-process.js";
8
8
  import { log } from "../logger.js";
9
9
  const DEFAULT_TIMEOUT_MS = 120_000;
10
10
  const DECOMPILED_JAVA_READ_CONCURRENCY = 8;
11
+ const DECOMPILE_COMPLETE_MARKER = ".decompile-complete";
11
12
  const VINEFLOWER_FLAG_PROFILES = [
12
13
  { label: "default", flags: ["-din=1", "-rbr=1", "-dgs=1"] },
13
14
  { label: "relaxed", flags: ["-din=1", "-rbr=0", "-dgs=0"] },
@@ -132,7 +133,10 @@ export async function decompileBinaryJar(binaryJarPath, cacheDir, options) {
132
133
  try {
133
134
  await mkdir(outputDir, { recursive: true });
134
135
  const outputDirStats = await stat(outputDir).catch(() => undefined);
135
- if (outputDirStats) {
136
+ const markerPresent = outputDirStats
137
+ ? await access(join(outputDir, DECOMPILE_COMPLETE_MARKER), constants.F_OK).then(() => true, () => false)
138
+ : false;
139
+ if (outputDirStats && markerPresent) {
136
140
  const existingJavaFiles = await collectJavaFiles(outputDir);
137
141
  if (existingJavaFiles.length > 0) {
138
142
  const results = await mapWithConcurrencyLimit(existingJavaFiles, DECOMPILED_JAVA_READ_CONCURRENCY, async (candidate) => {
@@ -153,6 +157,7 @@ export async function decompileBinaryJar(binaryJarPath, cacheDir, options) {
153
157
  };
154
158
  }
155
159
  }
160
+ clearOutputDir(outputDir);
156
161
  await assertVineflowerAvailable(options.vineflowerJarPath);
157
162
  await assertJavaAvailable();
158
163
  const profilesAttempted = [];
@@ -189,6 +194,7 @@ export async function decompileBinaryJar(binaryJarPath, cacheDir, options) {
189
194
  content: await readFileTreeText(abs)
190
195
  };
191
196
  });
197
+ await writeFile(join(outputDir, DECOMPILE_COMPLETE_MARKER), profile.label, "utf8");
192
198
  emitDecompileLog("decompile.done", {
193
199
  durationMs: Date.now() - startedAt,
194
200
  artifactIdCandidate: options.artifactIdCandidate,
@@ -248,7 +248,8 @@ export class AnalyzeModService {
248
248
  jarPath: input.subject.jarPath
249
249
  },
250
250
  executionMode: "apply",
251
- targetMapping: input.targetMapping
251
+ targetMapping: input.targetMapping,
252
+ ...(input.outputJar !== undefined ? { outputJar } : {})
252
253
  })
253
254
  ]
254
255
  },
@@ -72,7 +72,7 @@ export const analyzeSymbolShape = {
72
72
  }),
73
73
  version: nonEmptyString
74
74
  .optional()
75
- .describe("Point-in-time MC version for task=exists/map/exact-map/api-overview. For task=lifecycle it is accepted as a back-compat alias for toVersion (range end)."),
75
+ .describe("Point-in-time MC version for task=exists/map/exact-map/workspace/api-overview. For task=lifecycle it is accepted as a back-compat alias for toVersion (range end)."),
76
76
  fromVersion: nonEmptyString
77
77
  .optional()
78
78
  .describe("task=lifecycle only: range start (oldest) version; defaults to the oldest version in the manifest."),
@@ -112,11 +112,11 @@ const LIFECYCLE_ONLY_FIELDS = [
112
112
  "includeSnapshots"
113
113
  ];
114
114
  export const analyzeSymbolSchema = z.object(analyzeSymbolShape).superRefine((value, ctx) => {
115
- if (value.task !== "workspace" && value.task !== "lifecycle" && !value.version) {
115
+ if (value.task !== "lifecycle" && !value.version) {
116
116
  ctx.addIssue({
117
117
  code: z.ZodIssueCode.custom,
118
118
  path: ["version"],
119
- message: "version is required for non-workspace tasks."
119
+ message: "version is required for non-lifecycle tasks."
120
120
  });
121
121
  }
122
122
  if (value.task === "lifecycle" && !value.version && !value.toVersion) {
@@ -379,7 +379,7 @@ export class AnalyzeSymbolService {
379
379
  case "workspace": {
380
380
  const output = await this.deps.resolveWorkspaceSymbol({
381
381
  projectPath: input.projectPath,
382
- version: input.version ?? "unknown",
382
+ version: input.version,
383
383
  kind: subjectKind,
384
384
  name: input.subject.name,
385
385
  owner: input.subject.owner,
@@ -405,7 +405,7 @@ export class AnalyzeSymbolService {
405
405
  owner: input.subject.owner,
406
406
  descriptor: input.subject.descriptor,
407
407
  projectPath: input.projectPath,
408
- version: input.version ?? "unknown",
408
+ version: input.version,
409
409
  sourceMapping: input.sourceMapping ?? "obfuscated"
410
410
  }),
411
411
  counts: {
@@ -433,6 +433,11 @@ export class AnalyzeSymbolService {
433
433
  ...(input.gradleUserHome !== undefined ? { gradleUserHome: input.gradleUserHome } : {}),
434
434
  maxRows: input.maxRows
435
435
  });
436
+ const includeMatrix = include.includes("matrix") || detail !== "summary";
437
+ const matrixRowCap = input.maxRows ?? 25;
438
+ const matrixCapWarning = includeMatrix && output.rows.length > matrixRowCap
439
+ ? `Matrix rows were truncated to ${matrixRowCap} of ${output.rowCount}. Raise maxRows to include more rows.`
440
+ : undefined;
436
441
  return {
437
442
  ...buildEntryToolResult({
438
443
  task: "api-overview",
@@ -458,16 +463,20 @@ export class AnalyzeSymbolService {
458
463
  className: output.className,
459
464
  classIdentity: output.classIdentity
460
465
  },
461
- matrix: include.includes("matrix") || detail !== "summary"
466
+ matrix: includeMatrix
462
467
  ? {
463
468
  rowCount: output.rowCount,
464
- rowsTruncated: output.rowsTruncated,
465
- rows: output.rows.slice(0, 25)
469
+ rowsTruncated: output.rowsTruncated || output.rows.length > matrixRowCap,
470
+ rows: output.rows.slice(0, matrixRowCap)
466
471
  }
467
472
  : undefined
468
473
  }
469
474
  }),
470
- warnings: inferenceWarning ? [inferenceWarning, ...output.warnings] : output.warnings
475
+ warnings: [
476
+ ...(inferenceWarning ? [inferenceWarning] : []),
477
+ ...output.warnings,
478
+ ...(matrixCapWarning ? [matrixCapWarning] : [])
479
+ ]
471
480
  };
472
481
  }
473
482
  }
@@ -64,7 +64,7 @@ export class CompareMinecraftService {
64
64
  : {
65
65
  fromVersion: input.subject.fromVersion,
66
66
  toVersion: input.subject.toVersion,
67
- packageFilter: input.subject.kind === "class" ? undefined : input.subject.registry
67
+ packageFilter: undefined
68
68
  };
69
69
  const output = await this.deps.compareVersions({
70
70
  fromVersion: subject.fromVersion,
@@ -63,12 +63,13 @@ export async function handleClassMembers(deps, subject, detail, include, limit)
63
63
  include,
64
64
  warnings: [...artifact.warnings, ...members.warnings],
65
65
  truncated: createTruncationMeta({
66
- omittedGroups: ["members"],
66
+ omittedGroups: detail === "summary" ? ["members"] : [],
67
67
  nextActions: [
68
68
  createNextAction("inspect-minecraft", {
69
69
  task: "class-members",
70
70
  detail: "full",
71
71
  include: ["members"],
72
+ limit: Math.min(members.counts.total, 5000),
72
73
  subject
73
74
  })
74
75
  ]
@@ -8,6 +8,31 @@ export async function handleFile(deps, subject, detail, include) {
8
8
  const artifact = subject.kind === "file"
9
9
  ? await resolveArtifactReference(deps, subject, "file")
10
10
  : await resolveWorkspaceArtifactReference(deps, subject, fileSubject.artifact);
11
+ if (!artifact.artifactId) {
12
+ const summary = {
13
+ status: "blocked",
14
+ headline: `Could not resolve artifact context for ${fileSubject.filePath}.`,
15
+ subject: createSummarySubject({
16
+ task: "file",
17
+ filePath: fileSubject.filePath
18
+ })
19
+ };
20
+ return {
21
+ ...buildEntryToolResult({
22
+ task: "file",
23
+ summary,
24
+ detail,
25
+ include,
26
+ blocks: {
27
+ subject: {
28
+ requested: subject
29
+ }
30
+ },
31
+ alwaysBlocks: ["subject"]
32
+ }),
33
+ warnings: artifact.warnings
34
+ };
35
+ }
11
36
  const file = await deps.getArtifactFile({
12
37
  artifactId: artifact.artifactId,
13
38
  filePath: fileSubject.filePath
@@ -45,7 +70,7 @@ export async function handleFile(deps, subject, detail, include) {
45
70
  content: include.includes("source") || detail !== "summary" ? file.content : undefined
46
71
  }
47
72
  },
48
- alwaysBlocks: ["subject"]
73
+ alwaysBlocks: ["subject", "file"]
49
74
  }),
50
75
  warnings: [...artifact.warnings]
51
76
  };
@@ -41,7 +41,8 @@ export async function handleVersions(deps, input, detail, include) {
41
41
  snapshots: input.includeSnapshots ? versions.snapshots : undefined,
42
42
  cached: versions.cached
43
43
  }
44
- }
44
+ },
45
+ alwaysBlocks: ["versions"]
45
46
  }),
46
47
  warnings: versions.warnings ?? []
47
48
  };
@@ -210,6 +210,7 @@ export async function resolveWorkspaceArtifactReference(deps, subject, artifactR
210
210
  return {
211
211
  artifactId: artifact.artifactId,
212
212
  artifact,
213
+ version: artifact.version,
213
214
  warnings: [...artifact.warnings]
214
215
  };
215
216
  }
@@ -468,6 +469,7 @@ async function resolveArtifactRef(deps, ref, subject) {
468
469
  return {
469
470
  artifactId: artifact.artifactId,
470
471
  artifact,
472
+ version: artifact.version,
471
473
  warnings: [...artifact.warnings]
472
474
  };
473
475
  }
@@ -30,6 +30,8 @@ export declare function createTruncationMeta(input: {
30
30
  }): TruncationMeta;
31
31
  export declare function buildEntryToolMeta(input: {
32
32
  detail: DetailLevel;
33
+ /** When set and equal to `detail`, detailApplied is omitted (the default needs no echo). */
34
+ defaultDetail?: DetailLevel;
33
35
  include?: readonly string[];
34
36
  warnings?: readonly string[];
35
37
  truncated?: TruncationMeta;
@@ -71,8 +71,8 @@ export function createTruncationMeta(input) {
71
71
  }
72
72
  export function buildEntryToolMeta(input) {
73
73
  return {
74
- warnings: [...(input.warnings ?? [])],
75
- detailApplied: input.detail,
74
+ ...(input.warnings && input.warnings.length > 0 ? { warnings: [...input.warnings] } : {}),
75
+ ...(input.detail === input.defaultDetail ? {} : { detailApplied: input.detail }),
76
76
  ...(input.include && input.include.length > 0
77
77
  ? { includeApplied: normalizeIncludeGroups(input.include) }
78
78
  : {}),
@@ -454,7 +454,7 @@ export declare const validateProjectSchema: z.ZodEffects<z.ZodObject<{
454
454
  reportMode: "full" | "summary-first" | "compact";
455
455
  treatInfoAsWarning: boolean;
456
456
  includeIssues: boolean;
457
- task: "project-summary" | "mixin" | "access-widener" | "access-transformer";
457
+ task: "project-summary" | "mixin" | "access-transformer" | "access-widener";
458
458
  subject: {
459
459
  kind: "workspace";
460
460
  projectPath: string;
@@ -510,7 +510,7 @@ export declare const validateProjectSchema: z.ZodEffects<z.ZodObject<{
510
510
  include?: string[] | undefined;
511
511
  atNamespace?: "obfuscated" | "mojang" | "srg" | undefined;
512
512
  }, {
513
- task: "project-summary" | "mixin" | "access-widener" | "access-transformer";
513
+ task: "project-summary" | "mixin" | "access-transformer" | "access-widener";
514
514
  subject: {
515
515
  kind: "workspace";
516
516
  projectPath: string;
@@ -580,7 +580,7 @@ export declare const validateProjectSchema: z.ZodEffects<z.ZodObject<{
580
580
  reportMode: "full" | "summary-first" | "compact";
581
581
  treatInfoAsWarning: boolean;
582
582
  includeIssues: boolean;
583
- task: "project-summary" | "mixin" | "access-widener" | "access-transformer";
583
+ task: "project-summary" | "mixin" | "access-transformer" | "access-widener";
584
584
  subject: {
585
585
  kind: "workspace";
586
586
  projectPath: string;
@@ -636,7 +636,7 @@ export declare const validateProjectSchema: z.ZodEffects<z.ZodObject<{
636
636
  include?: string[] | undefined;
637
637
  atNamespace?: "obfuscated" | "mojang" | "srg" | undefined;
638
638
  }, {
639
- task: "project-summary" | "mixin" | "access-widener" | "access-transformer";
639
+ task: "project-summary" | "mixin" | "access-transformer" | "access-widener";
640
640
  subject: {
641
641
  kind: "workspace";
642
642
  projectPath: string;
@@ -5,7 +5,7 @@ import { suggestSimilar } from "../mixin-validator.js";
5
5
  export const VERIFY_MIXIN_TARGET_OFF = process.env.VERIFY_MIXIN_TARGET_OFF === "1";
6
6
  const ACCESSOR_NAME_RE = /^(get|set|is)([A-Z]\w*)$/;
7
7
  const INVOKER_NAME_RE = /^(invoke|call)([A-Z]\w*)$/;
8
- function decodeAccessFlags(flags) {
8
+ function decodeAccessFlags(flags, kind) {
9
9
  const labels = [];
10
10
  if (flags == null) {
11
11
  return labels;
@@ -23,9 +23,9 @@ function decodeAccessFlags(flags) {
23
23
  if ((flags & 0x0020) !== 0)
24
24
  labels.push("synchronized");
25
25
  if ((flags & 0x0040) !== 0)
26
- labels.push("volatile");
26
+ labels.push(kind === "method" ? "bridge" : "volatile");
27
27
  if ((flags & 0x0080) !== 0)
28
- labels.push("transient");
28
+ labels.push(kind === "method" ? "varargs" : "transient");
29
29
  if ((flags & 0x0100) !== 0)
30
30
  labels.push("native");
31
31
  if ((flags & 0x0400) !== 0)
@@ -137,11 +137,11 @@ function deriveVisibility(accessFlags) {
137
137
  return "private";
138
138
  return "package-private";
139
139
  }
140
- function buildMatch(member) {
140
+ function buildMatch(member, kind) {
141
141
  return {
142
142
  name: member.name,
143
143
  descriptor: member.jvmDescriptor,
144
- accessFlags: decodeAccessFlags(member.accessFlags),
144
+ accessFlags: decodeAccessFlags(member.accessFlags, kind),
145
145
  javaSignature: member.javaSignature,
146
146
  ...(member.sourceLine ? { sourceLine: member.sourceLine } : {})
147
147
  };
@@ -267,7 +267,7 @@ export class VerifyMixinTargetService {
267
267
  if (member.descriptor) {
268
268
  const descriptorHits = exactNameHits.filter((m) => m.jvmDescriptor === member.descriptor);
269
269
  if (descriptorHits.length > 0) {
270
- matches = descriptorHits.map(buildMatch);
270
+ matches = descriptorHits.map((m) => buildMatch(m, member.kind));
271
271
  }
272
272
  else {
273
273
  matches = [];
@@ -279,7 +279,7 @@ export class VerifyMixinTargetService {
279
279
  }
280
280
  }
281
281
  else {
282
- matches = exactNameHits.map(buildMatch);
282
+ matches = exactNameHits.map((m) => buildMatch(m, member.kind));
283
283
  }
284
284
  if (matches.length === 0 && candidates.length === 0) {
285
285
  const suggestions = suggestSimilar(memberName, allMemberNames);