@adhisang/minecraft-modding-mcp 1.2.1 → 2.1.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 +73 -0
- package/README.md +184 -64
- package/dist/cli.js +31 -4
- package/dist/compat-stdio-transport.d.ts +2 -7
- package/dist/compat-stdio-transport.js +12 -154
- package/dist/index.js +537 -202
- package/dist/json-rpc-framing.d.ts +22 -0
- package/dist/json-rpc-framing.js +168 -0
- package/dist/mapping-pipeline-service.d.ts +1 -1
- package/dist/mapping-pipeline-service.js +13 -5
- package/dist/mapping-service.d.ts +12 -4
- package/dist/mapping-service.js +222 -105
- package/dist/mcp-helpers.d.ts +10 -2
- package/dist/mcp-helpers.js +59 -5
- package/dist/minecraft-explorer-service.d.ts +1 -2
- package/dist/minecraft-explorer-service.js +120 -24
- package/dist/mixin-validator.d.ts +24 -2
- package/dist/mixin-validator.js +228 -103
- package/dist/mod-decompile-service.d.ts +5 -0
- package/dist/mod-decompile-service.js +40 -5
- package/dist/mod-remap-service.js +142 -30
- package/dist/mojang-tiny-mapping-service.js +26 -26
- package/dist/path-resolver.js +41 -4
- package/dist/registry-service.d.ts +10 -1
- package/dist/registry-service.js +154 -22
- package/dist/resources.js +7 -7
- package/dist/search-hit-accumulator.d.ts +0 -3
- package/dist/search-hit-accumulator.js +27 -6
- package/dist/source-jar-reader.js +16 -2
- package/dist/source-resolver.d.ts +1 -0
- package/dist/source-resolver.js +93 -2
- package/dist/source-service.d.ts +76 -47
- package/dist/source-service.js +1344 -763
- package/dist/stdio-supervisor.d.ts +46 -0
- package/dist/stdio-supervisor.js +349 -0
- package/dist/storage/files-repo.d.ts +3 -0
- package/dist/storage/files-repo.js +66 -1
- package/dist/storage/migrations.d.ts +1 -1
- package/dist/storage/migrations.js +6 -2
- package/dist/storage/schema.d.ts +1 -0
- package/dist/storage/schema.js +7 -0
- package/dist/symbols/symbol-extractor.js +6 -4
- package/dist/tool-execution-gate.d.ts +15 -0
- package/dist/tool-execution-gate.js +58 -0
- package/dist/tool-input.d.ts +6 -0
- package/dist/tool-input.js +64 -0
- package/dist/types.d.ts +1 -1
- package/dist/version-diff-service.js +10 -5
- package/dist/version-service.js +7 -2
- package/dist/workspace-mapping-service.js +12 -0
- package/package.json +4 -1
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
type SupervisorOptions = {
|
|
2
|
+
entryFile: string;
|
|
3
|
+
};
|
|
4
|
+
export declare class StdioSupervisor {
|
|
5
|
+
private readonly entryFile;
|
|
6
|
+
private readonly clientReader;
|
|
7
|
+
private readonly workerReader;
|
|
8
|
+
private readonly queuedMessages;
|
|
9
|
+
private readonly pendingRequests;
|
|
10
|
+
private child;
|
|
11
|
+
private childReady;
|
|
12
|
+
private shuttingDown;
|
|
13
|
+
private restartTimer;
|
|
14
|
+
private workerStderrBuffer;
|
|
15
|
+
private clientMode;
|
|
16
|
+
private initializeRequest;
|
|
17
|
+
private initializedNotification;
|
|
18
|
+
private clientInitialized;
|
|
19
|
+
private replayingInitialization;
|
|
20
|
+
private initializeSentToWorker;
|
|
21
|
+
constructor(options: SupervisorOptions);
|
|
22
|
+
start(): Promise<void>;
|
|
23
|
+
private readonly handleClientData;
|
|
24
|
+
private readonly handleClientError;
|
|
25
|
+
private readonly handleClientClosed;
|
|
26
|
+
private readonly handleTerminateSignal;
|
|
27
|
+
private handleClientMessage;
|
|
28
|
+
private forwardToWorker;
|
|
29
|
+
private spawnWorker;
|
|
30
|
+
private readonly handleWorkerData;
|
|
31
|
+
private readonly handleWorkerStdinError;
|
|
32
|
+
private readonly handleWorkerStderr;
|
|
33
|
+
private readonly handleWorkerProcessError;
|
|
34
|
+
private readonly handleWorkerExit;
|
|
35
|
+
private handleWorkerMessage;
|
|
36
|
+
private handleWorkerReady;
|
|
37
|
+
private isInitializationResponse;
|
|
38
|
+
private flushQueue;
|
|
39
|
+
private failPendingRequestsOnWorkerExit;
|
|
40
|
+
private writeToClient;
|
|
41
|
+
private scheduleRestart;
|
|
42
|
+
private detachChild;
|
|
43
|
+
private shutdown;
|
|
44
|
+
}
|
|
45
|
+
export declare const STDIO_WORKER_MODE_ENV = "MCP_STDIO_WORKER_MODE";
|
|
46
|
+
export {};
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import process from "node:process";
|
|
3
|
+
import { encodeJsonRpcMessage, JsonRpcFrameReader } from "./json-rpc-framing.js";
|
|
4
|
+
import { log } from "./logger.js";
|
|
5
|
+
const DEFAULT_CLIENT_MODE = "line";
|
|
6
|
+
const WORKER_MODE_ENV = "MCP_STDIO_WORKER_MODE";
|
|
7
|
+
const WORKER_READY_MARKER = "__MCP_STDIO_WORKER_READY__";
|
|
8
|
+
const SUPERVISOR_DEBUG_ENABLED = process.env.MCP_SUPERVISOR_DEBUG === "1";
|
|
9
|
+
function isRequest(message) {
|
|
10
|
+
return "method" in message && "id" in message;
|
|
11
|
+
}
|
|
12
|
+
function isNotification(message) {
|
|
13
|
+
return "method" in message && !("id" in message);
|
|
14
|
+
}
|
|
15
|
+
function isResponse(message) {
|
|
16
|
+
return !("method" in message) && "id" in message;
|
|
17
|
+
}
|
|
18
|
+
function getTrackedRequestId(message) {
|
|
19
|
+
return typeof message.id === "string" || typeof message.id === "number"
|
|
20
|
+
? message.id
|
|
21
|
+
: undefined;
|
|
22
|
+
}
|
|
23
|
+
function requestKey(id) {
|
|
24
|
+
return `${typeof id}:${String(id)}`;
|
|
25
|
+
}
|
|
26
|
+
function buildWorkerRestartError(id) {
|
|
27
|
+
return {
|
|
28
|
+
jsonrpc: "2.0",
|
|
29
|
+
id,
|
|
30
|
+
error: {
|
|
31
|
+
code: -32603,
|
|
32
|
+
message: "MCP worker restarted while handling the request. Retry the request."
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function debugSupervisor(event, details) {
|
|
37
|
+
if (!SUPERVISOR_DEBUG_ENABLED) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
log("info", `supervisor.debug.${event}`, details);
|
|
41
|
+
}
|
|
42
|
+
export class StdioSupervisor {
|
|
43
|
+
entryFile;
|
|
44
|
+
clientReader = new JsonRpcFrameReader();
|
|
45
|
+
workerReader = new JsonRpcFrameReader();
|
|
46
|
+
queuedMessages = [];
|
|
47
|
+
pendingRequests = new Map();
|
|
48
|
+
child;
|
|
49
|
+
childReady = false;
|
|
50
|
+
shuttingDown = false;
|
|
51
|
+
restartTimer;
|
|
52
|
+
workerStderrBuffer = "";
|
|
53
|
+
clientMode = DEFAULT_CLIENT_MODE;
|
|
54
|
+
initializeRequest;
|
|
55
|
+
initializedNotification;
|
|
56
|
+
clientInitialized = false;
|
|
57
|
+
replayingInitialization = false;
|
|
58
|
+
initializeSentToWorker = false;
|
|
59
|
+
constructor(options) {
|
|
60
|
+
this.entryFile = options.entryFile;
|
|
61
|
+
}
|
|
62
|
+
async start() {
|
|
63
|
+
process.stdin.on("data", this.handleClientData);
|
|
64
|
+
process.stdin.on("error", this.handleClientError);
|
|
65
|
+
process.stdin.on("end", this.handleClientClosed);
|
|
66
|
+
process.stdin.on("close", this.handleClientClosed);
|
|
67
|
+
process.stdin.resume();
|
|
68
|
+
process.on("SIGINT", this.handleTerminateSignal);
|
|
69
|
+
process.on("SIGTERM", this.handleTerminateSignal);
|
|
70
|
+
this.spawnWorker();
|
|
71
|
+
}
|
|
72
|
+
handleClientData = (chunk) => {
|
|
73
|
+
this.clientReader.processChunk(chunk, {
|
|
74
|
+
onFrame: ({ message, mode }) => {
|
|
75
|
+
this.clientMode = mode;
|
|
76
|
+
this.handleClientMessage(message);
|
|
77
|
+
},
|
|
78
|
+
onError: (error) => {
|
|
79
|
+
log("warn", "supervisor.client_parse_error", { message: error.message });
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
};
|
|
83
|
+
handleClientError = (error) => {
|
|
84
|
+
log("warn", "supervisor.client_stream_error", { message: error.message });
|
|
85
|
+
};
|
|
86
|
+
handleClientClosed = () => {
|
|
87
|
+
void this.shutdown();
|
|
88
|
+
};
|
|
89
|
+
handleTerminateSignal = () => {
|
|
90
|
+
void this.shutdown();
|
|
91
|
+
};
|
|
92
|
+
handleClientMessage(message) {
|
|
93
|
+
debugSupervisor("client_message", {
|
|
94
|
+
hasMethod: "method" in message,
|
|
95
|
+
method: "method" in message ? message.method : undefined,
|
|
96
|
+
id: "id" in message ? message.id : undefined,
|
|
97
|
+
childReady: this.childReady
|
|
98
|
+
});
|
|
99
|
+
if (isRequest(message) && message.method === "initialize") {
|
|
100
|
+
this.initializeRequest = message;
|
|
101
|
+
this.clientInitialized = false;
|
|
102
|
+
}
|
|
103
|
+
else if (isNotification(message) && message.method === "notifications/initialized") {
|
|
104
|
+
this.initializedNotification = message;
|
|
105
|
+
}
|
|
106
|
+
if (!this.childReady) {
|
|
107
|
+
this.queuedMessages.push(message);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
this.forwardToWorker(message);
|
|
111
|
+
}
|
|
112
|
+
forwardToWorker(message) {
|
|
113
|
+
const child = this.child;
|
|
114
|
+
if (!child || child.stdin.destroyed) {
|
|
115
|
+
this.queuedMessages.push(message);
|
|
116
|
+
if (!this.shuttingDown) {
|
|
117
|
+
this.scheduleRestart();
|
|
118
|
+
}
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (isRequest(message)) {
|
|
122
|
+
const id = getTrackedRequestId(message);
|
|
123
|
+
if (id !== undefined) {
|
|
124
|
+
this.pendingRequests.set(requestKey(id), {
|
|
125
|
+
id,
|
|
126
|
+
method: message.method
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
if (message.method === "initialize") {
|
|
130
|
+
this.initializeSentToWorker = true;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
debugSupervisor("forward_to_worker", {
|
|
134
|
+
method: "method" in message ? message.method : undefined,
|
|
135
|
+
id: "id" in message ? message.id : undefined
|
|
136
|
+
});
|
|
137
|
+
child.stdin.write(encodeJsonRpcMessage(message, "content-length"));
|
|
138
|
+
}
|
|
139
|
+
spawnWorker() {
|
|
140
|
+
if (this.shuttingDown) {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
const child = spawn(process.execPath, [...process.execArgv, this.entryFile], {
|
|
144
|
+
env: {
|
|
145
|
+
...process.env,
|
|
146
|
+
[WORKER_MODE_ENV]: "1"
|
|
147
|
+
},
|
|
148
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
149
|
+
});
|
|
150
|
+
this.child = child;
|
|
151
|
+
this.childReady = false;
|
|
152
|
+
this.initializeSentToWorker = false;
|
|
153
|
+
this.workerReader.clear();
|
|
154
|
+
this.workerStderrBuffer = "";
|
|
155
|
+
child.stdout.on("data", this.handleWorkerData);
|
|
156
|
+
child.stderr.on("data", this.handleWorkerStderr);
|
|
157
|
+
child.stdin.on("error", this.handleWorkerStdinError);
|
|
158
|
+
child.once("error", this.handleWorkerProcessError);
|
|
159
|
+
child.once("exit", this.handleWorkerExit);
|
|
160
|
+
log("info", "supervisor.worker_spawn", { pid: child.pid });
|
|
161
|
+
}
|
|
162
|
+
handleWorkerData = (chunk) => {
|
|
163
|
+
this.workerReader.processChunk(chunk, {
|
|
164
|
+
onFrame: ({ message }) => {
|
|
165
|
+
this.handleWorkerMessage(message);
|
|
166
|
+
},
|
|
167
|
+
onError: (error) => {
|
|
168
|
+
log("warn", "supervisor.worker_parse_error", { message: error.message });
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
};
|
|
172
|
+
handleWorkerStdinError = (error) => {
|
|
173
|
+
if (error.code === "EPIPE") {
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
log("warn", "supervisor.worker_stdin_error", { message: error.message });
|
|
177
|
+
};
|
|
178
|
+
handleWorkerStderr = (chunk) => {
|
|
179
|
+
this.workerStderrBuffer += chunk.toString();
|
|
180
|
+
const lines = this.workerStderrBuffer.split(/\r?\n/);
|
|
181
|
+
this.workerStderrBuffer = lines.pop() ?? "";
|
|
182
|
+
for (const line of lines) {
|
|
183
|
+
if (line === WORKER_READY_MARKER) {
|
|
184
|
+
this.handleWorkerReady();
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
process.stderr.write(`${line}\n`);
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
handleWorkerProcessError = (error) => {
|
|
191
|
+
log("error", "supervisor.worker_process_error", { message: error.message });
|
|
192
|
+
};
|
|
193
|
+
handleWorkerExit = (code, signal) => {
|
|
194
|
+
const childPid = this.child?.pid;
|
|
195
|
+
this.detachChild();
|
|
196
|
+
if (this.shuttingDown) {
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
log("warn", "supervisor.worker_exit", {
|
|
200
|
+
pid: childPid,
|
|
201
|
+
code,
|
|
202
|
+
signal,
|
|
203
|
+
pendingRequests: this.pendingRequests.size
|
|
204
|
+
});
|
|
205
|
+
this.failPendingRequestsOnWorkerExit();
|
|
206
|
+
this.scheduleRestart();
|
|
207
|
+
};
|
|
208
|
+
handleWorkerMessage(message) {
|
|
209
|
+
debugSupervisor("worker_message", {
|
|
210
|
+
hasMethod: "method" in message,
|
|
211
|
+
method: "method" in message ? message.method : undefined,
|
|
212
|
+
id: "id" in message ? message.id : undefined,
|
|
213
|
+
replayingInitialization: this.replayingInitialization
|
|
214
|
+
});
|
|
215
|
+
if (this.isInitializationResponse(message)) {
|
|
216
|
+
const id = getTrackedRequestId(message);
|
|
217
|
+
if (id !== undefined) {
|
|
218
|
+
this.pendingRequests.delete(requestKey(id));
|
|
219
|
+
}
|
|
220
|
+
if (this.replayingInitialization) {
|
|
221
|
+
this.replayingInitialization = false;
|
|
222
|
+
if (this.initializedNotification) {
|
|
223
|
+
this.forwardToWorker(this.initializedNotification);
|
|
224
|
+
}
|
|
225
|
+
this.childReady = true;
|
|
226
|
+
this.flushQueue();
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
this.clientInitialized = true;
|
|
230
|
+
this.childReady = true;
|
|
231
|
+
this.writeToClient(message);
|
|
232
|
+
this.flushQueue();
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
if (isResponse(message)) {
|
|
236
|
+
const id = getTrackedRequestId(message);
|
|
237
|
+
if (id !== undefined) {
|
|
238
|
+
this.pendingRequests.delete(requestKey(id));
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
this.writeToClient(message);
|
|
242
|
+
}
|
|
243
|
+
handleWorkerReady() {
|
|
244
|
+
debugSupervisor("worker_ready", {
|
|
245
|
+
hasInitializeRequest: this.initializeRequest !== undefined,
|
|
246
|
+
clientInitialized: this.clientInitialized
|
|
247
|
+
});
|
|
248
|
+
if (!this.initializeRequest) {
|
|
249
|
+
this.childReady = true;
|
|
250
|
+
this.flushQueue();
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
this.replayingInitialization = this.clientInitialized;
|
|
254
|
+
this.forwardToWorker(this.initializeRequest);
|
|
255
|
+
}
|
|
256
|
+
isInitializationResponse(message) {
|
|
257
|
+
const id = isResponse(message) ? getTrackedRequestId(message) : undefined;
|
|
258
|
+
const initializeId = this.initializeRequest
|
|
259
|
+
? getTrackedRequestId(this.initializeRequest)
|
|
260
|
+
: undefined;
|
|
261
|
+
return (id !== undefined &&
|
|
262
|
+
initializeId !== undefined &&
|
|
263
|
+
requestKey(id) === requestKey(initializeId));
|
|
264
|
+
}
|
|
265
|
+
flushQueue() {
|
|
266
|
+
if (!this.childReady || this.queuedMessages.length === 0) {
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
const pending = this.queuedMessages.splice(0, this.queuedMessages.length);
|
|
270
|
+
for (const message of pending) {
|
|
271
|
+
if (this.initializeSentToWorker &&
|
|
272
|
+
isRequest(message) &&
|
|
273
|
+
message.method === "initialize" &&
|
|
274
|
+
this.initializeRequest !== undefined &&
|
|
275
|
+
getTrackedRequestId(message) === getTrackedRequestId(this.initializeRequest)) {
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
this.forwardToWorker(message);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
failPendingRequestsOnWorkerExit() {
|
|
282
|
+
const preservedInitializeKey = this.initializeRequest && !this.clientInitialized
|
|
283
|
+
? requestKey(this.initializeRequest.id)
|
|
284
|
+
: undefined;
|
|
285
|
+
for (const [key, pending] of [...this.pendingRequests.entries()]) {
|
|
286
|
+
if (key === preservedInitializeKey) {
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
this.pendingRequests.delete(key);
|
|
290
|
+
this.writeToClient(buildWorkerRestartError(pending.id));
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
writeToClient(message) {
|
|
294
|
+
debugSupervisor("write_to_client", {
|
|
295
|
+
hasMethod: "method" in message,
|
|
296
|
+
method: "method" in message ? message.method : undefined,
|
|
297
|
+
id: "id" in message ? message.id : undefined,
|
|
298
|
+
clientMode: this.clientMode
|
|
299
|
+
});
|
|
300
|
+
const frame = encodeJsonRpcMessage(message, this.clientMode);
|
|
301
|
+
process.stdout.write(frame);
|
|
302
|
+
}
|
|
303
|
+
scheduleRestart() {
|
|
304
|
+
if (this.restartTimer || this.shuttingDown) {
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
this.restartTimer = setTimeout(() => {
|
|
308
|
+
this.restartTimer = undefined;
|
|
309
|
+
this.spawnWorker();
|
|
310
|
+
}, 100);
|
|
311
|
+
}
|
|
312
|
+
detachChild() {
|
|
313
|
+
const child = this.child;
|
|
314
|
+
if (!child) {
|
|
315
|
+
this.childReady = false;
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
child.stdout.off("data", this.handleWorkerData);
|
|
319
|
+
child.stderr.off("data", this.handleWorkerStderr);
|
|
320
|
+
child.stdin.off("error", this.handleWorkerStdinError);
|
|
321
|
+
child.off("error", this.handleWorkerProcessError);
|
|
322
|
+
child.off("exit", this.handleWorkerExit);
|
|
323
|
+
this.child = undefined;
|
|
324
|
+
this.childReady = false;
|
|
325
|
+
}
|
|
326
|
+
async shutdown() {
|
|
327
|
+
if (this.shuttingDown) {
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
this.shuttingDown = true;
|
|
331
|
+
if (this.restartTimer) {
|
|
332
|
+
clearTimeout(this.restartTimer);
|
|
333
|
+
this.restartTimer = undefined;
|
|
334
|
+
}
|
|
335
|
+
process.stdin.off("data", this.handleClientData);
|
|
336
|
+
process.stdin.off("error", this.handleClientError);
|
|
337
|
+
process.stdin.off("end", this.handleClientClosed);
|
|
338
|
+
process.stdin.off("close", this.handleClientClosed);
|
|
339
|
+
process.off("SIGINT", this.handleTerminateSignal);
|
|
340
|
+
process.off("SIGTERM", this.handleTerminateSignal);
|
|
341
|
+
const child = this.child;
|
|
342
|
+
this.detachChild();
|
|
343
|
+
if (child && child.exitCode === null) {
|
|
344
|
+
child.kill("SIGTERM");
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
export const STDIO_WORKER_MODE_ENV = WORKER_MODE_ENV;
|
|
349
|
+
//# sourceMappingURL=stdio-supervisor.js.map
|
|
@@ -50,6 +50,7 @@ export declare class FilesRepo {
|
|
|
50
50
|
private readonly searchPathStmt;
|
|
51
51
|
private readonly searchFtsStmt;
|
|
52
52
|
private readonly getByPathsStmtCache;
|
|
53
|
+
private readonly classLookupStmtCache;
|
|
53
54
|
constructor(db: SqliteDatabase);
|
|
54
55
|
clearFilesForArtifact(artifactId: string): void;
|
|
55
56
|
insertFilesForArtifact(artifactId: string, files: IndexedFile[]): void;
|
|
@@ -70,6 +71,8 @@ export declare class FilesRepo {
|
|
|
70
71
|
countTextCandidates(artifactId: string, query: string): number;
|
|
71
72
|
countPathCandidates(artifactId: string, query: string): number;
|
|
72
73
|
findFirstFilePathByName(artifactId: string, fileName: string): string | undefined;
|
|
74
|
+
private getClassLookupStmt;
|
|
75
|
+
findBestClassLookupPath(artifactId: string, exactFilePaths: string[], qualifiedClassName: string, simpleName: string, expectedPrefix: string): string | undefined;
|
|
73
76
|
searchFiles(artifactId: string, options: SearchFilesOptions): PagedResult<SearchFilesResult>;
|
|
74
77
|
totalContentBytes(): number;
|
|
75
78
|
contentBytesForArtifact(artifactId: string): number;
|
|
@@ -114,6 +114,7 @@ export class FilesRepo {
|
|
|
114
114
|
searchPathStmt;
|
|
115
115
|
searchFtsStmt;
|
|
116
116
|
getByPathsStmtCache = new Map();
|
|
117
|
+
classLookupStmtCache = new Map();
|
|
117
118
|
constructor(db) {
|
|
118
119
|
this.db = db;
|
|
119
120
|
this.deleteStmt = this.db.prepare(`
|
|
@@ -416,6 +417,65 @@ export class FilesRepo {
|
|
|
416
417
|
.get(artifactId, normalized, `%/${normalized}`);
|
|
417
418
|
return row?.file_path;
|
|
418
419
|
}
|
|
420
|
+
getClassLookupStmt(exactPathCount) {
|
|
421
|
+
const cached = this.classLookupStmtCache.get(exactPathCount);
|
|
422
|
+
if (cached) {
|
|
423
|
+
return cached;
|
|
424
|
+
}
|
|
425
|
+
const exactPlaceholders = Array.from({ length: exactPathCount }, () => "?").join(", ");
|
|
426
|
+
const statement = this.db.prepare(`
|
|
427
|
+
WITH ranked_candidates AS (
|
|
428
|
+
SELECT file_path, 0 AS rank
|
|
429
|
+
FROM files
|
|
430
|
+
WHERE artifact_id = ?
|
|
431
|
+
AND file_path IN (${exactPlaceholders})
|
|
432
|
+
|
|
433
|
+
UNION ALL
|
|
434
|
+
|
|
435
|
+
SELECT file_path,
|
|
436
|
+
CASE
|
|
437
|
+
WHEN qualified_name = ? THEN 1
|
|
438
|
+
WHEN symbol_name = ? THEN 2
|
|
439
|
+
ELSE 3
|
|
440
|
+
END AS rank
|
|
441
|
+
FROM symbols
|
|
442
|
+
WHERE artifact_id = ?
|
|
443
|
+
AND symbol_kind = 'class'
|
|
444
|
+
AND (
|
|
445
|
+
qualified_name = ?
|
|
446
|
+
OR symbol_name = ?
|
|
447
|
+
OR qualified_name LIKE ?
|
|
448
|
+
)
|
|
449
|
+
AND (? = '' OR instr(file_path, ?) = 1)
|
|
450
|
+
|
|
451
|
+
UNION ALL
|
|
452
|
+
|
|
453
|
+
SELECT file_path, 4 AS rank
|
|
454
|
+
FROM files
|
|
455
|
+
WHERE artifact_id = ?
|
|
456
|
+
AND (file_path = ? OR file_path LIKE ? ESCAPE '\\')
|
|
457
|
+
AND (? = '' OR instr(file_path, ?) = 1)
|
|
458
|
+
)
|
|
459
|
+
SELECT file_path
|
|
460
|
+
FROM ranked_candidates
|
|
461
|
+
ORDER BY rank ASC, file_path ASC
|
|
462
|
+
LIMIT 1
|
|
463
|
+
`);
|
|
464
|
+
this.classLookupStmtCache.set(exactPathCount, statement);
|
|
465
|
+
return statement;
|
|
466
|
+
}
|
|
467
|
+
findBestClassLookupPath(artifactId, exactFilePaths, qualifiedClassName, simpleName, expectedPrefix) {
|
|
468
|
+
const normalizedExactFilePaths = [...new Set(exactFilePaths.map((entry) => entry.trim()).filter(Boolean))];
|
|
469
|
+
const normalizedClassName = qualifiedClassName.trim();
|
|
470
|
+
const normalizedSimpleName = simpleName.trim();
|
|
471
|
+
if (normalizedExactFilePaths.length === 0 || !normalizedClassName || !normalizedSimpleName) {
|
|
472
|
+
return undefined;
|
|
473
|
+
}
|
|
474
|
+
const stmt = this.getClassLookupStmt(normalizedExactFilePaths.length);
|
|
475
|
+
const fileName = `${normalizedSimpleName}.java`;
|
|
476
|
+
const row = stmt.get(artifactId, ...normalizedExactFilePaths, normalizedClassName, normalizedSimpleName, artifactId, normalizedClassName, normalizedSimpleName, `%.${normalizedSimpleName}`, expectedPrefix, expectedPrefix, artifactId, fileName, `%/${fileName}`, expectedPrefix, expectedPrefix);
|
|
477
|
+
return row?.file_path;
|
|
478
|
+
}
|
|
419
479
|
searchFiles(artifactId, options) {
|
|
420
480
|
const page = this.searchFilesWithContent(artifactId, options);
|
|
421
481
|
return {
|
|
@@ -446,10 +506,15 @@ export class FilesRepo {
|
|
|
446
506
|
const normalizedCount = Math.max(1, Math.trunc(pathCount));
|
|
447
507
|
const cached = this.getByPathsStmtCache.get(normalizedCount);
|
|
448
508
|
if (cached) {
|
|
509
|
+
this.getByPathsStmtCache.delete(normalizedCount);
|
|
510
|
+
this.getByPathsStmtCache.set(normalizedCount, cached);
|
|
449
511
|
return cached;
|
|
450
512
|
}
|
|
451
513
|
if (this.getByPathsStmtCache.size >= 64) {
|
|
452
|
-
this.getByPathsStmtCache.
|
|
514
|
+
const oldestKey = this.getByPathsStmtCache.keys().next().value;
|
|
515
|
+
if (oldestKey !== undefined) {
|
|
516
|
+
this.getByPathsStmtCache.delete(oldestKey);
|
|
517
|
+
}
|
|
453
518
|
}
|
|
454
519
|
const placeholders = Array.from({ length: normalizedCount }, () => "?").join(", ");
|
|
455
520
|
const stmt = this.db.prepare(`
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import { createError, ERROR_CODES } from "../errors.js";
|
|
2
|
-
import { SCHEMA_V1_STATEMENTS } from "./schema.js";
|
|
3
|
-
export const LATEST_SCHEMA_VERSION =
|
|
2
|
+
import { SCHEMA_V1_STATEMENTS, SCHEMA_V2_STATEMENTS } from "./schema.js";
|
|
3
|
+
export const LATEST_SCHEMA_VERSION = 2;
|
|
4
4
|
const migrations = [
|
|
5
5
|
{
|
|
6
6
|
version: 1,
|
|
7
7
|
statements: SCHEMA_V1_STATEMENTS
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
version: 2,
|
|
11
|
+
statements: SCHEMA_V2_STATEMENTS
|
|
8
12
|
}
|
|
9
13
|
];
|
|
10
14
|
function selectSchemaVersion(tx) {
|
package/dist/storage/schema.d.ts
CHANGED
package/dist/storage/schema.js
CHANGED
|
@@ -157,4 +157,11 @@ export const SCHEMA_V1_STATEMENTS = [
|
|
|
157
157
|
);
|
|
158
158
|
END`
|
|
159
159
|
];
|
|
160
|
+
export const SCHEMA_V2_STATEMENTS = [
|
|
161
|
+
`DELETE FROM symbols`,
|
|
162
|
+
`DELETE FROM files`,
|
|
163
|
+
`DELETE FROM artifact_content_bytes`,
|
|
164
|
+
`DELETE FROM artifact_index_meta`,
|
|
165
|
+
`DELETE FROM artifacts`
|
|
166
|
+
];
|
|
160
167
|
//# sourceMappingURL=schema.js.map
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
const CLASS_DECLARATION = /^(?:\s*@[\w.]+\s+)*(?:\s*(?:public|private|protected|abstract|final|sealed|non-sealed|static)\s+)*\s*(class|interface|enum|record)\s+([A-Za-z_$][\w$]*)/;
|
|
2
2
|
const METHOD_DECLARATION = /^(?:\s*@[\w.]+\s+)*[^{;]*?\b([A-Za-z_$][\w$]*)\s*\([^)]*\)\s*(?:\{|;)/;
|
|
3
3
|
const FIELD_DECLARATION = /^(?:\s*@[\w.]+\s+)*[^\s][\w<>\[\],.?]+\s+([A-Za-z_$][\w$]*)\s*(?:=|;|,)/;
|
|
4
|
+
const NOISE_TOKENS = new Set(["if", "for", "while", "switch", "catch", "return", "new", "throw"]);
|
|
4
5
|
function normalizeLine(line) {
|
|
5
6
|
return line.replace(/\s+/g, " ").trim();
|
|
6
7
|
}
|
|
7
8
|
function isNoiseToken(token) {
|
|
8
|
-
return
|
|
9
|
+
return NOISE_TOKENS.has(token);
|
|
9
10
|
}
|
|
10
11
|
function lineIndexToLine(lineNo) {
|
|
11
12
|
return lineNo + 1;
|
|
@@ -13,6 +14,7 @@ function lineIndexToLine(lineNo) {
|
|
|
13
14
|
export function extractSymbolsFromSource(filePath, content) {
|
|
14
15
|
const lines = content.split(/\r?\n/);
|
|
15
16
|
const symbols = [];
|
|
17
|
+
const qualifiedName = filePath.replace(/\.java$/, "").replaceAll("/", ".");
|
|
16
18
|
for (let index = 0; index < lines.length; index += 1) {
|
|
17
19
|
const rawLine = lines[index] ?? "";
|
|
18
20
|
const line = normalizeLine(rawLine);
|
|
@@ -27,7 +29,7 @@ export function extractSymbolsFromSource(filePath, content) {
|
|
|
27
29
|
symbols.push({
|
|
28
30
|
symbolKind,
|
|
29
31
|
symbolName,
|
|
30
|
-
qualifiedName
|
|
32
|
+
qualifiedName,
|
|
31
33
|
line: lineIndexToLine(index)
|
|
32
34
|
});
|
|
33
35
|
}
|
|
@@ -40,7 +42,7 @@ export function extractSymbolsFromSource(filePath, content) {
|
|
|
40
42
|
symbols.push({
|
|
41
43
|
symbolKind: "method",
|
|
42
44
|
symbolName,
|
|
43
|
-
qualifiedName
|
|
45
|
+
qualifiedName,
|
|
44
46
|
line: lineIndexToLine(index)
|
|
45
47
|
});
|
|
46
48
|
}
|
|
@@ -53,7 +55,7 @@ export function extractSymbolsFromSource(filePath, content) {
|
|
|
53
55
|
symbols.push({
|
|
54
56
|
symbolKind: "field",
|
|
55
57
|
symbolName,
|
|
56
|
-
qualifiedName
|
|
58
|
+
qualifiedName,
|
|
57
59
|
line: lineIndexToLine(index)
|
|
58
60
|
});
|
|
59
61
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
type GateOptions = {
|
|
2
|
+
maxConcurrent: number;
|
|
3
|
+
maxQueue: number;
|
|
4
|
+
};
|
|
5
|
+
export declare class ToolExecutionGate {
|
|
6
|
+
private readonly maxConcurrent;
|
|
7
|
+
private readonly maxQueue;
|
|
8
|
+
private activeCount;
|
|
9
|
+
private readonly queue;
|
|
10
|
+
constructor(options?: Partial<GateOptions>);
|
|
11
|
+
run<T>(tool: string, task: () => Promise<T>): Promise<T>;
|
|
12
|
+
private execute;
|
|
13
|
+
private drain;
|
|
14
|
+
}
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { createError, ERROR_CODES } from "./errors.js";
|
|
2
|
+
const DEFAULT_OPTIONS = {
|
|
3
|
+
maxConcurrent: 1,
|
|
4
|
+
maxQueue: 2
|
|
5
|
+
};
|
|
6
|
+
export class ToolExecutionGate {
|
|
7
|
+
maxConcurrent;
|
|
8
|
+
maxQueue;
|
|
9
|
+
activeCount = 0;
|
|
10
|
+
queue = [];
|
|
11
|
+
constructor(options = {}) {
|
|
12
|
+
this.maxConcurrent = Math.max(1, Math.trunc(options.maxConcurrent ?? DEFAULT_OPTIONS.maxConcurrent));
|
|
13
|
+
this.maxQueue = Math.max(0, Math.trunc(options.maxQueue ?? DEFAULT_OPTIONS.maxQueue));
|
|
14
|
+
}
|
|
15
|
+
run(tool, task) {
|
|
16
|
+
if (this.activeCount < this.maxConcurrent) {
|
|
17
|
+
return this.execute({ tool, task: task });
|
|
18
|
+
}
|
|
19
|
+
if (this.queue.length >= this.maxQueue) {
|
|
20
|
+
return Promise.reject(createError({
|
|
21
|
+
code: ERROR_CODES.LIMIT_EXCEEDED,
|
|
22
|
+
message: `Heavy tool queue is full; "${tool}" was not started.`,
|
|
23
|
+
details: {
|
|
24
|
+
tool,
|
|
25
|
+
activeCount: this.activeCount,
|
|
26
|
+
queuedCount: this.queue.length,
|
|
27
|
+
maxConcurrent: this.maxConcurrent,
|
|
28
|
+
maxQueue: this.maxQueue,
|
|
29
|
+
nextAction: "Retry after the current heavy analysis request completes. Avoid sending multiple heavy mapping/version analysis tools in parallel."
|
|
30
|
+
}
|
|
31
|
+
}));
|
|
32
|
+
}
|
|
33
|
+
return new Promise((resolve, reject) => {
|
|
34
|
+
this.queue.push({
|
|
35
|
+
tool,
|
|
36
|
+
task: task,
|
|
37
|
+
resolve: resolve,
|
|
38
|
+
reject
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
execute(entry) {
|
|
43
|
+
this.activeCount += 1;
|
|
44
|
+
return Promise.resolve()
|
|
45
|
+
.then(entry.task)
|
|
46
|
+
.finally(() => {
|
|
47
|
+
this.activeCount = Math.max(0, this.activeCount - 1);
|
|
48
|
+
this.drain();
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
drain() {
|
|
52
|
+
while (this.activeCount < this.maxConcurrent && this.queue.length > 0) {
|
|
53
|
+
const next = this.queue.shift();
|
|
54
|
+
this.execute(next).then(next.resolve, next.reject);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=tool-execution-gate.js.map
|