@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.
- package/CHANGELOG.md +49 -0
- package/README.md +37 -18
- package/dist/access-transformer-parser.d.ts +17 -0
- package/dist/access-transformer-parser.js +97 -0
- package/dist/cache-registry.d.ts +1 -1
- package/dist/cache-registry.js +10 -2
- package/dist/concurrency.d.ts +1 -0
- package/dist/concurrency.js +24 -0
- package/dist/config.d.ts +10 -1
- package/dist/config.js +52 -1
- package/dist/decompiler/vineflower.js +22 -21
- package/dist/entry-tools/analyze-mod-service.d.ts +4 -4
- package/dist/entry-tools/analyze-symbol-service.d.ts +22 -22
- package/dist/entry-tools/analyze-symbol-service.js +13 -2
- package/dist/entry-tools/inspect-minecraft-service.d.ts +168 -168
- package/dist/entry-tools/inspect-minecraft-service.js +8 -2
- package/dist/entry-tools/manage-cache-service.d.ts +4 -4
- package/dist/entry-tools/validate-project-service.d.ts +153 -16
- package/dist/entry-tools/validate-project-service.js +442 -25
- package/dist/gradle-paths.d.ts +4 -0
- package/dist/gradle-paths.js +57 -0
- package/dist/index.js +148 -30
- package/dist/lru-list.d.ts +31 -0
- package/dist/lru-list.js +102 -0
- package/dist/mapping-pipeline-service.d.ts +12 -1
- package/dist/mapping-pipeline-service.js +28 -1
- package/dist/mapping-service.d.ts +16 -0
- package/dist/mapping-service.js +405 -68
- package/dist/minecraft-explorer-service.d.ts +13 -0
- package/dist/minecraft-explorer-service.js +8 -4
- package/dist/mixin-validator.d.ts +33 -2
- package/dist/mixin-validator.js +218 -17
- package/dist/mod-analyzer.d.ts +1 -0
- package/dist/mod-analyzer.js +17 -1
- package/dist/mod-decompile-service.js +4 -4
- package/dist/mod-remap-service.js +1 -54
- package/dist/mod-search-service.d.ts +1 -0
- package/dist/mod-search-service.js +84 -51
- package/dist/observability.d.ts +18 -1
- package/dist/observability.js +44 -1
- package/dist/response-utils.d.ts +69 -0
- package/dist/response-utils.js +227 -0
- package/dist/source-jar-reader.d.ts +16 -0
- package/dist/source-jar-reader.js +103 -1
- package/dist/source-resolver.d.ts +9 -1
- package/dist/source-resolver.js +23 -16
- package/dist/source-service.d.ts +119 -3
- package/dist/source-service.js +1836 -218
- package/dist/storage/artifacts-repo.d.ts +4 -1
- package/dist/storage/artifacts-repo.js +33 -5
- package/dist/storage/files-repo.d.ts +0 -2
- package/dist/storage/files-repo.js +0 -11
- package/dist/storage/migrations.d.ts +1 -1
- package/dist/storage/migrations.js +10 -2
- package/dist/storage/schema.d.ts +2 -0
- package/dist/storage/schema.js +25 -0
- package/dist/tool-contract-manifest.js +8 -6
- package/dist/types.d.ts +20 -0
- package/dist/workspace-mapping-service.d.ts +13 -0
- package/dist/workspace-mapping-service.js +146 -14
- package/package.json +3 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
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 (
|
|
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
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
146
|
-
// File might not exist at the expected path, skip
|
|
139
|
+
if (fileResult.hits.length === 0) {
|
|
147
140
|
continue;
|
|
148
141
|
}
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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();
|
package/dist/observability.d.ts
CHANGED
|
@@ -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;
|
package/dist/observability.js
CHANGED
|
@@ -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
|
+
}>;
|