magector 2.1.8 → 2.2.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/package.json +5 -5
- package/src/mcp-server.js +242 -68
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "magector",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "Semantic code search for Magento 2 — index, search, MCP server",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/mcp-server.js",
|
|
@@ -39,10 +39,10 @@
|
|
|
39
39
|
"ruvector": "^0.1.96"
|
|
40
40
|
},
|
|
41
41
|
"optionalDependencies": {
|
|
42
|
-
"@magector/cli-darwin-arm64": "2.
|
|
43
|
-
"@magector/cli-linux-x64": "2.
|
|
44
|
-
"@magector/cli-linux-arm64": "2.
|
|
45
|
-
"@magector/cli-win32-x64": "2.
|
|
42
|
+
"@magector/cli-darwin-arm64": "2.2.0",
|
|
43
|
+
"@magector/cli-linux-x64": "2.2.0",
|
|
44
|
+
"@magector/cli-linux-arm64": "2.2.0",
|
|
45
|
+
"@magector/cli-win32-x64": "2.2.0"
|
|
46
46
|
},
|
|
47
47
|
"keywords": [
|
|
48
48
|
"magento",
|
package/src/mcp-server.js
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
} from '@modelcontextprotocol/sdk/types.js';
|
|
17
17
|
import { execFileSync, spawn } from 'child_process';
|
|
18
18
|
import { createInterface } from 'readline';
|
|
19
|
+
import { createServer as createNetServer, createConnection } from 'net';
|
|
19
20
|
import { existsSync, statSync, unlinkSync, copyFileSync, renameSync, appendFileSync, writeFileSync, readFileSync, mkdirSync } from 'fs';
|
|
20
21
|
import { stat } from 'fs/promises';
|
|
21
22
|
import { glob } from 'glob';
|
|
@@ -142,6 +143,8 @@ function extractJson(stdout) {
|
|
|
142
143
|
|
|
143
144
|
const PID_PATH = path.join(config.magentoRoot, '.magector', 'serve.pid');
|
|
144
145
|
const REINDEX_PID_PATH = path.join(config.magentoRoot, '.magector', 'reindex.pid');
|
|
146
|
+
const SOCK_PATH = path.join(config.magentoRoot, '.magector', 'serve.sock');
|
|
147
|
+
const FORMAT_CACHE_PATH = path.join(config.magentoRoot, '.magector', 'format-ok.json');
|
|
145
148
|
|
|
146
149
|
/**
|
|
147
150
|
* Write the serve process PID to disk so future instances can clean up orphans.
|
|
@@ -180,10 +183,29 @@ function getRunningReindexPid() {
|
|
|
180
183
|
}
|
|
181
184
|
}
|
|
182
185
|
|
|
186
|
+
/**
|
|
187
|
+
* Check if an existing serve process is alive and usable.
|
|
188
|
+
* Returns the PID if alive, null if stale/missing.
|
|
189
|
+
* Does NOT kill it — multiple MCP instances can share one serve process
|
|
190
|
+
* by sending queries to it via stdin (each instance starts its own).
|
|
191
|
+
*/
|
|
192
|
+
function getExistingServePid() {
|
|
193
|
+
try {
|
|
194
|
+
if (!existsSync(PID_PATH)) return null;
|
|
195
|
+
const pid = parseInt(readFileSync(PID_PATH, 'utf-8').trim(), 10);
|
|
196
|
+
if (!pid || isNaN(pid)) return null;
|
|
197
|
+
process.kill(pid, 0); // signal 0 = existence check
|
|
198
|
+
return pid;
|
|
199
|
+
} catch {
|
|
200
|
+
removePidFile();
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
183
205
|
/**
|
|
184
206
|
* Kill any stale serve process from a previous MCP server instance.
|
|
185
|
-
*
|
|
186
|
-
*
|
|
207
|
+
* Only called during cleanup (exit/SIGTERM), not during startup —
|
|
208
|
+
* multiple concurrent MCP instances each run their own serve process.
|
|
187
209
|
*/
|
|
188
210
|
function killStaleServeProcess() {
|
|
189
211
|
try {
|
|
@@ -191,11 +213,9 @@ function killStaleServeProcess() {
|
|
|
191
213
|
const stalePid = parseInt(readFileSync(PID_PATH, 'utf-8').trim(), 10);
|
|
192
214
|
if (!stalePid || isNaN(stalePid)) return;
|
|
193
215
|
|
|
194
|
-
// Check if the process is still alive
|
|
195
216
|
try {
|
|
196
|
-
process.kill(stalePid, 0);
|
|
217
|
+
process.kill(stalePid, 0);
|
|
197
218
|
} catch {
|
|
198
|
-
// Process doesn't exist, clean up stale PID file
|
|
199
219
|
removePidFile();
|
|
200
220
|
return;
|
|
201
221
|
}
|
|
@@ -204,14 +224,11 @@ function killStaleServeProcess() {
|
|
|
204
224
|
console.error(`Killing stale serve process (PID ${stalePid})`);
|
|
205
225
|
try { process.kill(stalePid, 'SIGTERM'); } catch {}
|
|
206
226
|
|
|
207
|
-
// Give it a moment, then force kill if still alive
|
|
208
227
|
setTimeout(() => {
|
|
209
228
|
try {
|
|
210
229
|
process.kill(stalePid, 0);
|
|
211
230
|
process.kill(stalePid, 'SIGKILL');
|
|
212
|
-
} catch {
|
|
213
|
-
// Already dead, good
|
|
214
|
-
}
|
|
231
|
+
} catch {}
|
|
215
232
|
}, 2000);
|
|
216
233
|
|
|
217
234
|
removePidFile();
|
|
@@ -224,27 +241,49 @@ function killStaleServeProcess() {
|
|
|
224
241
|
|
|
225
242
|
let reindexInProgress = false;
|
|
226
243
|
let reindexProcess = null;
|
|
244
|
+
let warmupInProgress = true; // true until checkDbFormat + serve process ready
|
|
227
245
|
|
|
228
246
|
/**
|
|
229
247
|
* Check if the database file is compatible with the current binary.
|
|
230
|
-
*
|
|
248
|
+
* Uses a cached result file to avoid running stats (30-60s) on every startup.
|
|
249
|
+
* Cache key: binary path mtime + db file mtime + db size.
|
|
231
250
|
*/
|
|
232
|
-
function checkDbFormat() {
|
|
251
|
+
async function checkDbFormat() {
|
|
233
252
|
if (!existsSync(config.dbPath)) return true;
|
|
234
253
|
|
|
235
254
|
try {
|
|
236
|
-
// Check if file is non-trivial (has actual index data)
|
|
237
255
|
const fstat = statSync(config.dbPath);
|
|
238
|
-
if (fstat.size < 100) return true;
|
|
256
|
+
if (fstat.size < 100) return true;
|
|
239
257
|
|
|
240
|
-
//
|
|
241
|
-
const
|
|
242
|
-
|
|
243
|
-
|
|
258
|
+
// Check cached result — avoids 40s stats command on every MCP startup
|
|
259
|
+
const binaryStat = statSync(config.rustBinary);
|
|
260
|
+
const cacheKey = `${binaryStat.mtimeMs}|${fstat.mtimeMs}|${fstat.size}`;
|
|
261
|
+
try {
|
|
262
|
+
const cached = JSON.parse(readFileSync(FORMAT_CACHE_PATH, 'utf-8'));
|
|
263
|
+
if (cached.key === cacheKey) {
|
|
264
|
+
logToFile('INFO', `Format check cached: ${cached.ok ? 'compatible' : 'incompatible'}`);
|
|
265
|
+
return cached.ok;
|
|
266
|
+
}
|
|
267
|
+
} catch { /* no cache or invalid */ }
|
|
268
|
+
|
|
269
|
+
// Cache miss — run stats (expensive: loads full HNSW graph)
|
|
270
|
+
logToFile('INFO', 'Format check: running stats (this takes 30-60s for large indexes)...');
|
|
271
|
+
const result = await new Promise((resolve, reject) => {
|
|
272
|
+
const proc = spawn(config.rustBinary, ['stats', '-d', config.dbPath],
|
|
273
|
+
{ stdio: ['pipe', 'pipe', 'pipe'], env: rustEnv });
|
|
274
|
+
let stdout = '';
|
|
275
|
+
proc.stdout.on('data', (d) => { stdout += d.toString(); });
|
|
276
|
+
proc.on('error', reject);
|
|
277
|
+
proc.on('exit', (code) => code === 0 ? resolve(stdout) : reject(new Error(`stats exit ${code}`)));
|
|
278
|
+
setTimeout(() => { try { proc.kill(); } catch {} reject(new Error('stats timeout')); }, 120000);
|
|
279
|
+
});
|
|
244
280
|
|
|
245
281
|
const vectors = parseInt(result.match(/Total vectors:\s*(\d+)/)?.[1] || '0');
|
|
246
|
-
|
|
247
|
-
|
|
282
|
+
const ok = vectors > 0;
|
|
283
|
+
|
|
284
|
+
// Write cache
|
|
285
|
+
try { writeFileSync(FORMAT_CACHE_PATH, JSON.stringify({ key: cacheKey, ok })); } catch {}
|
|
286
|
+
return ok;
|
|
248
287
|
} catch {
|
|
249
288
|
return false;
|
|
250
289
|
}
|
|
@@ -561,6 +600,111 @@ function startServeProcess() {
|
|
|
561
600
|
}
|
|
562
601
|
}
|
|
563
602
|
|
|
603
|
+
// ─── Singleton Socket Proxy ──────────────────────────────────────
|
|
604
|
+
// Only one serve process runs per project. Other MCP instances connect
|
|
605
|
+
// to a Unix socket proxy instead of spawning their own serve process.
|
|
606
|
+
|
|
607
|
+
let socketServer = null;
|
|
608
|
+
let isSocketClient = false; // true if we're a secondary instance using the socket
|
|
609
|
+
|
|
610
|
+
/**
|
|
611
|
+
* Start a Unix socket server that proxies queries to the local serve process.
|
|
612
|
+
* Other MCP instances connect here instead of starting their own serve.
|
|
613
|
+
*/
|
|
614
|
+
function startSocketProxy() {
|
|
615
|
+
try { if (existsSync(SOCK_PATH)) unlinkSync(SOCK_PATH); } catch {}
|
|
616
|
+
socketServer = createNetServer((conn) => {
|
|
617
|
+
const rl = createInterface({ input: conn });
|
|
618
|
+
rl.on('line', async (line) => {
|
|
619
|
+
try {
|
|
620
|
+
const req = JSON.parse(line);
|
|
621
|
+
const resp = await serveQuery(req.command, req.params || {}, req.timeout || 30000);
|
|
622
|
+
conn.write(JSON.stringify(resp) + '\n');
|
|
623
|
+
} catch (err) {
|
|
624
|
+
conn.write(JSON.stringify({ ok: false, error: err.message }) + '\n');
|
|
625
|
+
}
|
|
626
|
+
});
|
|
627
|
+
conn.on('error', () => {}); // ignore client disconnect
|
|
628
|
+
});
|
|
629
|
+
socketServer.on('error', (err) => {
|
|
630
|
+
logToFile('WARN', `Socket proxy error: ${err.message}`);
|
|
631
|
+
});
|
|
632
|
+
socketServer.listen(SOCK_PATH, () => {
|
|
633
|
+
logToFile('INFO', `Socket proxy listening on ${SOCK_PATH}`);
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
/**
|
|
638
|
+
* Try to connect to an existing socket proxy (another MCP instance owns the serve process).
|
|
639
|
+
* Returns true if connected successfully — serveQuery will route through the socket.
|
|
640
|
+
*/
|
|
641
|
+
function tryConnectSocket() {
|
|
642
|
+
return new Promise((resolve) => {
|
|
643
|
+
if (!existsSync(SOCK_PATH)) { resolve(false); return; }
|
|
644
|
+
const conn = createConnection(SOCK_PATH);
|
|
645
|
+
const timeout = setTimeout(() => { conn.destroy(); resolve(false); }, 3000);
|
|
646
|
+
conn.on('connect', () => {
|
|
647
|
+
clearTimeout(timeout);
|
|
648
|
+
// Connection works — set up socket-based serveQuery
|
|
649
|
+
const rl = createInterface({ input: conn });
|
|
650
|
+
let pendingResolve = null;
|
|
651
|
+
|
|
652
|
+
rl.on('line', (line) => {
|
|
653
|
+
try {
|
|
654
|
+
const resp = JSON.parse(line);
|
|
655
|
+
if (pendingResolve) { pendingResolve(resp); pendingResolve = null; }
|
|
656
|
+
} catch {}
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
conn.on('error', () => {
|
|
660
|
+
isSocketClient = false;
|
|
661
|
+
serveReady = false;
|
|
662
|
+
logToFile('WARN', 'Socket connection lost — falling back to cold-start');
|
|
663
|
+
});
|
|
664
|
+
conn.on('close', () => {
|
|
665
|
+
isSocketClient = false;
|
|
666
|
+
serveReady = false;
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
// Override serveQuery to route through socket
|
|
670
|
+
const socketQueryQueue = [];
|
|
671
|
+
let socketBusy = false;
|
|
672
|
+
|
|
673
|
+
async function processSocketQueue() {
|
|
674
|
+
if (socketBusy || socketQueryQueue.length === 0) return;
|
|
675
|
+
socketBusy = true;
|
|
676
|
+
const { command, params, timeoutMs, resolve: qResolve, reject: qReject } = socketQueryQueue.shift();
|
|
677
|
+
const timer = setTimeout(() => { pendingResolve = null; qReject(new Error('Socket query timeout')); socketBusy = false; processSocketQueue(); }, timeoutMs);
|
|
678
|
+
pendingResolve = (resp) => { clearTimeout(timer); qResolve(resp); socketBusy = false; processSocketQueue(); };
|
|
679
|
+
try {
|
|
680
|
+
conn.write(JSON.stringify({ command, params, timeout: timeoutMs }) + '\n');
|
|
681
|
+
} catch (err) {
|
|
682
|
+
clearTimeout(timer);
|
|
683
|
+
pendingResolve = null;
|
|
684
|
+
qReject(err);
|
|
685
|
+
socketBusy = false;
|
|
686
|
+
processSocketQueue();
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// Replace the global serveQuery with socket-based version
|
|
691
|
+
globalServeQuery = (command, params, timeoutMs) => new Promise((res, rej) => {
|
|
692
|
+
socketQueryQueue.push({ command, params, timeoutMs, resolve: res, reject: rej });
|
|
693
|
+
processSocketQueue();
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
isSocketClient = true;
|
|
697
|
+
serveReady = true;
|
|
698
|
+
logToFile('INFO', 'Connected to existing serve process via socket proxy');
|
|
699
|
+
resolve(true);
|
|
700
|
+
});
|
|
701
|
+
conn.on('error', () => { clearTimeout(timeout); resolve(false); });
|
|
702
|
+
});
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
// Global reference to serveQuery implementation (local or socket)
|
|
706
|
+
let globalServeQuery = null;
|
|
707
|
+
|
|
564
708
|
function serveQuery(command, params = {}, timeoutMs = 30000) {
|
|
565
709
|
return new Promise((resolve, reject) => {
|
|
566
710
|
const id = serveNextId++;
|
|
@@ -592,10 +736,11 @@ async function rustSearchAsync(query, limit = 10) {
|
|
|
592
736
|
await Promise.race([serveReadyPromise, new Promise(r => setTimeout(() => r(false), 10000))]);
|
|
593
737
|
}
|
|
594
738
|
|
|
595
|
-
// Try
|
|
596
|
-
|
|
739
|
+
// Try socket proxy (secondary instance) or local serve process (primary)
|
|
740
|
+
const queryFn = globalServeQuery || ((serveProcess && serveReady) ? serveQuery : null);
|
|
741
|
+
if (queryFn) {
|
|
597
742
|
try {
|
|
598
|
-
const resp = await
|
|
743
|
+
const resp = await queryFn('search', { query, limit });
|
|
599
744
|
if (resp.ok && Array.isArray(resp.data)) {
|
|
600
745
|
cacheSet(cacheKey, resp.data);
|
|
601
746
|
return resp.data;
|
|
@@ -2328,11 +2473,23 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2328
2473
|
const reqStart = Date.now();
|
|
2329
2474
|
logToFile('REQ', `${name}(${JSON.stringify(args || {})})`);
|
|
2330
2475
|
|
|
2331
|
-
//
|
|
2332
|
-
// If old DB is preserved, searches keep running against it during rebuild.
|
|
2476
|
+
// ── Warmup guard: index compatibility check or serve process still loading ──
|
|
2333
2477
|
const indexFreeTools = ['magento_stats', 'magento_analyze_diff', 'magento_complexity',
|
|
2334
2478
|
'magento_trace_dependency', 'magento_error_parser', 'magento_find_layout',
|
|
2335
2479
|
'magento_impact_analysis', 'magento_find_event_flow', 'magento_find_test'];
|
|
2480
|
+
if (warmupInProgress && !indexFreeTools.includes(name)) {
|
|
2481
|
+
logToFile('REQ', `${name} → blocked (warmup: loading index)`);
|
|
2482
|
+
return {
|
|
2483
|
+
content: [{
|
|
2484
|
+
type: 'text',
|
|
2485
|
+
text: 'Magector is warming up — loading the search index into memory. This takes 30-60 seconds on first startup. Please retry your query in a moment.'
|
|
2486
|
+
}],
|
|
2487
|
+
isError: true,
|
|
2488
|
+
};
|
|
2489
|
+
}
|
|
2490
|
+
|
|
2491
|
+
// Block search tools only when re-indexing AND no usable old DB exists.
|
|
2492
|
+
// If old DB is preserved, searches keep running against it during rebuild.
|
|
2336
2493
|
const hasUsableDb = existsSync(config.dbPath) && (() => { try { return statSync(config.dbPath).size > 100; } catch { return false; } })();
|
|
2337
2494
|
if (reindexInProgress && !hasUsableDb && !indexFreeTools.includes(name)) {
|
|
2338
2495
|
logToFile('REQ', `${name} → blocked (re-indexing, no usable DB)`);
|
|
@@ -3212,56 +3369,68 @@ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
|
3212
3369
|
});
|
|
3213
3370
|
|
|
3214
3371
|
async function main() {
|
|
3215
|
-
//
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
// Check database format compatibility before starting serve process.
|
|
3219
|
-
// With incremental saves, a partial but valid index should be kept — don't
|
|
3220
|
-
// force a full re-index just because the previous session didn't finish.
|
|
3221
|
-
if (existsSync(config.dbPath)) {
|
|
3222
|
-
if (!checkDbFormat()) {
|
|
3223
|
-
logToFile('WARN', 'Database format incompatible — scheduling background re-index');
|
|
3224
|
-
startBackgroundReindex();
|
|
3225
|
-
} else {
|
|
3226
|
-
logToFile('INFO', 'Existing database is compatible — reusing index');
|
|
3227
|
-
}
|
|
3228
|
-
} else if (config.magentoRoot && existsSync(config.magentoRoot)) {
|
|
3229
|
-
// No DB file at all — need initial index
|
|
3230
|
-
logToFile('INFO', 'No index database found — scheduling background index');
|
|
3231
|
-
startBackgroundReindex();
|
|
3232
|
-
}
|
|
3372
|
+
// Don't kill existing serve processes — other MCP instances may be using them.
|
|
3373
|
+
// Each instance starts its own serve process; cleanup happens on exit.
|
|
3233
3374
|
|
|
3234
|
-
//
|
|
3235
|
-
//
|
|
3236
|
-
const
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3375
|
+
// Connect MCP transport FIRST so tools can return "warming up" messages
|
|
3376
|
+
// instead of the client hanging during index load.
|
|
3377
|
+
const transport = new StdioServerTransport();
|
|
3378
|
+
await server.connect(transport);
|
|
3379
|
+
logToFile('INFO', 'Magector MCP server connected (warming up...)');
|
|
3380
|
+
console.error('Magector MCP server connected (warming up...)');
|
|
3381
|
+
|
|
3382
|
+
// ── Singleton serve: try connecting to existing instance first ──
|
|
3383
|
+
try {
|
|
3384
|
+
// Try to connect to an existing serve process via Unix socket
|
|
3385
|
+
const connected = await tryConnectSocket();
|
|
3386
|
+
if (connected) {
|
|
3387
|
+
logToFile('INFO', 'Joined existing serve process (singleton mode)');
|
|
3388
|
+
console.error('Joined existing serve process (singleton mode)');
|
|
3389
|
+
} else {
|
|
3390
|
+
// We are the primary instance — check DB format and start serve
|
|
3391
|
+
if (existsSync(config.dbPath)) {
|
|
3392
|
+
if (!(await checkDbFormat())) {
|
|
3393
|
+
logToFile('WARN', 'Database format incompatible — scheduling background re-index');
|
|
3394
|
+
startBackgroundReindex();
|
|
3248
3395
|
} else {
|
|
3249
|
-
logToFile('
|
|
3250
|
-
console.error('Serve process not ready in time, will use fallback');
|
|
3396
|
+
logToFile('INFO', 'Existing database is compatible — reusing index');
|
|
3251
3397
|
}
|
|
3398
|
+
} else if (config.magentoRoot && existsSync(config.magentoRoot)) {
|
|
3399
|
+
logToFile('INFO', 'No index database found — scheduling background index');
|
|
3400
|
+
startBackgroundReindex();
|
|
3252
3401
|
}
|
|
3253
|
-
} catch {
|
|
3254
|
-
// Non-fatal: falls back to execFileSync per query
|
|
3255
|
-
}
|
|
3256
|
-
}
|
|
3257
3402
|
|
|
3258
|
-
|
|
3259
|
-
|
|
3403
|
+
const canStartServe = !reindexInProgress || (existsSync(config.dbPath) && (() => { try { return statSync(config.dbPath).size > 100; } catch { return false; } })());
|
|
3404
|
+
if (canStartServe) {
|
|
3405
|
+
try {
|
|
3406
|
+
startServeProcess();
|
|
3407
|
+
if (serveReadyPromise) {
|
|
3408
|
+
const ready = await Promise.race([
|
|
3409
|
+
serveReadyPromise,
|
|
3410
|
+
new Promise(r => setTimeout(() => r(false), 15000))
|
|
3411
|
+
]);
|
|
3412
|
+
if (ready) {
|
|
3413
|
+
logToFile('INFO', 'Serve process ready (primary instance)');
|
|
3414
|
+
console.error('Serve process ready (primary instance)');
|
|
3415
|
+
// Start socket proxy so other instances can connect
|
|
3416
|
+
startSocketProxy();
|
|
3417
|
+
} else {
|
|
3418
|
+
logToFile('WARN', 'Serve process not ready in time, will use fallback');
|
|
3419
|
+
console.error('Serve process not ready in time, will use fallback');
|
|
3420
|
+
}
|
|
3421
|
+
}
|
|
3422
|
+
} catch {
|
|
3423
|
+
// Non-fatal: falls back to execFileSync per query
|
|
3424
|
+
}
|
|
3425
|
+
}
|
|
3426
|
+
}
|
|
3260
3427
|
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3428
|
+
await loadDescriptions();
|
|
3429
|
+
} finally {
|
|
3430
|
+
warmupInProgress = false;
|
|
3431
|
+
logToFile('INFO', 'Warmup complete — all tools available');
|
|
3432
|
+
console.error('Warmup complete — all tools available');
|
|
3433
|
+
}
|
|
3265
3434
|
}
|
|
3266
3435
|
|
|
3267
3436
|
// Cleanup on exit — kill all child processes and remove PID file
|
|
@@ -3277,6 +3446,11 @@ function cleanup(reason) {
|
|
|
3277
3446
|
try { reindexProcess.kill(); } catch {}
|
|
3278
3447
|
reindexProcess = null;
|
|
3279
3448
|
}
|
|
3449
|
+
if (socketServer) {
|
|
3450
|
+
try { socketServer.close(); } catch {}
|
|
3451
|
+
try { if (existsSync(SOCK_PATH)) unlinkSync(SOCK_PATH); } catch {}
|
|
3452
|
+
socketServer = null;
|
|
3453
|
+
}
|
|
3280
3454
|
removePidFile();
|
|
3281
3455
|
removeReindexPidFile();
|
|
3282
3456
|
}
|