@adhisang/minecraft-modding-mcp 3.1.1 → 4.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 (61) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/README.md +37 -18
  3. package/dist/access-transformer-parser.d.ts +17 -0
  4. package/dist/access-transformer-parser.js +97 -0
  5. package/dist/cache-registry.d.ts +1 -1
  6. package/dist/cache-registry.js +10 -2
  7. package/dist/concurrency.d.ts +1 -0
  8. package/dist/concurrency.js +24 -0
  9. package/dist/config.d.ts +10 -1
  10. package/dist/config.js +52 -1
  11. package/dist/decompiler/vineflower.js +22 -21
  12. package/dist/entry-tools/analyze-mod-service.d.ts +4 -4
  13. package/dist/entry-tools/analyze-symbol-service.d.ts +22 -22
  14. package/dist/entry-tools/analyze-symbol-service.js +13 -2
  15. package/dist/entry-tools/inspect-minecraft-service.d.ts +168 -168
  16. package/dist/entry-tools/inspect-minecraft-service.js +8 -2
  17. package/dist/entry-tools/manage-cache-service.d.ts +4 -4
  18. package/dist/entry-tools/validate-project-service.d.ts +153 -16
  19. package/dist/entry-tools/validate-project-service.js +442 -25
  20. package/dist/gradle-paths.d.ts +4 -0
  21. package/dist/gradle-paths.js +57 -0
  22. package/dist/index.js +148 -30
  23. package/dist/lru-list.d.ts +31 -0
  24. package/dist/lru-list.js +102 -0
  25. package/dist/mapping-pipeline-service.d.ts +12 -1
  26. package/dist/mapping-pipeline-service.js +28 -1
  27. package/dist/mapping-service.d.ts +16 -0
  28. package/dist/mapping-service.js +405 -68
  29. package/dist/minecraft-explorer-service.d.ts +13 -0
  30. package/dist/minecraft-explorer-service.js +8 -4
  31. package/dist/mixin-validator.d.ts +33 -2
  32. package/dist/mixin-validator.js +218 -17
  33. package/dist/mod-analyzer.d.ts +1 -0
  34. package/dist/mod-analyzer.js +17 -1
  35. package/dist/mod-decompile-service.js +4 -4
  36. package/dist/mod-remap-service.js +1 -54
  37. package/dist/mod-search-service.d.ts +1 -0
  38. package/dist/mod-search-service.js +84 -51
  39. package/dist/observability.d.ts +18 -1
  40. package/dist/observability.js +44 -1
  41. package/dist/response-utils.d.ts +69 -0
  42. package/dist/response-utils.js +227 -0
  43. package/dist/source-jar-reader.d.ts +16 -0
  44. package/dist/source-jar-reader.js +103 -1
  45. package/dist/source-resolver.d.ts +9 -1
  46. package/dist/source-resolver.js +23 -16
  47. package/dist/source-service.d.ts +119 -3
  48. package/dist/source-service.js +1836 -218
  49. package/dist/storage/artifacts-repo.d.ts +4 -1
  50. package/dist/storage/artifacts-repo.js +33 -5
  51. package/dist/storage/files-repo.d.ts +0 -2
  52. package/dist/storage/files-repo.js +0 -11
  53. package/dist/storage/migrations.d.ts +1 -1
  54. package/dist/storage/migrations.js +10 -2
  55. package/dist/storage/schema.d.ts +2 -0
  56. package/dist/storage/schema.js +25 -0
  57. package/dist/tool-contract-manifest.js +8 -6
  58. package/dist/types.d.ts +20 -0
  59. package/dist/workspace-mapping-service.d.ts +13 -0
  60. package/dist/workspace-mapping-service.js +146 -14
  61. package/package.json +3 -1
@@ -1,5 +1,6 @@
1
- import { readFileSync } from "node:fs";
1
+ import { readFile } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
+ import { mapWithConcurrencyLimit } from "./concurrency.js";
3
4
  import { createError, ERROR_CODES } from "./errors.js";
4
5
  import { log } from "./logger.js";
5
6
  import { validateAndNormalizeJarPath } from "./path-resolver.js";
@@ -9,6 +10,7 @@ const DEFAULT_LIMIT = 50;
9
10
  const MAX_LIMIT = 200;
10
11
  const MAX_QUERY_LENGTH = 200;
11
12
  const CONTEXT_LINES = 1;
13
+ const DECOMPILED_JAVA_READ_CONCURRENCY = 8;
12
14
  const METHOD_PATTERN = /^\s*(public|private|protected)\s+.*\(/;
13
15
  const FIELD_PATTERN = /^\s*(public|private|protected)\s+(?:static\s+)?(?:final\s+)?[\w<>,\[\]?]+\s+\w+\s*[;=]/;
14
16
  function buildRegex(query) {
@@ -41,6 +43,9 @@ function extractContext(lines, lineIndex) {
41
43
  function filePathToClassName(filePath) {
42
44
  return filePath.replace(/\.java$/, "").replaceAll("/", ".");
43
45
  }
46
+ function cloneRegex(regex) {
47
+ return new RegExp(regex.source, regex.flags);
48
+ }
44
49
  export class ModSearchService {
45
50
  modDecompileService;
46
51
  constructor(modDecompileService) {
@@ -112,65 +117,40 @@ export class ModSearchService {
112
117
  const hits = [];
113
118
  let totalHits = 0;
114
119
  let reachedLimit = false;
115
- for (const className of classNames) {
120
+ for (let batchStartIndex = 0; batchStartIndex < classNames.length; batchStartIndex += DECOMPILED_JAVA_READ_CONCURRENCY) {
116
121
  if (hits.length >= limit) {
117
122
  reachedLimit = true;
118
123
  break;
119
124
  }
120
- const filePath = className.replaceAll(".", "/") + ".java";
121
- // Class name search: check if the simple class name matches
122
- if (searchType === "class" || searchType === "all") {
123
- const simpleClassName = className.split(".").pop() ?? className;
124
- regex.lastIndex = 0;
125
- if (regex.test(simpleClassName)) {
126
- totalHits++;
127
- if (hits.length < limit) {
128
- hits.push({
129
- type: "class",
130
- name: className,
131
- file: filePath
132
- });
133
- }
134
- // If searching only classes, skip content search for this file
135
- if (searchType === "class")
136
- continue;
137
- }
138
- }
139
- // Content/method/field search: read and scan the file
140
- if (searchType === "method" || searchType === "field" || searchType === "content" || searchType === "all") {
141
- let content;
142
- try {
143
- content = readFileSync(join(outputDir, filePath), "utf8");
125
+ const batchClassNames = classNames.slice(batchStartIndex, batchStartIndex + DECOMPILED_JAVA_READ_CONCURRENCY);
126
+ const batchHitLimit = Math.max(1, limit - hits.length);
127
+ const fileResults = await mapWithConcurrencyLimit(batchClassNames, DECOMPILED_JAVA_READ_CONCURRENCY, async (className) => this.searchDecompiledClassFile({
128
+ className,
129
+ outputDir,
130
+ searchType,
131
+ regex: cloneRegex(regex),
132
+ maxHits: batchHitLimit
133
+ }));
134
+ for (const fileResult of fileResults) {
135
+ if (hits.length >= limit) {
136
+ reachedLimit = true;
137
+ break;
144
138
  }
145
- catch {
146
- // File might not exist at the expected path, skip
139
+ if (fileResult.hits.length === 0) {
147
140
  continue;
148
141
  }
149
- const lines = content.split("\n");
150
- for (let i = 0; i < lines.length; i++) {
151
- if (hits.length >= limit) {
152
- reachedLimit = true;
153
- break;
154
- }
155
- regex.lastIndex = 0;
156
- if (!regex.test(lines[i]))
157
- continue;
158
- const lineType = classifyLine(lines[i]);
159
- // Filter by search type
160
- if (searchType !== "all" && searchType !== lineType)
161
- continue;
162
- totalHits++;
163
- if (hits.length < limit) {
164
- hits.push({
165
- type: lineType,
166
- name: lineType === "content" ? className : extractSymbolName(lines[i], lineType),
167
- file: filePath,
168
- line: i + 1,
169
- context: extractContext(lines, i)
170
- });
171
- }
142
+ const remaining = limit - hits.length;
143
+ const acceptedHits = fileResult.hits.slice(0, remaining);
144
+ hits.push(...acceptedHits);
145
+ totalHits += acceptedHits.length;
146
+ if (hits.length >= limit || fileResult.hits.length > remaining) {
147
+ reachedLimit = true;
148
+ break;
172
149
  }
173
150
  }
151
+ if (reachedLimit) {
152
+ break;
153
+ }
174
154
  }
175
155
  log("info", "mod-search.done", {
176
156
  jarPath,
@@ -255,6 +235,59 @@ export class ModSearchService {
255
235
  warnings
256
236
  };
257
237
  }
238
+ async searchDecompiledClassFile(input) {
239
+ const hits = [];
240
+ const filePath = input.className.replaceAll(".", "/") + ".java";
241
+ if (input.searchType === "class" || input.searchType === "all") {
242
+ const simpleClassName = input.className.split(".").pop() ?? input.className;
243
+ input.regex.lastIndex = 0;
244
+ if (input.regex.test(simpleClassName)) {
245
+ hits.push({
246
+ type: "class",
247
+ name: input.className,
248
+ file: filePath
249
+ });
250
+ if (input.searchType === "class" || hits.length >= input.maxHits) {
251
+ return { hits };
252
+ }
253
+ }
254
+ }
255
+ if (input.searchType !== "method" &&
256
+ input.searchType !== "field" &&
257
+ input.searchType !== "content" &&
258
+ input.searchType !== "all") {
259
+ return { hits };
260
+ }
261
+ let content;
262
+ try {
263
+ content = await readFile(join(input.outputDir, filePath), "utf8");
264
+ }
265
+ catch {
266
+ return { hits };
267
+ }
268
+ const lines = content.split("\n");
269
+ for (let i = 0; i < lines.length; i++) {
270
+ if (hits.length >= input.maxHits) {
271
+ break;
272
+ }
273
+ input.regex.lastIndex = 0;
274
+ if (!input.regex.test(lines[i])) {
275
+ continue;
276
+ }
277
+ const lineType = classifyLine(lines[i]);
278
+ if (input.searchType !== "all" && input.searchType !== lineType) {
279
+ continue;
280
+ }
281
+ hits.push({
282
+ type: lineType,
283
+ name: lineType === "content" ? input.className : extractSymbolName(lines[i], lineType),
284
+ file: filePath,
285
+ line: i + 1,
286
+ context: extractContext(lines, i)
287
+ });
288
+ }
289
+ return { hits };
290
+ }
258
291
  }
259
292
  function extractSymbolName(line, type) {
260
293
  const trimmed = line.trim();
@@ -22,6 +22,7 @@ export interface RuntimeMetricSnapshot {
22
22
  get_file_duration_ms: MetricTimingSnapshot;
23
23
  list_files_duration_ms: MetricTimingSnapshot;
24
24
  decompile_duration_ms: MetricTimingSnapshot;
25
+ binary_remap_duration_ms: MetricTimingSnapshot;
25
26
  search_intent_symbol_duration_ms: MetricTimingSnapshot;
26
27
  search_intent_text_duration_ms: MetricTimingSnapshot;
27
28
  search_intent_path_duration_ms: MetricTimingSnapshot;
@@ -47,8 +48,13 @@ export interface RuntimeMetricSnapshot {
47
48
  cache_artifact_bytes_lru: CacheArtifactByteAccountingRow[];
48
49
  cache_hit_rate: number;
49
50
  repo_failover_count: number;
51
+ tool_call_counts: Record<string, number>;
52
+ tool_call_duration_ms: Record<string, MetricTimingSnapshot>;
53
+ mapping_resolution_cache_hits: number;
54
+ mapping_resolution_cache_misses: number;
55
+ mapping_resolution_cache_size: number;
50
56
  }
51
- type DurationMetricName = keyof Pick<RuntimeMetricSnapshot, "resolve_duration_ms" | "search_duration_ms" | "get_file_duration_ms" | "list_files_duration_ms" | "decompile_duration_ms" | "search_intent_symbol_duration_ms" | "search_intent_text_duration_ms" | "search_intent_path_duration_ms">;
57
+ type DurationMetricName = keyof Pick<RuntimeMetricSnapshot, "resolve_duration_ms" | "search_duration_ms" | "get_file_duration_ms" | "list_files_duration_ms" | "decompile_duration_ms" | "binary_remap_duration_ms" | "search_intent_symbol_duration_ms" | "search_intent_text_duration_ms" | "search_intent_path_duration_ms">;
52
58
  export declare class RuntimeMetrics {
53
59
  private readonly timings;
54
60
  private cacheHits;
@@ -74,6 +80,11 @@ export declare class RuntimeMetrics {
74
80
  private cacheEntries;
75
81
  private cacheTotalContentBytes;
76
82
  private cacheArtifactBytesLruRef;
83
+ private toolCallCounts;
84
+ private toolCallTimings;
85
+ private mappingResolutionCacheHits;
86
+ private mappingResolutionCacheMisses;
87
+ private mappingResolutionCacheSize;
77
88
  constructor();
78
89
  recordDuration(name: DurationMetricName, durationMs: number): void;
79
90
  recordArtifactCacheHit(): void;
@@ -97,6 +108,12 @@ export declare class RuntimeMetrics {
97
108
  setCacheEntries(entries: number): void;
98
109
  setCacheTotalContentBytes(totalBytes: number): void;
99
110
  setCacheArtifactByteAccountingRef(entries: ReadonlyArray<CacheArtifactByteAccountingRefRow>): void;
111
+ recordToolCall(tool: string, durationMs: number): void;
112
+ setMappingResolutionCacheStats(stats: {
113
+ hits: number;
114
+ misses: number;
115
+ size: number;
116
+ }): void;
100
117
  snapshot(): RuntimeMetricSnapshot;
101
118
  private toSnapshot;
102
119
  private resolveCacheHitRate;
@@ -32,6 +32,11 @@ export class RuntimeMetrics {
32
32
  cacheEntries = 0;
33
33
  cacheTotalContentBytes = 0;
34
34
  cacheArtifactBytesLruRef = [];
35
+ toolCallCounts = new Map();
36
+ toolCallTimings = new Map();
37
+ mappingResolutionCacheHits = 0;
38
+ mappingResolutionCacheMisses = 0;
39
+ mappingResolutionCacheSize = 0;
35
40
  constructor() {
36
41
  const names = [
37
42
  "resolve_duration_ms",
@@ -39,6 +44,7 @@ export class RuntimeMetrics {
39
44
  "get_file_duration_ms",
40
45
  "list_files_duration_ms",
41
46
  "decompile_duration_ms",
47
+ "binary_remap_duration_ms",
42
48
  "search_intent_symbol_duration_ms",
43
49
  "search_intent_text_duration_ms",
44
50
  "search_intent_path_duration_ms"
@@ -140,6 +146,27 @@ export class RuntimeMetrics {
140
146
  setCacheArtifactByteAccountingRef(entries) {
141
147
  this.cacheArtifactBytesLruRef = entries;
142
148
  }
149
+ recordToolCall(tool, durationMs) {
150
+ this.toolCallCounts.set(tool, (this.toolCallCounts.get(tool) ?? 0) + 1);
151
+ let timing = this.toolCallTimings.get(tool);
152
+ if (!timing) {
153
+ timing = { count: 0, totalMs: 0, lastMs: 0, samples: [] };
154
+ this.toolCallTimings.set(tool, timing);
155
+ }
156
+ const normalizedDuration = Math.max(0, Math.trunc(durationMs));
157
+ timing.count += 1;
158
+ timing.totalMs += normalizedDuration;
159
+ timing.lastMs = normalizedDuration;
160
+ timing.samples.push(normalizedDuration);
161
+ if (timing.samples.length > MAX_TIMING_SAMPLES) {
162
+ timing.samples.shift();
163
+ }
164
+ }
165
+ setMappingResolutionCacheStats(stats) {
166
+ this.mappingResolutionCacheHits = stats.hits;
167
+ this.mappingResolutionCacheMisses = stats.misses;
168
+ this.mappingResolutionCacheSize = stats.size;
169
+ }
143
170
  snapshot() {
144
171
  return {
145
172
  resolve_duration_ms: this.toSnapshot("resolve_duration_ms"),
@@ -147,6 +174,7 @@ export class RuntimeMetrics {
147
174
  get_file_duration_ms: this.toSnapshot("get_file_duration_ms"),
148
175
  list_files_duration_ms: this.toSnapshot("list_files_duration_ms"),
149
176
  decompile_duration_ms: this.toSnapshot("decompile_duration_ms"),
177
+ binary_remap_duration_ms: this.toSnapshot("binary_remap_duration_ms"),
150
178
  search_intent_symbol_duration_ms: this.toSnapshot("search_intent_symbol_duration_ms"),
151
179
  search_intent_text_duration_ms: this.toSnapshot("search_intent_text_duration_ms"),
152
180
  search_intent_path_duration_ms: this.toSnapshot("search_intent_path_duration_ms"),
@@ -175,7 +203,22 @@ export class RuntimeMetrics {
175
203
  updated_at: entry.updatedAt
176
204
  })),
177
205
  cache_hit_rate: this.resolveCacheHitRate(),
178
- repo_failover_count: this.repoFailoverCount
206
+ repo_failover_count: this.repoFailoverCount,
207
+ tool_call_counts: Object.fromEntries(this.toolCallCounts),
208
+ tool_call_duration_ms: Object.fromEntries([...this.toolCallTimings].map(([tool, timing]) => [
209
+ tool,
210
+ {
211
+ count: timing.count,
212
+ totalMs: timing.totalMs,
213
+ avgMs: timing.count > 0 ? timing.totalMs / timing.count : 0,
214
+ lastMs: timing.lastMs,
215
+ p95Ms: percentile(timing.samples, 95),
216
+ p99Ms: percentile(timing.samples, 99)
217
+ }
218
+ ])),
219
+ mapping_resolution_cache_hits: this.mappingResolutionCacheHits,
220
+ mapping_resolution_cache_misses: this.mappingResolutionCacheMisses,
221
+ mapping_resolution_cache_size: this.mappingResolutionCacheSize
179
222
  };
180
223
  }
181
224
  toSnapshot(name) {
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Compact-mode response utilities.
3
+ *
4
+ * Applied at the public boundary (runTool) only — internal service types are never modified.
5
+ */
6
+ /** Tools that accept the `compact` parameter. */
7
+ export declare const COMPACT_ENABLED_TOOL_NAMES: Set<string>;
8
+ /** Mapping-oriented tools that get additional field projection via compactMappingResponse. */
9
+ export declare const COMPACT_MAPPING_TOOL_NAMES: Set<string>;
10
+ /** Source-oriented tools (get-class-source) that get compactSourceResponse projection. */
11
+ export declare const COMPACT_SOURCE_TOOL_NAMES: Set<string>;
12
+ /** Member-listing tools (get-class-members) that get compactMembersResponse projection. */
13
+ export declare const COMPACT_MEMBERS_TOOL_NAMES: Set<string>;
14
+ /**
15
+ * Tools that only need the light artifactContents projection (search hits,
16
+ * file listing). The primary payload is already small; the projection just
17
+ * drops the artifact-level summary that callers rarely consume.
18
+ */
19
+ export declare const COMPACT_LIGHT_TOOL_NAMES: Set<string>;
20
+ /**
21
+ * Double-gated compact check: tool must be in the allowlist AND parsedInput.compact must be true.
22
+ * Prevents activation on passthrough schemas where Zod doesn't strip unknown keys.
23
+ */
24
+ export declare function isCompactEnabled(tool: string, parsedInput: unknown): boolean;
25
+ /**
26
+ * Primary-payload keys that compact mode must preserve per tool, even when
27
+ * the value is an empty array (no hits, no files, no members). Without this
28
+ * the generic {@link compactResponse} path would strip `hits: []` / `items: []`
29
+ * from successful zero-result responses and callers could not distinguish
30
+ * "empty success" from "field missing".
31
+ *
32
+ * Only tools whose primary payload can legitimately be an empty array need
33
+ * an entry here. `get-class-source` returns `sourceText: string` which is
34
+ * never stripped by compactResponse.
35
+ */
36
+ export declare const TOOL_PRESERVE_PAYLOAD_KEYS: Record<string, ReadonlySet<string>>;
37
+ /**
38
+ * Shallow-strip empty values from a response object.
39
+ * Only operates on the top level — nested structures are preserved as-is.
40
+ * Non-plain objects (Date, Map, class instances) are never treated as empty.
41
+ *
42
+ * `preserveKeys` names keys whose values MUST survive the strip even if
43
+ * empty (used by tools whose primary payload is an array that can legitimately
44
+ * be empty — e.g. zero-hit search, empty file listing). `null` / `undefined`
45
+ * values are still dropped even for preserved keys, so absent optional
46
+ * payload fields do not leak through as explicit nulls.
47
+ */
48
+ export declare function compactResponse(obj: Record<string, unknown>, preserveKeys?: ReadonlySet<string>): Record<string, unknown>;
49
+ /** resolve-artifact compact: omit debug/diagnostic fields. */
50
+ export declare function compactArtifactResponse(obj: Record<string, unknown>): Record<string, unknown>;
51
+ /** get-class-source compact: drop provenance, artifactContents, qualityFlags. */
52
+ export declare function compactSourceResponse(obj: Record<string, unknown>): Record<string, unknown>;
53
+ /** get-class-members compact: drop provenance, artifactContents, qualityFlags, context. */
54
+ export declare function compactMembersResponse(obj: Record<string, unknown>): Record<string, unknown>;
55
+ /** Light compact projection: drop the artifactContents summary only. */
56
+ export declare function compactLightResponse(obj: Record<string, unknown>): Record<string, unknown>;
57
+ /**
58
+ * Mapping tool compact: project candidates for size reduction.
59
+ *
60
+ * Resolved-exact path: omit candidates entirely when provably redundant.
61
+ * All of: resolved===true, resolvedSymbol exists, single exact candidate, not truncated,
62
+ * confidence missing or 1.
63
+ *
64
+ * Unresolved/ambiguous path: keep top {@link UNRESOLVED_FULL_DETAIL_LIMIT} candidates with full
65
+ * metadata, slim the tail to {kind,symbol,owner,name,descriptor,confidence,matchKind}, and
66
+ * surface `candidatesTruncated:true` + `totalCandidateCount` so the caller knows what it's
67
+ * seeing.
68
+ */
69
+ export declare function compactMappingResponse(obj: Record<string, unknown>): Record<string, unknown>;
@@ -0,0 +1,227 @@
1
+ /**
2
+ * Compact-mode response utilities.
3
+ *
4
+ * Applied at the public boundary (runTool) only — internal service types are never modified.
5
+ */
6
+ /** Tools that accept the `compact` parameter. */
7
+ export const COMPACT_ENABLED_TOOL_NAMES = new Set([
8
+ "resolve-artifact",
9
+ "find-mapping",
10
+ "resolve-method-mapping-exact",
11
+ "resolve-workspace-symbol",
12
+ "check-symbol-exists",
13
+ "get-class-source",
14
+ "get-class-members",
15
+ "search-class-source",
16
+ "list-artifact-files"
17
+ ]);
18
+ /** Mapping-oriented tools that get additional field projection via compactMappingResponse. */
19
+ export const COMPACT_MAPPING_TOOL_NAMES = new Set([
20
+ "find-mapping",
21
+ "resolve-method-mapping-exact",
22
+ "resolve-workspace-symbol",
23
+ "check-symbol-exists"
24
+ ]);
25
+ /** Source-oriented tools (get-class-source) that get compactSourceResponse projection. */
26
+ export const COMPACT_SOURCE_TOOL_NAMES = new Set([
27
+ "get-class-source"
28
+ ]);
29
+ /** Member-listing tools (get-class-members) that get compactMembersResponse projection. */
30
+ export const COMPACT_MEMBERS_TOOL_NAMES = new Set([
31
+ "get-class-members"
32
+ ]);
33
+ /**
34
+ * Tools that only need the light artifactContents projection (search hits,
35
+ * file listing). The primary payload is already small; the projection just
36
+ * drops the artifact-level summary that callers rarely consume.
37
+ */
38
+ export const COMPACT_LIGHT_TOOL_NAMES = new Set([
39
+ "search-class-source",
40
+ "list-artifact-files"
41
+ ]);
42
+ /**
43
+ * Double-gated compact check: tool must be in the allowlist AND parsedInput.compact must be true.
44
+ * Prevents activation on passthrough schemas where Zod doesn't strip unknown keys.
45
+ */
46
+ export function isCompactEnabled(tool, parsedInput) {
47
+ if (!COMPACT_ENABLED_TOOL_NAMES.has(tool))
48
+ return false;
49
+ if (parsedInput &&
50
+ typeof parsedInput === "object" &&
51
+ !Array.isArray(parsedInput) &&
52
+ parsedInput.compact === true) {
53
+ return true;
54
+ }
55
+ return false;
56
+ }
57
+ function isPlainObject(v) {
58
+ if (typeof v !== "object" || v === null || Array.isArray(v))
59
+ return false;
60
+ const proto = Object.getPrototypeOf(v);
61
+ return proto === Object.prototype || proto === null;
62
+ }
63
+ /**
64
+ * Primary-payload keys that compact mode must preserve per tool, even when
65
+ * the value is an empty array (no hits, no files, no members). Without this
66
+ * the generic {@link compactResponse} path would strip `hits: []` / `items: []`
67
+ * from successful zero-result responses and callers could not distinguish
68
+ * "empty success" from "field missing".
69
+ *
70
+ * Only tools whose primary payload can legitimately be an empty array need
71
+ * an entry here. `get-class-source` returns `sourceText: string` which is
72
+ * never stripped by compactResponse.
73
+ */
74
+ export const TOOL_PRESERVE_PAYLOAD_KEYS = {
75
+ "search-class-source": new Set(["hits"]),
76
+ "list-artifact-files": new Set(["items"]),
77
+ "get-class-members": new Set(["members", "counts", "decompiledFallback", "decompiledMemberCounts"])
78
+ };
79
+ /**
80
+ * Shallow-strip empty values from a response object.
81
+ * Only operates on the top level — nested structures are preserved as-is.
82
+ * Non-plain objects (Date, Map, class instances) are never treated as empty.
83
+ *
84
+ * `preserveKeys` names keys whose values MUST survive the strip even if
85
+ * empty (used by tools whose primary payload is an array that can legitimately
86
+ * be empty — e.g. zero-hit search, empty file listing). `null` / `undefined`
87
+ * values are still dropped even for preserved keys, so absent optional
88
+ * payload fields do not leak through as explicit nulls.
89
+ */
90
+ export function compactResponse(obj, preserveKeys) {
91
+ if (!isPlainObject(obj))
92
+ return {};
93
+ const result = {};
94
+ for (const [key, value] of Object.entries(obj)) {
95
+ if (value === null || value === undefined)
96
+ continue;
97
+ if (preserveKeys?.has(key)) {
98
+ result[key] = value;
99
+ continue;
100
+ }
101
+ if (Array.isArray(value) && value.length === 0)
102
+ continue;
103
+ if (isPlainObject(value) && Object.keys(value).length === 0)
104
+ continue;
105
+ result[key] = value;
106
+ }
107
+ return result;
108
+ }
109
+ function projectOmitKeys(obj, omit) {
110
+ const projected = {};
111
+ for (const [key, value] of Object.entries(obj)) {
112
+ if (omit.has(key))
113
+ continue;
114
+ projected[key] = value;
115
+ }
116
+ return projected;
117
+ }
118
+ /** Fields to omit from resolve-artifact in compact mode. */
119
+ const ARTIFACT_COMPACT_OMIT_KEYS = new Set([
120
+ "provenance",
121
+ "artifactContents",
122
+ "sampleEntries",
123
+ "adjacentSourceCandidates",
124
+ "binaryJarPath",
125
+ "coordinate",
126
+ "repoUrl",
127
+ "resolvedSourceJarPath"
128
+ ]);
129
+ /** resolve-artifact compact: omit debug/diagnostic fields. */
130
+ export function compactArtifactResponse(obj) {
131
+ return projectOmitKeys(obj, ARTIFACT_COMPACT_OMIT_KEYS);
132
+ }
133
+ /** Fields to omit from get-class-source in compact mode. */
134
+ const SOURCE_COMPACT_OMIT_KEYS = new Set([
135
+ "provenance",
136
+ "artifactContents",
137
+ "qualityFlags"
138
+ ]);
139
+ /** get-class-source compact: drop provenance, artifactContents, qualityFlags. */
140
+ export function compactSourceResponse(obj) {
141
+ return projectOmitKeys(obj, SOURCE_COMPACT_OMIT_KEYS);
142
+ }
143
+ /** Fields to omit from get-class-members in compact mode. */
144
+ const MEMBERS_COMPACT_OMIT_KEYS = new Set([
145
+ "provenance",
146
+ "artifactContents",
147
+ "qualityFlags",
148
+ "context"
149
+ ]);
150
+ /** get-class-members compact: drop provenance, artifactContents, qualityFlags, context. */
151
+ export function compactMembersResponse(obj) {
152
+ return projectOmitKeys(obj, MEMBERS_COMPACT_OMIT_KEYS);
153
+ }
154
+ /** Fields to omit from search-class-source / list-artifact-files in compact mode. */
155
+ const LIGHT_COMPACT_OMIT_KEYS = new Set([
156
+ "artifactContents"
157
+ ]);
158
+ /** Light compact projection: drop the artifactContents summary only. */
159
+ export function compactLightResponse(obj) {
160
+ return projectOmitKeys(obj, LIGHT_COMPACT_OMIT_KEYS);
161
+ }
162
+ /** Max number of unresolved candidates that get full metadata in compact mode. */
163
+ const UNRESOLVED_FULL_DETAIL_LIMIT = 3;
164
+ /**
165
+ * Slim projection of a candidate: retains only identification + confidence fields.
166
+ *
167
+ * `kind` and `symbol` are part of the public `SymbolReference` / candidate contract that
168
+ * clients branch on and use as rendering keys, so they MUST survive the slim. The heavy
169
+ * fields removed here are the cycle-local diagnostic metadata (provenance, context,
170
+ * ambiguityReasons, warnings on the candidate, etc.) — not the identity fields.
171
+ */
172
+ function slimCandidate(candidate) {
173
+ if (!isPlainObject(candidate))
174
+ return candidate;
175
+ const picked = {};
176
+ for (const key of ["kind", "symbol", "owner", "name", "descriptor", "confidence", "matchKind"]) {
177
+ if (candidate[key] !== undefined) {
178
+ picked[key] = candidate[key];
179
+ }
180
+ }
181
+ return picked;
182
+ }
183
+ /**
184
+ * Mapping tool compact: project candidates for size reduction.
185
+ *
186
+ * Resolved-exact path: omit candidates entirely when provably redundant.
187
+ * All of: resolved===true, resolvedSymbol exists, single exact candidate, not truncated,
188
+ * confidence missing or 1.
189
+ *
190
+ * Unresolved/ambiguous path: keep top {@link UNRESOLVED_FULL_DETAIL_LIMIT} candidates with full
191
+ * metadata, slim the tail to {kind,symbol,owner,name,descriptor,confidence,matchKind}, and
192
+ * surface `candidatesTruncated:true` + `totalCandidateCount` so the caller knows what it's
193
+ * seeing.
194
+ */
195
+ export function compactMappingResponse(obj) {
196
+ const projected = { ...obj };
197
+ const candidates = projected.candidates;
198
+ if (projected.resolved === true &&
199
+ projected.resolvedSymbol !== undefined &&
200
+ Array.isArray(candidates) &&
201
+ candidates.length === 1 &&
202
+ projected.candidateCount === 1 &&
203
+ !projected.candidatesTruncated) {
204
+ const candidate = candidates[0];
205
+ if (candidate &&
206
+ candidate.matchKind === "exact" &&
207
+ (candidate.confidence === undefined || candidate.confidence === 1)) {
208
+ delete projected.candidates;
209
+ return projected;
210
+ }
211
+ }
212
+ if (projected.resolved === false &&
213
+ Array.isArray(candidates) &&
214
+ candidates.length > UNRESOLVED_FULL_DETAIL_LIMIT) {
215
+ const head = candidates.slice(0, UNRESOLVED_FULL_DETAIL_LIMIT);
216
+ const tail = candidates.slice(UNRESOLVED_FULL_DETAIL_LIMIT).map(slimCandidate);
217
+ projected.candidates = [...head, ...tail];
218
+ // Tail slimming keeps the full candidate array; only metadata was dropped. Use a
219
+ // dedicated `candidateDetailsTruncated` signal so clients do not confuse it with the
220
+ // existing `candidatesTruncated` semantics ("more candidates exist than this response
221
+ // contains"). If the upstream already reported list-level truncation via
222
+ // `candidatesTruncated`, that value is preserved unchanged.
223
+ projected.candidateDetailsTruncated = true;
224
+ }
225
+ return projected;
226
+ }
227
+ //# sourceMappingURL=response-utils.js.map
@@ -2,12 +2,28 @@ export interface JavaEntryText {
2
2
  filePath: string;
3
3
  content: string;
4
4
  }
5
+ export interface JarEntryText {
6
+ filePath: string;
7
+ content: string;
8
+ }
9
+ export interface CollectMatchedJarEntriesOptions {
10
+ maxBytes?: number;
11
+ maxEntries?: number;
12
+ continueOnError?: boolean;
13
+ }
14
+ export declare function hasJavaSourceExtension(entryPath: string): boolean;
5
15
  export declare class EntryTooLargeError extends Error {
6
16
  constructor(entryPath: string, jarPath: string, maxBytes: number);
7
17
  }
8
18
  export declare function listJarEntries(jarPath: string): Promise<string[]>;
9
19
  export declare function listJavaEntries(jarPath: string): Promise<string[]>;
20
+ export declare function hasAnyJarEntry(jarPath: string, predicate: (entryPath: string) => boolean): Promise<boolean>;
10
21
  export declare function readJarEntryAsUtf8(jarPath: string, entryPath: string): Promise<string>;
11
22
  export declare function readJarEntryAsBuffer(jarPath: string, entryPath: string): Promise<Buffer>;
23
+ export declare function collectMatchedJarEntriesAsUtf8(jarPath: string, predicate: (entryPath: string) => boolean, options?: CollectMatchedJarEntriesOptions): Promise<JarEntryText[]>;
12
24
  export declare function iterateJavaEntriesAsUtf8(jarPath: string, maxBytes?: number): AsyncGenerator<JavaEntryText>;
13
25
  export declare function readAllJavaEntriesAsUtf8(jarPath: string, maxBytes?: number): Promise<JavaEntryText[]>;
26
+ export declare function detectFabricLikeInputNamespace(inputJar: string): Promise<{
27
+ fromNamespace: "intermediary" | "mojang";
28
+ warnings: string[];
29
+ }>;