magector 2.1.1 → 2.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/package.json +5 -5
  2. package/src/mcp-server.js +71 -4
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "magector",
3
- "version": "2.1.1",
3
+ "version": "2.1.3",
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.1.1",
43
- "@magector/cli-linux-x64": "2.1.1",
44
- "@magector/cli-linux-arm64": "2.1.1",
45
- "@magector/cli-win32-x64": "2.1.1"
42
+ "@magector/cli-darwin-arm64": "2.1.3",
43
+ "@magector/cli-linux-x64": "2.1.3",
44
+ "@magector/cli-linux-arm64": "2.1.3",
45
+ "@magector/cli-win32-x64": "2.1.3"
46
46
  },
47
47
  "keywords": [
48
48
  "magento",
package/src/mcp-server.js CHANGED
@@ -121,8 +121,14 @@ function extractJson(stdout) {
121
121
  }
122
122
  // Fallback: try parsing the entire output (handles multi-line JSON)
123
123
  // Strip lines that look like tracing (start with ANSI escape or timestamp bracket)
124
+ // Also skip non-JSON prefix lines (e.g. "setting number of points 50000" from HNSW)
125
+ const logLineRe = /^\s*(\x1b\[|\[[\d\-T:.Z]+)/;
126
+ const jsonStartRe = /^\s*[\[{"\-0-9tfn]/;
127
+ let startIdx = lines.findIndex(l => l.trim() && !logLineRe.test(l) && jsonStartRe.test(l));
128
+ if (startIdx < 0) startIdx = 0;
124
129
  const cleaned = lines
125
- .filter(l => !l.match(/^\s*(\x1b\[|\[[\d\-T:.Z]+)/) && l.trim())
130
+ .slice(startIdx)
131
+ .filter(l => !logLineRe.test(l) && l.trim())
126
132
  .join('\n')
127
133
  .trim();
128
134
  if (cleaned) {
@@ -135,6 +141,7 @@ function extractJson(stdout) {
135
141
  // Track the serve process PID to clean up orphans on restart.
136
142
 
137
143
  const PID_PATH = path.join(config.magentoRoot, '.magector', 'serve.pid');
144
+ const REINDEX_PID_PATH = path.join(config.magentoRoot, '.magector', 'reindex.pid');
138
145
 
139
146
  /**
140
147
  * Write the serve process PID to disk so future instances can clean up orphans.
@@ -147,6 +154,32 @@ function removePidFile() {
147
154
  try { if (existsSync(PID_PATH)) unlinkSync(PID_PATH); } catch {}
148
155
  }
149
156
 
157
+ function writeReindexPidFile(pid) {
158
+ try { writeFileSync(REINDEX_PID_PATH, String(pid)); } catch {}
159
+ }
160
+
161
+ function removeReindexPidFile() {
162
+ try { if (existsSync(REINDEX_PID_PATH)) unlinkSync(REINDEX_PID_PATH); } catch {}
163
+ }
164
+
165
+ /**
166
+ * Check if another reindex process is already running (from another MCP instance).
167
+ * Returns the PID if alive, null otherwise.
168
+ */
169
+ function getRunningReindexPid() {
170
+ try {
171
+ if (!existsSync(REINDEX_PID_PATH)) return null;
172
+ const pid = parseInt(readFileSync(REINDEX_PID_PATH, 'utf-8').trim(), 10);
173
+ if (!pid || isNaN(pid)) return null;
174
+ process.kill(pid, 0); // signal 0 = existence check
175
+ return pid;
176
+ } catch {
177
+ // Process doesn't exist or PID file unreadable — clean up
178
+ removeReindexPidFile();
179
+ return null;
180
+ }
181
+ }
182
+
150
183
  /**
151
184
  * Kill any stale serve process from a previous MCP server instance.
152
185
  * This handles the common case where the MCP server was killed without
@@ -223,6 +256,27 @@ function checkDbFormat() {
223
256
  */
224
257
  function startBackgroundReindex() {
225
258
  if (reindexInProgress) return;
259
+
260
+ // Check if another MCP instance is already running a reindex
261
+ const existingPid = getRunningReindexPid();
262
+ if (existingPid) {
263
+ logToFile('INFO', `Reindex already running in another process (PID ${existingPid}) — skipping`);
264
+ console.error(`Reindex already running (PID ${existingPid}) — skipping`);
265
+ reindexInProgress = true; // mark locally so tools know
266
+ // Poll the external process and react when it finishes
267
+ const pollInterval = setInterval(() => {
268
+ if (!getRunningReindexPid()) {
269
+ clearInterval(pollInterval);
270
+ reindexInProgress = false;
271
+ logToFile('INFO', 'External reindex finished. Restarting serve process.');
272
+ if (serveProcess) serveProcess.kill();
273
+ searchCache.clear();
274
+ startServeProcess();
275
+ }
276
+ }, 10000);
277
+ return;
278
+ }
279
+
226
280
  if (!config.magentoRoot || !existsSync(config.magentoRoot)) {
227
281
  const msg = 'Cannot auto-reindex: MAGENTO_ROOT not set or not found';
228
282
  console.error(msg);
@@ -263,6 +317,9 @@ function startBackgroundReindex() {
263
317
  env: rustEnv,
264
318
  });
265
319
 
320
+ // Write PID file so other MCP instances know a reindex is running
321
+ writeReindexPidFile(reindexProcess.pid);
322
+
266
323
  reindexProcess.stdout.on('data', (d) => {
267
324
  const text = d.toString().replace(/\x1b\[[0-9;]*m/g, '').trim();
268
325
  if (text) logToFile('INDEX', text);
@@ -275,6 +332,7 @@ function startBackgroundReindex() {
275
332
  reindexProcess.on('exit', (code) => {
276
333
  reindexInProgress = false;
277
334
  reindexProcess = null;
335
+ removeReindexPidFile();
278
336
  if (code === 0) {
279
337
  // Atomic swap: old → .bak, new → current
280
338
  try {
@@ -305,6 +363,7 @@ function startBackgroundReindex() {
305
363
  reindexProcess.on('error', (err) => {
306
364
  reindexInProgress = false;
307
365
  reindexProcess = null;
366
+ removeReindexPidFile();
308
367
  logToFile('ERR', `Background re-index error: ${err.message}`);
309
368
  console.error(`Background re-index error: ${err.message}`);
310
369
  });
@@ -500,7 +559,8 @@ async function rustSearchAsync(query, limit = 10) {
500
559
  const cacheKey = `${query}|${limit}`;
501
560
  if (searchCache.has(cacheKey)) {
502
561
  logToFile('CACHE', `HIT: "${query}" (limit=${limit})`);
503
- return searchCache.get(cacheKey);
562
+ const cached = searchCache.get(cacheKey);
563
+ return Array.isArray(cached) ? cached : [];
504
564
  }
505
565
 
506
566
  // Wait for serve process if it's starting up but not yet ready
@@ -513,7 +573,7 @@ async function rustSearchAsync(query, limit = 10) {
513
573
  if (serveProcess && serveReady) {
514
574
  try {
515
575
  const resp = await serveQuery('search', { query, limit });
516
- if (resp.ok && resp.data) {
576
+ if (resp.ok && Array.isArray(resp.data)) {
517
577
  cacheSet(cacheKey, resp.data);
518
578
  return resp.data;
519
579
  }
@@ -524,7 +584,13 @@ async function rustSearchAsync(query, limit = 10) {
524
584
 
525
585
  // Fallback: cold-start execFileSync
526
586
  logToFile('INFO', `Using execFileSync fallback for search: "${query}"`);
527
- return rustSearchSync(query, limit);
587
+ try {
588
+ const result = rustSearchSync(query, limit);
589
+ return Array.isArray(result) ? result : [];
590
+ } catch (err) {
591
+ logToFile('WARN', `execFileSync fallback failed: ${err.message}`);
592
+ return [];
593
+ }
528
594
  }
529
595
 
530
596
  function rustSearchSync(query, limit = 10) {
@@ -3189,6 +3255,7 @@ function cleanup(reason) {
3189
3255
  reindexProcess = null;
3190
3256
  }
3191
3257
  removePidFile();
3258
+ removeReindexPidFile();
3192
3259
  }
3193
3260
 
3194
3261
  process.on('exit', () => cleanup('process exit'));