@adhisang/minecraft-modding-mcp 4.2.1 → 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.
- package/CHANGELOG.md +69 -0
- package/README.md +5 -4
- package/dist/access-transformer-parser.d.ts +2 -1
- package/dist/access-transformer-parser.js +10 -1
- package/dist/access-widener-parser.d.ts +1 -0
- package/dist/access-widener-parser.js +6 -3
- package/dist/build-suggested-call.js +31 -10
- package/dist/cache-registry.d.ts +8 -0
- package/dist/cache-registry.js +50 -32
- package/dist/config.d.ts +1 -0
- package/dist/config.js +4 -0
- package/dist/decompiler/vineflower.js +8 -2
- package/dist/entry-tools/analyze-mod-service.d.ts +24 -24
- package/dist/entry-tools/analyze-mod-service.js +2 -1
- package/dist/entry-tools/analyze-symbol-service.d.ts +57 -22
- package/dist/entry-tools/analyze-symbol-service.js +139 -34
- package/dist/entry-tools/batch-class-members-service.d.ts +3 -1
- package/dist/entry-tools/batch-class-members-service.js +6 -6
- package/dist/entry-tools/batch-class-source-service.d.ts +3 -1
- package/dist/entry-tools/batch-class-source-service.js +5 -6
- package/dist/entry-tools/batch-mappings-service.d.ts +3 -1
- package/dist/entry-tools/batch-mappings-service.js +4 -5
- package/dist/entry-tools/batch-symbol-exists-service.d.ts +3 -1
- package/dist/entry-tools/batch-symbol-exists-service.js +4 -5
- package/dist/entry-tools/compare-minecraft-service.d.ts +10 -10
- package/dist/entry-tools/compare-minecraft-service.js +1 -1
- package/dist/entry-tools/entry-tool-schema.d.ts +1 -1
- package/dist/entry-tools/inspect-minecraft/handlers/artifact.js +1 -3
- package/dist/entry-tools/inspect-minecraft/handlers/class-members.js +4 -3
- package/dist/entry-tools/inspect-minecraft/handlers/class-overview.js +0 -5
- package/dist/entry-tools/inspect-minecraft/handlers/class-source.js +0 -1
- package/dist/entry-tools/inspect-minecraft/handlers/file.js +26 -2
- package/dist/entry-tools/inspect-minecraft/handlers/list-files.js +0 -1
- package/dist/entry-tools/inspect-minecraft/handlers/search.d.ts +8 -0
- package/dist/entry-tools/inspect-minecraft/handlers/search.js +13 -2
- package/dist/entry-tools/inspect-minecraft/handlers/versions.d.ts +1 -1
- package/dist/entry-tools/inspect-minecraft/handlers/versions.js +3 -2
- package/dist/entry-tools/inspect-minecraft/internal.d.ts +90 -89
- package/dist/entry-tools/inspect-minecraft/internal.js +32 -14
- package/dist/entry-tools/inspect-minecraft-service.d.ts +218 -218
- package/dist/entry-tools/inspect-minecraft-service.js +1 -1
- package/dist/entry-tools/manage-cache-service.d.ts +4 -4
- package/dist/entry-tools/response-contract.d.ts +3 -1
- package/dist/entry-tools/response-contract.js +3 -2
- package/dist/entry-tools/validate-project/cases/mixin.js +2 -1
- package/dist/entry-tools/validate-project/cases/project-summary.js +5 -3
- package/dist/entry-tools/validate-project-service.d.ts +42 -36
- package/dist/entry-tools/validate-project-service.js +3 -1
- package/dist/entry-tools/verify-mixin-target-service.d.ts +30 -0
- package/dist/entry-tools/verify-mixin-target-service.js +120 -29
- package/dist/error-mapping.d.ts +51 -0
- package/dist/error-mapping.js +157 -2
- package/dist/index.js +147 -76
- package/dist/java-process.js +1 -1
- package/dist/json-rpc-framing.d.ts +4 -0
- package/dist/json-rpc-framing.js +23 -1
- package/dist/mapping/internal-types.d.ts +24 -0
- package/dist/mapping/loaders/tiny-maven.js +46 -20
- package/dist/mapping/lookup.d.ts +13 -2
- package/dist/mapping/lookup.js +60 -2
- package/dist/mapping/types.d.ts +6 -0
- package/dist/mapping-service.d.ts +26 -2
- package/dist/mapping-service.js +212 -55
- package/dist/mcp-helpers.d.ts +1 -0
- package/dist/mcp-helpers.js +25 -3
- package/dist/minecraft-explorer-service.js +24 -11
- package/dist/mixin/access-validators.js +33 -1
- package/dist/mixin/annotation-validators.js +73 -9
- package/dist/mixin/parsed-validator.js +3 -2
- package/dist/mixin-parser.js +119 -7
- package/dist/mod-decompile-service.d.ts +2 -0
- package/dist/mod-decompile-service.js +21 -3
- package/dist/mod-remap-service.d.ts +20 -1
- package/dist/mod-remap-service.js +101 -45
- package/dist/mojang-tiny-mapping-service.js +5 -2
- package/dist/nbt/java-nbt-codec.js +46 -16
- package/dist/nbt/json-patch.js +51 -12
- package/dist/nbt/typed-json.d.ts +1 -0
- package/dist/nbt/typed-json.js +12 -2
- package/dist/nbt/types.d.ts +2 -2
- package/dist/observability.d.ts +6 -0
- package/dist/observability.js +10 -0
- package/dist/page-cursor.d.ts +17 -0
- package/dist/page-cursor.js +0 -0
- package/dist/path-converter.js +10 -3
- package/dist/registry-service.d.ts +2 -0
- package/dist/registry-service.js +41 -2
- package/dist/repo-downloader.js +4 -3
- package/dist/resources.js +45 -9
- package/dist/response-utils.d.ts +29 -4
- package/dist/response-utils.js +95 -5
- package/dist/source/access-validate.js +12 -2
- package/dist/source/artifact-resolver.js +2 -2
- package/dist/source/class-source/members-builder.d.ts +44 -1
- package/dist/source/class-source/members-builder.js +51 -4
- package/dist/source/class-source/snippet-builder.d.ts +13 -0
- package/dist/source/class-source/snippet-builder.js +54 -4
- package/dist/source/class-source-helpers.d.ts +4 -4
- package/dist/source/class-source-helpers.js +77 -20
- package/dist/source/class-source.d.ts +19 -0
- package/dist/source/class-source.js +144 -40
- package/dist/source/file-access.js +3 -2
- package/dist/source/indexer.js +29 -9
- package/dist/source/lifecycle/mapping-helpers.js +28 -3
- package/dist/source/lifecycle/runtime-check.js +20 -6
- package/dist/source/search.d.ts +1 -1
- package/dist/source/search.js +117 -30
- package/dist/source/validate-mixin/pipeline/resolve.js +23 -3
- package/dist/source/validate-mixin.d.ts +2 -1
- package/dist/source/validate-mixin.js +45 -4
- package/dist/source/workspace-target.js +17 -10
- package/dist/source-jar-reader.d.ts +54 -1
- package/dist/source-jar-reader.js +121 -10
- package/dist/source-resolver.js +2 -2
- package/dist/source-service.d.ts +28 -7
- package/dist/source-service.js +9 -1
- package/dist/stdio-supervisor.d.ts +2 -2
- package/dist/stdio-supervisor.js +29 -9
- package/dist/storage/db.js +24 -1
- package/dist/storage/files-repo.d.ts +15 -0
- package/dist/storage/files-repo.js +67 -37
- package/dist/storage/symbols-repo.d.ts +15 -0
- package/dist/storage/symbols-repo.js +53 -19
- package/dist/symbols/symbol-extractor.js +35 -6
- package/dist/text-truncate.d.ts +7 -0
- package/dist/text-truncate.js +21 -0
- package/dist/tiny-remapper-resolver.js +10 -0
- package/dist/tool-contract-manifest.js +2 -2
- package/dist/tool-execution-gate.js +12 -1
- package/dist/tool-guidance.d.ts +6 -2
- package/dist/tool-guidance.js +63 -13
- package/dist/tool-schemas.d.ts +561 -496
- package/dist/tool-schemas.js +175 -139
- package/dist/types.d.ts +1 -0
- package/dist/version-diff-service.js +5 -0
- package/dist/version-service.d.ts +3 -0
- package/dist/version-service.js +18 -2
- package/dist/warning-details.d.ts +30 -0
- package/dist/warning-details.js +80 -0
- package/dist/workspace-mapping-service.js +9 -0
- package/docs/README-ja.md +2 -2
- package/docs/examples.md +26 -2
- package/docs/tool-reference.md +22 -17
- package/package.json +1 -1
|
@@ -67,18 +67,21 @@ function parseSearchCursor(cursor) {
|
|
|
67
67
|
});
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
|
-
function nextCursorFromRows(rows) {
|
|
71
|
-
if (rows.length === 0) {
|
|
70
|
+
function nextCursorFromRows(rows, limit) {
|
|
71
|
+
if (rows.length === 0 || rows.length < limit) {
|
|
72
72
|
return undefined;
|
|
73
73
|
}
|
|
74
74
|
const last = rows[rows.length - 1];
|
|
75
75
|
return buildCursor(last.file_path);
|
|
76
76
|
}
|
|
77
|
+
function compareFilePaths(left, right) {
|
|
78
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
79
|
+
}
|
|
77
80
|
function compareSearchOrdering(left, right) {
|
|
78
81
|
if (right.score !== left.score) {
|
|
79
82
|
return right.score - left.score;
|
|
80
83
|
}
|
|
81
|
-
return left.filePath
|
|
84
|
+
return compareFilePaths(left.filePath, right.filePath);
|
|
82
85
|
}
|
|
83
86
|
function isAfterSearchCursor(hit, cursor) {
|
|
84
87
|
if (hit.score < cursor.score) {
|
|
@@ -87,7 +90,7 @@ function isAfterSearchCursor(hit, cursor) {
|
|
|
87
90
|
if (hit.score > cursor.score) {
|
|
88
91
|
return false;
|
|
89
92
|
}
|
|
90
|
-
return hit.filePath
|
|
93
|
+
return compareFilePaths(hit.filePath, cursor.filePath) > 0;
|
|
91
94
|
}
|
|
92
95
|
function buildPreview(content, query) {
|
|
93
96
|
const normalizedQuery = query.toLowerCase();
|
|
@@ -121,6 +124,10 @@ function buildIndexedMatchQuery(query, match) {
|
|
|
121
124
|
return token;
|
|
122
125
|
}).join(" ");
|
|
123
126
|
}
|
|
127
|
+
/** Escape LIKE wildcards so the needle is matched literally under ESCAPE '\'. */
|
|
128
|
+
function escapeLikeNeedle(value) {
|
|
129
|
+
return value.replace(/[\\%_]/g, "\\$&");
|
|
130
|
+
}
|
|
124
131
|
export class FilesRepo {
|
|
125
132
|
db;
|
|
126
133
|
deleteStmt;
|
|
@@ -129,9 +136,14 @@ export class FilesRepo {
|
|
|
129
136
|
listStmt;
|
|
130
137
|
listRowsStmt;
|
|
131
138
|
searchPathStmt;
|
|
139
|
+
searchContentLikePathsStmt;
|
|
132
140
|
searchFtsStmt;
|
|
133
141
|
getByPathsStmtCache = new Map();
|
|
134
142
|
classLookupStmtCache = new Map();
|
|
143
|
+
// Fixed-SQL statements built outside the constructor; cached (LRU by SQL text)
|
|
144
|
+
// so hot paths do not re-prepare per call.
|
|
145
|
+
dynamicStmtCache = new Map();
|
|
146
|
+
static DYNAMIC_STMT_CACHE_MAX = 64;
|
|
135
147
|
constructor(db) {
|
|
136
148
|
this.db = db;
|
|
137
149
|
this.deleteStmt = this.db.prepare(`
|
|
@@ -150,14 +162,14 @@ export class FilesRepo {
|
|
|
150
162
|
this.listStmt = this.db.prepare(`
|
|
151
163
|
SELECT artifact_id, file_path
|
|
152
164
|
FROM files
|
|
153
|
-
WHERE artifact_id = ? AND file_path > ? AND (? IS NULL OR file_path LIKE ?
|
|
165
|
+
WHERE artifact_id = ? AND file_path > ? AND (? IS NULL OR file_path LIKE ? ESCAPE '\\')
|
|
154
166
|
ORDER BY file_path ASC
|
|
155
167
|
LIMIT ?
|
|
156
168
|
`);
|
|
157
169
|
this.listRowsStmt = this.db.prepare(`
|
|
158
170
|
SELECT artifact_id, file_path, content, content_bytes, content_hash
|
|
159
171
|
FROM files
|
|
160
|
-
WHERE artifact_id = ? AND file_path > ? AND (? IS NULL OR file_path LIKE ?
|
|
172
|
+
WHERE artifact_id = ? AND file_path > ? AND (? IS NULL OR file_path LIKE ? ESCAPE '\\')
|
|
161
173
|
ORDER BY file_path ASC
|
|
162
174
|
LIMIT ?
|
|
163
175
|
`);
|
|
@@ -167,6 +179,13 @@ export class FilesRepo {
|
|
|
167
179
|
WHERE artifact_id = ? AND file_path LIKE ? ESCAPE '\\'
|
|
168
180
|
ORDER BY file_path ASC
|
|
169
181
|
LIMIT ?
|
|
182
|
+
`);
|
|
183
|
+
this.searchContentLikePathsStmt = this.db.prepare(`
|
|
184
|
+
SELECT file_path
|
|
185
|
+
FROM files
|
|
186
|
+
WHERE artifact_id = ? AND content LIKE ? ESCAPE '\\'
|
|
187
|
+
ORDER BY file_path ASC
|
|
188
|
+
LIMIT ?
|
|
170
189
|
`);
|
|
171
190
|
this.searchFtsStmt = this.db.prepare(`
|
|
172
191
|
SELECT file_path, rank
|
|
@@ -176,6 +195,23 @@ export class FilesRepo {
|
|
|
176
195
|
LIMIT ?
|
|
177
196
|
`);
|
|
178
197
|
}
|
|
198
|
+
prepareCached(sql) {
|
|
199
|
+
const cached = this.dynamicStmtCache.get(sql);
|
|
200
|
+
if (cached) {
|
|
201
|
+
// Refresh recency so hot statements survive eviction.
|
|
202
|
+
this.dynamicStmtCache.delete(sql);
|
|
203
|
+
this.dynamicStmtCache.set(sql, cached);
|
|
204
|
+
return cached;
|
|
205
|
+
}
|
|
206
|
+
const stmt = this.db.prepare(sql);
|
|
207
|
+
this.dynamicStmtCache.set(sql, stmt);
|
|
208
|
+
if (this.dynamicStmtCache.size > FilesRepo.DYNAMIC_STMT_CACHE_MAX) {
|
|
209
|
+
const oldest = this.dynamicStmtCache.keys().next().value;
|
|
210
|
+
if (oldest !== undefined)
|
|
211
|
+
this.dynamicStmtCache.delete(oldest);
|
|
212
|
+
}
|
|
213
|
+
return stmt;
|
|
214
|
+
}
|
|
179
215
|
clearFilesForArtifact(artifactId) {
|
|
180
216
|
this.deleteStmt.run([artifactId]);
|
|
181
217
|
}
|
|
@@ -219,15 +255,15 @@ export class FilesRepo {
|
|
|
219
255
|
}
|
|
220
256
|
listFiles(artifactId, options) {
|
|
221
257
|
const cursor = parseCursor(options.cursor);
|
|
222
|
-
const rows = this.listStmt.all(artifactId, cursor?.sortKey ?? "", options.prefix ?? null, options.prefix
|
|
258
|
+
const rows = this.listStmt.all(artifactId, cursor?.sortKey ?? "", options.prefix ?? null, options.prefix != null ? `${escapeLikeNeedle(options.prefix)}%` : "", Math.max(1, options.limit));
|
|
223
259
|
return {
|
|
224
260
|
items: rows.map((row) => row.file_path),
|
|
225
|
-
nextCursor: nextCursorFromRows(rows)
|
|
261
|
+
nextCursor: nextCursorFromRows(rows, Math.max(1, options.limit))
|
|
226
262
|
};
|
|
227
263
|
}
|
|
228
264
|
listFileRows(artifactId, options) {
|
|
229
265
|
const cursor = parseCursor(options.cursor);
|
|
230
|
-
const rows = this.listRowsStmt.all(artifactId, cursor?.sortKey ?? "", options.prefix ?? null, options.prefix
|
|
266
|
+
const rows = this.listRowsStmt.all(artifactId, cursor?.sortKey ?? "", options.prefix ?? null, options.prefix != null ? `${escapeLikeNeedle(options.prefix)}%` : "", Math.max(1, options.limit));
|
|
231
267
|
return {
|
|
232
268
|
items: rows.map((row) => ({
|
|
233
269
|
artifactId: row.artifact_id,
|
|
@@ -236,9 +272,21 @@ export class FilesRepo {
|
|
|
236
272
|
contentBytes: row.content_bytes,
|
|
237
273
|
contentHash: row.content_hash
|
|
238
274
|
})),
|
|
239
|
-
nextCursor: nextCursorFromRows(rows)
|
|
275
|
+
nextCursor: nextCursorFromRows(rows, Math.max(1, options.limit))
|
|
240
276
|
};
|
|
241
277
|
}
|
|
278
|
+
/**
|
|
279
|
+
* Narrow the literal/contains text-scan candidate set without hydrating content:
|
|
280
|
+
* returns the file_paths whose content matches `content LIKE %needle%` (ASCII
|
|
281
|
+
* case-insensitive). The result is a SUPERSET of the JS contains/exact match set
|
|
282
|
+
* for an ASCII needle, so callers MUST still run the exact JS post-verify. Only
|
|
283
|
+
* file_path is selected — content stays in the DB until getFileContentsByPaths.
|
|
284
|
+
*/
|
|
285
|
+
searchContentLikeCandidatePaths(artifactId, needle, limit) {
|
|
286
|
+
const pattern = `%${escapeLikeNeedle(needle)}%`;
|
|
287
|
+
const rows = this.searchContentLikePathsStmt.all(artifactId, pattern, Math.max(1, limit));
|
|
288
|
+
return { filePaths: rows.map((row) => row.file_path), scannedRows: rows.length };
|
|
289
|
+
}
|
|
242
290
|
getFileContentsByPaths(artifactId, filePaths) {
|
|
243
291
|
if (filePaths.length === 0) {
|
|
244
292
|
return [];
|
|
@@ -264,7 +312,7 @@ export class FilesRepo {
|
|
|
264
312
|
return { items: [], nextCursor: undefined, scannedRows: 0, dbRoundtrips: 0 };
|
|
265
313
|
}
|
|
266
314
|
const cursor = parseSearchCursor(options.cursor);
|
|
267
|
-
const likeQuery = `%${normalized}%`;
|
|
315
|
+
const likeQuery = `%${escapeLikeNeedle(normalized)}%`;
|
|
268
316
|
const ftsQuery = buildIndexedMatchQuery(normalized, options.match);
|
|
269
317
|
const mode = options.mode ?? "mixed";
|
|
270
318
|
// Cursor-adaptive fetch limit: when no cursor, use a generous limit;
|
|
@@ -274,28 +322,11 @@ export class FilesRepo {
|
|
|
274
322
|
const fetchLimit = baseFetchLimit;
|
|
275
323
|
// When cursor score is below all possible bands, skip both queries
|
|
276
324
|
const cursorExhausted = cursor != null && cursor.score < 100;
|
|
277
|
-
|
|
278
|
-
const cursorPastPath = cursor != null && cursor.score < 120;
|
|
279
|
-
const includePath = mode !== "text" && !cursorExhausted && !cursorPastPath;
|
|
325
|
+
const includePath = mode !== "text" && !cursorExhausted;
|
|
280
326
|
const includeContent = mode !== "path" && !cursorExhausted;
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
// Cursor is within the path tier — only fetch paths after cursor.filePath
|
|
285
|
-
pathRows = this.db.prepare(`
|
|
286
|
-
SELECT file_path
|
|
287
|
-
FROM files
|
|
288
|
-
WHERE artifact_id = ? AND file_path LIKE ? ESCAPE '\\' AND file_path > ?
|
|
289
|
-
ORDER BY file_path ASC
|
|
290
|
-
LIMIT ?
|
|
291
|
-
`).all(artifactId, likeQuery, cursor.filePath, fetchLimit);
|
|
292
|
-
}
|
|
293
|
-
else if (includePath) {
|
|
294
|
-
pathRows = this.searchPathStmt.all(artifactId, likeQuery, fetchLimit);
|
|
295
|
-
}
|
|
296
|
-
else {
|
|
297
|
-
pathRows = [];
|
|
298
|
-
}
|
|
327
|
+
const pathRows = includePath
|
|
328
|
+
? this.searchPathStmt.all(artifactId, likeQuery, fetchLimit)
|
|
329
|
+
: [];
|
|
299
330
|
const merged = pathRows.map((row) => ({
|
|
300
331
|
filePath: row.file_path,
|
|
301
332
|
score: 120,
|
|
@@ -393,7 +424,7 @@ export class FilesRepo {
|
|
|
393
424
|
return 0;
|
|
394
425
|
}
|
|
395
426
|
try {
|
|
396
|
-
const row = this.
|
|
427
|
+
const row = this.prepareCached(`SELECT COUNT(*) AS cnt FROM files_fts WHERE artifact_id = ? AND files_fts MATCH ?`).get(artifactId, ftsQuery);
|
|
397
428
|
return row?.cnt ?? 0;
|
|
398
429
|
}
|
|
399
430
|
catch {
|
|
@@ -409,8 +440,8 @@ export class FilesRepo {
|
|
|
409
440
|
if (!normalized) {
|
|
410
441
|
return 0;
|
|
411
442
|
}
|
|
412
|
-
const likeQuery = `%${normalized}%`;
|
|
413
|
-
const row = this.
|
|
443
|
+
const likeQuery = `%${escapeLikeNeedle(normalized)}%`;
|
|
444
|
+
const row = this.prepareCached(`SELECT COUNT(*) AS cnt FROM files WHERE artifact_id = ? AND file_path LIKE ? ESCAPE '\\'`).get(artifactId, likeQuery);
|
|
414
445
|
return row?.cnt ?? 0;
|
|
415
446
|
}
|
|
416
447
|
findFirstFilePathByName(artifactId, fileName) {
|
|
@@ -418,8 +449,7 @@ export class FilesRepo {
|
|
|
418
449
|
if (!normalized) {
|
|
419
450
|
return undefined;
|
|
420
451
|
}
|
|
421
|
-
const row = this.
|
|
422
|
-
.prepare(`
|
|
452
|
+
const row = this.prepareCached(`
|
|
423
453
|
SELECT file_path
|
|
424
454
|
FROM files
|
|
425
455
|
WHERE artifact_id = ?
|
|
@@ -22,6 +22,12 @@ export interface FindScopedSymbolsOptions {
|
|
|
22
22
|
query: string;
|
|
23
23
|
match: ScopedSymbolMatch;
|
|
24
24
|
symbolKind?: string;
|
|
25
|
+
/**
|
|
26
|
+
* Restrict results to these symbol kinds via a bound `symbol_kind IN (...)` clause.
|
|
27
|
+
* Combined with the singular `symbolKind` it ANDs (both apply). An empty array is
|
|
28
|
+
* ignored (degrades to no kind filter) to avoid emitting an invalid `IN ()`.
|
|
29
|
+
*/
|
|
30
|
+
symbolKinds?: string[];
|
|
25
31
|
packagePrefix?: string;
|
|
26
32
|
filePathLike?: string;
|
|
27
33
|
limit?: number;
|
|
@@ -40,12 +46,21 @@ export declare class SymbolsRepo {
|
|
|
40
46
|
private readonly listByArtifactStmt;
|
|
41
47
|
private readonly listByArtifactKindStmt;
|
|
42
48
|
private readonly listByFileStmt;
|
|
49
|
+
private readonly dynamicStmtCache;
|
|
50
|
+
private static readonly DYNAMIC_STMT_CACHE_MAX;
|
|
43
51
|
constructor(db: SqliteDatabase);
|
|
52
|
+
private prepareCached;
|
|
44
53
|
clearSymbolsForArtifact(artifactId: string): void;
|
|
45
54
|
insertSymbolsForArtifact(artifactId: string, symbols: IndexedSymbol[]): void;
|
|
46
55
|
replaceSymbolsForArtifact(artifactId: string, symbols: IndexedSymbol[]): void;
|
|
47
56
|
findSymbols(options: FindSymbolsOptions): PagedResult<SymbolRow>;
|
|
48
57
|
listSymbolsForArtifact(artifactId: string, symbolKind?: string): SymbolRow[];
|
|
58
|
+
/**
|
|
59
|
+
* Streaming variant of listSymbolsForArtifact: rows are mapped lazily so callers
|
|
60
|
+
* that filter (e.g. regex symbol search) never materialize the full symbol set.
|
|
61
|
+
* Do not run other statements on this connection while consuming the iterator.
|
|
62
|
+
*/
|
|
63
|
+
iterateSymbolsForArtifact(artifactId: string, symbolKind?: string): IterableIterator<SymbolRow>;
|
|
49
64
|
listSymbolsForFile(artifactId: string, filePath: string): SymbolRow[];
|
|
50
65
|
listSymbolsForFiles(artifactId: string, filePaths: string[], symbolKind?: string): Map<string, SymbolRow[]>;
|
|
51
66
|
findBySymbolNames(artifactId: string, symbolNames: string[]): SymbolRow[];
|
|
@@ -60,6 +60,10 @@ export class SymbolsRepo {
|
|
|
60
60
|
listByArtifactStmt;
|
|
61
61
|
listByArtifactKindStmt;
|
|
62
62
|
listByFileStmt;
|
|
63
|
+
// Dynamic WHERE-clause builders produce a small bounded set of SQL strings;
|
|
64
|
+
// cache their prepared statements (LRU by SQL text) instead of re-preparing per call.
|
|
65
|
+
dynamicStmtCache = new Map();
|
|
66
|
+
static DYNAMIC_STMT_CACHE_MAX = 64;
|
|
63
67
|
constructor(db) {
|
|
64
68
|
this.db = db;
|
|
65
69
|
this.deleteStmt = this.db.prepare(`
|
|
@@ -106,6 +110,23 @@ export class SymbolsRepo {
|
|
|
106
110
|
ORDER BY line ASC, symbol_name ASC
|
|
107
111
|
`);
|
|
108
112
|
}
|
|
113
|
+
prepareCached(sql) {
|
|
114
|
+
const cached = this.dynamicStmtCache.get(sql);
|
|
115
|
+
if (cached) {
|
|
116
|
+
// Refresh recency so hot statements survive eviction.
|
|
117
|
+
this.dynamicStmtCache.delete(sql);
|
|
118
|
+
this.dynamicStmtCache.set(sql, cached);
|
|
119
|
+
return cached;
|
|
120
|
+
}
|
|
121
|
+
const stmt = this.db.prepare(sql);
|
|
122
|
+
this.dynamicStmtCache.set(sql, stmt);
|
|
123
|
+
if (this.dynamicStmtCache.size > SymbolsRepo.DYNAMIC_STMT_CACHE_MAX) {
|
|
124
|
+
const oldest = this.dynamicStmtCache.keys().next().value;
|
|
125
|
+
if (oldest !== undefined)
|
|
126
|
+
this.dynamicStmtCache.delete(oldest);
|
|
127
|
+
}
|
|
128
|
+
return stmt;
|
|
129
|
+
}
|
|
109
130
|
clearSymbolsForArtifact(artifactId) {
|
|
110
131
|
this.deleteStmt.run([artifactId]);
|
|
111
132
|
}
|
|
@@ -150,7 +171,7 @@ export class SymbolsRepo {
|
|
|
150
171
|
LIMIT ?
|
|
151
172
|
`;
|
|
152
173
|
const nameParam = options.exact ? symbolName : `${symbolName}%`;
|
|
153
|
-
rows = this.
|
|
174
|
+
rows = this.prepareCached(sql).all(options.artifactId, options.symbolKind ?? "", options.symbolKind ?? "", nameParam, cursor.symbolName, cursor.symbolName, cursor.filePath, cursor.symbolName, cursor.filePath, cursor.line, Math.max(1, fetchLimit));
|
|
154
175
|
}
|
|
155
176
|
else {
|
|
156
177
|
const queryRows = options.exact ? this.searchExactStmt : this.searchStmt;
|
|
@@ -173,16 +194,26 @@ export class SymbolsRepo {
|
|
|
173
194
|
};
|
|
174
195
|
}
|
|
175
196
|
listSymbolsForArtifact(artifactId, symbolKind) {
|
|
197
|
+
return [...this.iterateSymbolsForArtifact(artifactId, symbolKind)];
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Streaming variant of listSymbolsForArtifact: rows are mapped lazily so callers
|
|
201
|
+
* that filter (e.g. regex symbol search) never materialize the full symbol set.
|
|
202
|
+
* Do not run other statements on this connection while consuming the iterator.
|
|
203
|
+
*/
|
|
204
|
+
*iterateSymbolsForArtifact(artifactId, symbolKind) {
|
|
176
205
|
const rows = (symbolKind
|
|
177
|
-
? this.listByArtifactKindStmt.
|
|
178
|
-
: this.listByArtifactStmt.
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
206
|
+
? this.listByArtifactKindStmt.iterate(artifactId, symbolKind)
|
|
207
|
+
: this.listByArtifactStmt.iterate(artifactId));
|
|
208
|
+
for (const row of rows) {
|
|
209
|
+
yield toSymbolRow(artifactId, {
|
|
210
|
+
filePath: row.file_path,
|
|
211
|
+
symbolKind: row.symbol_kind,
|
|
212
|
+
symbolName: row.symbol_name,
|
|
213
|
+
qualifiedName: row.qualified_name ?? undefined,
|
|
214
|
+
line: row.line
|
|
215
|
+
});
|
|
216
|
+
}
|
|
186
217
|
}
|
|
187
218
|
listSymbolsForFile(artifactId, filePath) {
|
|
188
219
|
const rows = this.listByFileStmt.all(artifactId, filePath);
|
|
@@ -209,7 +240,7 @@ export class SymbolsRepo {
|
|
|
209
240
|
ORDER BY file_path ASC, line ASC, symbol_name ASC
|
|
210
241
|
`;
|
|
211
242
|
const params = symbolKind ? [artifactId, ...uniqueFilePaths, symbolKind] : [artifactId, ...uniqueFilePaths];
|
|
212
|
-
const rows = this.
|
|
243
|
+
const rows = this.prepareCached(sql).all(...params);
|
|
213
244
|
const byFile = new Map();
|
|
214
245
|
for (const row of rows) {
|
|
215
246
|
const bucket = byFile.get(row.file_path) ?? [];
|
|
@@ -230,8 +261,7 @@ export class SymbolsRepo {
|
|
|
230
261
|
return [];
|
|
231
262
|
}
|
|
232
263
|
const placeholders = unique.map(() => "?").join(", ");
|
|
233
|
-
const rows = this.
|
|
234
|
-
.prepare(`
|
|
264
|
+
const rows = this.prepareCached(`
|
|
235
265
|
SELECT file_path, symbol_kind, symbol_name, qualified_name, line
|
|
236
266
|
FROM symbols
|
|
237
267
|
WHERE artifact_id = ?
|
|
@@ -260,6 +290,12 @@ export class SymbolsRepo {
|
|
|
260
290
|
where.push("symbol_kind = ?");
|
|
261
291
|
params.push(options.symbolKind);
|
|
262
292
|
}
|
|
293
|
+
const symbolKinds = options.symbolKinds;
|
|
294
|
+
if (symbolKinds && symbolKinds.length > 0) {
|
|
295
|
+
const placeholders = symbolKinds.map(() => "?").join(", ");
|
|
296
|
+
where.push(`symbol_kind IN (${placeholders})`);
|
|
297
|
+
params.push(...symbolKinds);
|
|
298
|
+
}
|
|
263
299
|
if (options.packagePrefix?.trim()) {
|
|
264
300
|
const normalizedPrefix = options.packagePrefix.replace(/\.+/g, "/").replace(/\/+$/, "");
|
|
265
301
|
where.push("file_path LIKE ? ESCAPE '\\'");
|
|
@@ -300,7 +336,7 @@ export class SymbolsRepo {
|
|
|
300
336
|
ORDER BY symbol_name ASC, file_path ASC, line ASC
|
|
301
337
|
LIMIT ?
|
|
302
338
|
`;
|
|
303
|
-
const rows = this.
|
|
339
|
+
const rows = this.prepareCached(sql).all(...params);
|
|
304
340
|
const hasMore = rows.length > rawLimit;
|
|
305
341
|
const page = hasMore ? rows.slice(0, rawLimit) : rows;
|
|
306
342
|
const lastRow = page.length > 0 ? page[page.length - 1] : undefined;
|
|
@@ -354,12 +390,11 @@ export class SymbolsRepo {
|
|
|
354
390
|
WHERE artifact_id = ?
|
|
355
391
|
${where.length > 0 ? `AND ${where.join("\n AND ")}` : ""}
|
|
356
392
|
`;
|
|
357
|
-
const row = this.
|
|
393
|
+
const row = this.prepareCached(sql).get(...params);
|
|
358
394
|
return row?.cnt ?? 0;
|
|
359
395
|
}
|
|
360
396
|
listDistinctFilePathsByKind(artifactId, symbolKind) {
|
|
361
|
-
const rows = this.
|
|
362
|
-
.prepare(`
|
|
397
|
+
const rows = this.prepareCached(`
|
|
363
398
|
SELECT DISTINCT file_path
|
|
364
399
|
FROM symbols
|
|
365
400
|
WHERE artifact_id = ? AND symbol_kind = ?
|
|
@@ -374,8 +409,7 @@ export class SymbolsRepo {
|
|
|
374
409
|
if (!normalizedClassName || !normalizedSimpleName) {
|
|
375
410
|
return undefined;
|
|
376
411
|
}
|
|
377
|
-
const row = this.
|
|
378
|
-
.prepare(`
|
|
412
|
+
const row = this.prepareCached(`
|
|
379
413
|
SELECT file_path
|
|
380
414
|
FROM symbols
|
|
381
415
|
WHERE artifact_id = ?
|
|
@@ -1,6 +1,26 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
const
|
|
1
|
+
// A type token may carry spaced generics (Map<String, Integer>), arrays (int[][]),
|
|
2
|
+
// wildcards (List<? extends Foo>), and FQNs (java.util.Map). It must not contain "=".
|
|
3
|
+
const TYPE_TOKEN = "[\\w.$][\\w.$<>\\[\\],?\\s]*";
|
|
4
|
+
const MODIFIER = "(?:public|private|protected|abstract|final|static|native|synchronized|default|strictfp|transient|volatile|sealed|non-sealed)";
|
|
5
|
+
const MODIFIER_OR_ANNOTATION_RUN = `(?:(?:@[\\w.]+|${MODIFIER})\\s+)*`;
|
|
6
|
+
const TYPE_PARAMS = "(?:<[^>]+>\\s*)?";
|
|
7
|
+
const THROWS_CLAUSE = "(?:\\s+throws\\s+[\\w.$,\\s]+)?";
|
|
8
|
+
const CLASS_DECLARATION = new RegExp(`^${MODIFIER_OR_ANNOTATION_RUN}(class|interface|enum|record)\\s+([A-Za-z_$][\\w$]*)`);
|
|
9
|
+
// Method/constructor with a body: return type is optional (constructors have none); ends with "{".
|
|
10
|
+
const METHOD_BLOCK_DECLARATION = new RegExp(`^${MODIFIER_OR_ANNOTATION_RUN}${TYPE_PARAMS}(?:${TYPE_TOKEN}\\s+)?([A-Za-z_$][\\w$]*)\\s*\\([^)]*\\)${THROWS_CLAUSE}\\s*\\{`);
|
|
11
|
+
// Abstract/interface method declaration: a return type is REQUIRED; ends with ";".
|
|
12
|
+
// Requiring the return type is what separates "void onTick();" from a bare call "doThing();".
|
|
13
|
+
const METHOD_ABSTRACT_DECLARATION = new RegExp(`^${MODIFIER_OR_ANNOTATION_RUN}${TYPE_PARAMS}${TYPE_TOKEN}\\s+([A-Za-z_$][\\w$]*)\\s*\\([^)]*\\)${THROWS_CLAUSE}\\s*;`);
|
|
14
|
+
const FIELD_DECLARATION = new RegExp(`^${MODIFIER_OR_ANNOTATION_RUN}${TYPE_TOKEN}\\s+([A-Za-z_$][\\w$]*)\\s*(?:=|;|,)`);
|
|
15
|
+
// Lines that begin with one of these keywords are statements, not declarations. They are
|
|
16
|
+
// skipped wholesale so that "return helper();" / "for (...)" / "new Foo() {" never produce
|
|
17
|
+
// phantom members. "default"/"synchronized" are intentionally excluded (they double as
|
|
18
|
+
// member modifiers); the method regexes reject their statement forms structurally.
|
|
19
|
+
const STATEMENT_LINE_KEYWORDS = new Set([
|
|
20
|
+
"if", "for", "while", "switch", "catch", "return", "throw", "else", "do",
|
|
21
|
+
"case", "break", "continue", "assert", "super", "this", "try", "new", "yield"
|
|
22
|
+
]);
|
|
23
|
+
// Defence-in-depth: even if a keyword is captured as a member name, drop it.
|
|
4
24
|
const NOISE_TOKENS = new Set(["if", "for", "while", "switch", "catch", "return", "new", "throw"]);
|
|
5
25
|
function normalizeLine(line) {
|
|
6
26
|
return line.replace(/\s+/g, " ").trim();
|
|
@@ -8,6 +28,10 @@ function normalizeLine(line) {
|
|
|
8
28
|
function isNoiseToken(token) {
|
|
9
29
|
return NOISE_TOKENS.has(token);
|
|
10
30
|
}
|
|
31
|
+
function isStatementLine(line) {
|
|
32
|
+
const firstWord = /^([A-Za-z_$][\w$]*)/.exec(line)?.[1];
|
|
33
|
+
return firstWord != null && STATEMENT_LINE_KEYWORDS.has(firstWord);
|
|
34
|
+
}
|
|
11
35
|
function lineIndexToLine(lineNo) {
|
|
12
36
|
return lineNo + 1;
|
|
13
37
|
}
|
|
@@ -17,8 +41,13 @@ export function extractSymbolsFromSource(filePath, content) {
|
|
|
17
41
|
const qualifiedName = filePath.replace(/\.java$/, "").replaceAll("/", ".");
|
|
18
42
|
for (let index = 0; index < lines.length; index += 1) {
|
|
19
43
|
const rawLine = lines[index] ?? "";
|
|
20
|
-
const
|
|
21
|
-
if (!
|
|
44
|
+
const normalized = normalizeLine(rawLine);
|
|
45
|
+
if (!normalized) {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
// Drop a single leading "}"/"{" so "} public void foo() {" still parses.
|
|
49
|
+
const line = normalized.replace(/^[}{]\s*/, "");
|
|
50
|
+
if (!line || isStatementLine(line)) {
|
|
22
51
|
continue;
|
|
23
52
|
}
|
|
24
53
|
const classMatch = line.match(CLASS_DECLARATION);
|
|
@@ -35,7 +64,7 @@ export function extractSymbolsFromSource(filePath, content) {
|
|
|
35
64
|
}
|
|
36
65
|
continue;
|
|
37
66
|
}
|
|
38
|
-
const methodMatch = line.match(
|
|
67
|
+
const methodMatch = line.match(METHOD_BLOCK_DECLARATION) ?? line.match(METHOD_ABSTRACT_DECLARATION);
|
|
39
68
|
if (methodMatch) {
|
|
40
69
|
const symbolName = methodMatch[1];
|
|
41
70
|
if (symbolName && !isNoiseToken(symbolName)) {
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slice `text` to at most `maxChars` UTF-16 code units without splitting a
|
|
3
|
+
* surrogate pair. A naive `text.slice(0, maxChars)` can leave a lone high
|
|
4
|
+
* surrogate at the boundary (e.g. when an emoji straddles the cut), producing
|
|
5
|
+
* an invalid string; this drops that dangling half so callers never emit one.
|
|
6
|
+
*/
|
|
7
|
+
export declare function sliceToMaxCharsSafe(text: string, maxChars: number): string;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slice `text` to at most `maxChars` UTF-16 code units without splitting a
|
|
3
|
+
* surrogate pair. A naive `text.slice(0, maxChars)` can leave a lone high
|
|
4
|
+
* surrogate at the boundary (e.g. when an emoji straddles the cut), producing
|
|
5
|
+
* an invalid string; this drops that dangling half so callers never emit one.
|
|
6
|
+
*/
|
|
7
|
+
export function sliceToMaxCharsSafe(text, maxChars) {
|
|
8
|
+
if (maxChars <= 0) {
|
|
9
|
+
return "";
|
|
10
|
+
}
|
|
11
|
+
if (text.length <= maxChars) {
|
|
12
|
+
return text;
|
|
13
|
+
}
|
|
14
|
+
let cut = text.slice(0, maxChars);
|
|
15
|
+
const lastUnit = cut.charCodeAt(cut.length - 1);
|
|
16
|
+
if (lastUnit >= 0xd800 && lastUnit <= 0xdbff) {
|
|
17
|
+
cut = cut.slice(0, -1);
|
|
18
|
+
}
|
|
19
|
+
return cut;
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=text-truncate.js.map
|
|
@@ -15,6 +15,16 @@ function tinyRemapperDownloadUrl(version) {
|
|
|
15
15
|
export async function resolveTinyRemapperJar(cacheDir, overridePath, fetchFn) {
|
|
16
16
|
// 1. Environment / config override
|
|
17
17
|
if (overridePath) {
|
|
18
|
+
if (!existsSync(overridePath)) {
|
|
19
|
+
throw createError({
|
|
20
|
+
code: ERROR_CODES.INVALID_INPUT,
|
|
21
|
+
message: `Configured tiny-remapper jar does not exist: ${overridePath}`,
|
|
22
|
+
details: {
|
|
23
|
+
path: overridePath,
|
|
24
|
+
nextAction: "Point tinyRemapperJarPath / MCP_TINY_REMAPPER_JAR at an existing fat jar, or unset it to download automatically."
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
}
|
|
18
28
|
return overridePath;
|
|
19
29
|
}
|
|
20
30
|
const version = process.env.MCP_TINY_REMAPPER_VERSION ?? DEFAULT_VERSION;
|
|
@@ -67,14 +67,14 @@ const SECTION_ROWS = {
|
|
|
67
67
|
"mapping-symbols": {
|
|
68
68
|
en: [
|
|
69
69
|
"| `find-mapping` | Look up mapping candidates for class, field, or method symbols |",
|
|
70
|
-
"| `resolve-method-mapping-exact` |
|
|
70
|
+
"| `resolve-method-mapping-exact` | Strict shortcut for `find-mapping` (kind=method, signatureMode=exact); requires the full owner+name+descriptor triple |",
|
|
71
71
|
"| `get-class-api-matrix` | Show one class API across `obfuscated`, `mojang`, `intermediary`, and `yarn` |",
|
|
72
72
|
"| `resolve-workspace-symbol` | Resolve compile-visible symbol names from a Gradle workspace |",
|
|
73
73
|
"| `check-symbol-exists` | Check whether a class, field, or method exists in a namespace |"
|
|
74
74
|
],
|
|
75
75
|
ja: [
|
|
76
76
|
"| `find-mapping` | クラス、フィールド、メソッドのシンボルに対するマッピング候補を調べる |",
|
|
77
|
-
"| `resolve-method-mapping-exact` | owner
|
|
77
|
+
"| `resolve-method-mapping-exact` | `find-mapping`(kind=method, signatureMode=exact)の厳密版ショートカット。owner+name+descriptor の三つ組が必須 |",
|
|
78
78
|
"| `get-class-api-matrix` | 1 つのクラス API を `obfuscated`、`mojang`、`intermediary`、`yarn` で見比べる |",
|
|
79
79
|
"| `resolve-workspace-symbol` | Gradle ワークスペースからコンパイル時に見えるシンボル名を解決する |",
|
|
80
80
|
"| `check-symbol-exists` | 名前空間内でクラス、フィールド、メソッドが存在するかを確認する |"
|
|
@@ -3,6 +3,13 @@ const DEFAULT_OPTIONS = {
|
|
|
3
3
|
maxConcurrent: 1,
|
|
4
4
|
maxQueue: 2
|
|
5
5
|
};
|
|
6
|
+
// Heavy tools that have a batch equivalent able to collapse many same-kind
|
|
7
|
+
// queries into a single gated call. Used to point overflow guidance at the
|
|
8
|
+
// right batch-* tool instead of just telling the caller to retry serially.
|
|
9
|
+
const BATCH_EQUIVALENTS = {
|
|
10
|
+
"find-mapping": "batch-mappings",
|
|
11
|
+
"resolve-method-mapping-exact": "batch-mappings"
|
|
12
|
+
};
|
|
6
13
|
export class ToolExecutionGate {
|
|
7
14
|
maxConcurrent;
|
|
8
15
|
maxQueue;
|
|
@@ -17,6 +24,10 @@ export class ToolExecutionGate {
|
|
|
17
24
|
return this.execute({ tool, task: task });
|
|
18
25
|
}
|
|
19
26
|
if (this.queue.length >= this.maxQueue) {
|
|
27
|
+
const batchTool = BATCH_EQUIVALENTS[tool];
|
|
28
|
+
const nextAction = batchTool
|
|
29
|
+
? `Retry after the current heavy analysis request completes. To run many ${tool} queries efficiently, collapse them into a single ${batchTool} call instead of issuing them in parallel.`
|
|
30
|
+
: "Retry after the current heavy analysis request completes. Avoid sending multiple heavy mapping/version analysis tools in parallel.";
|
|
20
31
|
return Promise.reject(createError({
|
|
21
32
|
code: ERROR_CODES.LIMIT_EXCEEDED,
|
|
22
33
|
message: `Heavy tool queue is full; "${tool}" was not started.`,
|
|
@@ -26,7 +37,7 @@ export class ToolExecutionGate {
|
|
|
26
37
|
queuedCount: this.queue.length,
|
|
27
38
|
maxConcurrent: this.maxConcurrent,
|
|
28
39
|
maxQueue: this.maxQueue,
|
|
29
|
-
nextAction
|
|
40
|
+
nextAction
|
|
30
41
|
}
|
|
31
42
|
}));
|
|
32
43
|
}
|
package/dist/tool-guidance.d.ts
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import { ZodError } from "zod";
|
|
2
|
+
import type { WarningDetail } from "./warning-details.js";
|
|
2
3
|
import { type ExampleCall, type ProblemDetails, type ProblemFieldError, type SuggestedCall } from "./error-mapping.js";
|
|
3
4
|
import type { SourceTargetInput } from "./types.js";
|
|
4
5
|
export type ToolMeta = {
|
|
5
6
|
requestId: string;
|
|
6
7
|
tool: string;
|
|
7
8
|
durationMs: number;
|
|
8
|
-
warnings
|
|
9
|
+
/** Omitted entirely when there are no warnings (token efficiency). */
|
|
10
|
+
warnings?: string[];
|
|
11
|
+
/** Structured companion to `warnings`: one classified entry per warning string, referencing its text via `warnings[detail.index]`. */
|
|
12
|
+
warningDetails?: WarningDetail[];
|
|
9
13
|
detailApplied?: "summary" | "standard" | "full";
|
|
10
14
|
includeApplied?: string[];
|
|
11
15
|
truncated?: Record<string, unknown>;
|
|
@@ -29,7 +33,7 @@ export declare const SUGGESTED_CALL_DEFAULTS: {
|
|
|
29
33
|
readonly explain: false;
|
|
30
34
|
readonly preferProjectMapping: false;
|
|
31
35
|
readonly minSeverity: "all";
|
|
32
|
-
readonly reportMode: "
|
|
36
|
+
readonly reportMode: "summary-first";
|
|
33
37
|
readonly treatInfoAsWarning: true;
|
|
34
38
|
readonly includeIssues: true;
|
|
35
39
|
};
|