magector 2.6.0 → 2.6.1

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 +96 -18
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "magector",
3
- "version": "2.6.0",
3
+ "version": "2.6.1",
4
4
  "description": "Semantic code search for Magento 2 — index, search, MCP server",
5
5
  "type": "module",
6
6
  "main": "src/mcp-server.js",
@@ -33,10 +33,10 @@
33
33
  "ruvector": "^0.1.96"
34
34
  },
35
35
  "optionalDependencies": {
36
- "@magector/cli-darwin-arm64": "2.6.0",
37
- "@magector/cli-linux-x64": "2.6.0",
38
- "@magector/cli-linux-arm64": "2.6.0",
39
- "@magector/cli-win32-x64": "2.6.0"
36
+ "@magector/cli-darwin-arm64": "2.6.1",
37
+ "@magector/cli-linux-x64": "2.6.1",
38
+ "@magector/cli-linux-arm64": "2.6.1",
39
+ "@magector/cli-win32-x64": "2.6.1"
40
40
  },
41
41
  "keywords": [
42
42
  "magento",
package/src/mcp-server.js CHANGED
@@ -202,7 +202,16 @@ function releasePrimaryLock() {
202
202
  * Write the serve process PID to disk so future instances can clean up orphans.
203
203
  */
204
204
  function writePidFile(pid) {
205
- try { writeFileSync(PID_PATH, String(pid)); } catch {}
205
+ try { writeFileSync(PID_PATH, `${pid}\n${__pkg.version}`); } catch {}
206
+ }
207
+
208
+ function getServePidVersion() {
209
+ try {
210
+ if (!existsSync(PID_PATH)) return null;
211
+ const content = readFileSync(PID_PATH, 'utf-8').trim();
212
+ const lines = content.split('\n');
213
+ return lines[1] || null;
214
+ } catch { return null; }
206
215
  }
207
216
 
208
217
  function removePidFile() {
@@ -809,20 +818,29 @@ async function rustSearchAsync(query, limit = 10) {
809
818
  if (queryFn) {
810
819
  try {
811
820
  const resp = await queryFn('search', { query, limit });
812
- if (resp.ok && Array.isArray(resp.data)) {
821
+ if (resp.ok && Array.isArray(resp.data) && resp.data.length > 0) {
813
822
  cacheSet(cacheKey, resp.data);
814
823
  return resp.data;
815
824
  }
825
+ // Serve returned empty results — fall through to execFileSync
826
+ // This catches stale serve processes with wrong/empty index
827
+ if (resp.ok && Array.isArray(resp.data) && resp.data.length === 0) {
828
+ logToFile('WARN', `Serve returned 0 results for "${query}" — trying execFileSync fallback`);
829
+ }
816
830
  } catch (err) {
817
831
  logToFile('WARN', `Serve query failed, falling back to execFileSync: ${err.message}`);
818
832
  }
819
833
  }
820
834
 
821
- // Fallback: cold-start execFileSync
835
+ // Fallback: cold-start execFileSync (always works if CLI works)
822
836
  logToFile('INFO', `Using execFileSync fallback for search: "${query}"`);
823
837
  try {
824
838
  const result = rustSearchSync(query, limit);
825
- return Array.isArray(result) ? result : [];
839
+ const arr = Array.isArray(result) ? result : [];
840
+ if (arr.length > 0) {
841
+ cacheSet(cacheKey, arr);
842
+ }
843
+ return arr;
826
844
  } catch (err) {
827
845
  logToFile('WARN', `execFileSync fallback failed: ${err.message}`);
828
846
  return [];
@@ -4209,14 +4227,20 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
4209
4227
  const diFiles = await getDiXmlFiles(fpRoot);
4210
4228
  // Normalize target class for matching (both \ and \\)
4211
4229
  const normalizedTarget = args.targetClass.replace(/\\\\/g, '\\');
4230
+ const isFqcn = normalizedTarget.includes('\\');
4231
+ const shortTarget = normalizedTarget.split('\\').pop().toLowerCase();
4212
4232
  for (const { content, relPath } of diFiles) {
4213
- if (!content.includes(normalizedTarget)) continue;
4233
+ if (!content.includes(isFqcn ? normalizedTarget : args.targetClass)) continue;
4214
4234
  // Find plugin registrations for this target
4215
4235
  const typeBlockRegex = /<type\s+name="([^"]+)"[^>]*>([\s\S]*?)<\/type>/g;
4216
4236
  let tm;
4217
4237
  while ((tm = typeBlockRegex.exec(content)) !== null) {
4218
4238
  const typeName = tm[1].replace(/\\\\/g, '\\');
4219
- if (typeName !== normalizedTarget) continue;
4239
+ // FQCN: exact match. Short name: match if type ends with the short name
4240
+ const typeMatches = isFqcn
4241
+ ? typeName === normalizedTarget
4242
+ : typeName.split('\\').pop().toLowerCase() === shortTarget;
4243
+ if (!typeMatches) continue;
4220
4244
  const block = tm[2];
4221
4245
  const pluginRegex = /<plugin\s+([^/>]*)\/?>/g;
4222
4246
  let pm;
@@ -4288,19 +4312,35 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
4288
4312
  }
4289
4313
 
4290
4314
  case 'magento_find_observer': {
4291
- const query = `event ${args.eventName} observer`;
4292
- const raw = await rustSearchAsync(query, 30);
4293
- let results = raw.map(normalizeResult).filter(r =>
4294
- r.isObserver || r.path?.includes('/Observer/') || r.path?.includes('events.xml')
4295
- );
4296
- results = rerank(results, { isObserver: true, pathContains: ['events.xml', '/Observer/'] });
4315
+ // Primary: parse events.xml for exact event name match (structural, not semantic)
4316
+ const eventFlow = await traceEventFlow(args.eventName);
4317
+ let text = '';
4318
+
4319
+ if (eventFlow.observers.length > 0) {
4320
+ text += `### Observers for \`${args.eventName}\` (${eventFlow.observers.length})\n\n`;
4321
+ for (const obs of eventFlow.observers) {
4322
+ text += `- **${obs.name}** → \`${obs.instance}::${obs.method}()\` (${obs.file})\n`;
4323
+ }
4324
+ if (eventFlow.observerDetails.length > 0) {
4325
+ text += `\n### Observer PHP Files\n`;
4326
+ for (const det of eventFlow.observerDetails) {
4327
+ text += `- \`${det.instance}\` → ${det.path}\n`;
4328
+ }
4329
+ }
4330
+ }
4297
4331
 
4298
- return {
4299
- content: [{
4300
- type: 'text',
4301
- text: formatSearchResults(results.slice(0, 15))
4302
- }]
4303
- };
4332
+ // Fallback: semantic search if events.xml parsing found nothing
4333
+ if (eventFlow.observers.length === 0) {
4334
+ const query = `event ${args.eventName} observer`;
4335
+ const raw = await rustSearchAsync(query, 30);
4336
+ let results = raw.map(normalizeResult).filter(r =>
4337
+ r.isObserver || r.path?.includes('/Observer/') || r.path?.includes('events.xml')
4338
+ );
4339
+ results = rerank(results, { isObserver: true, pathContains: ['events.xml', '/Observer/'] });
4340
+ text = formatSearchResults(results.slice(0, 15));
4341
+ }
4342
+
4343
+ return { content: [{ type: 'text', text }] };
4304
4344
  }
4305
4345
 
4306
4346
  case 'magento_find_preference': {
@@ -5533,6 +5573,34 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
5533
5573
  text = formatSearchResults(res.slice(0, 5));
5534
5574
  break;
5535
5575
  }
5576
+ case 'magento_module_structure': {
5577
+ const raw = await rustSearchAsync(a.moduleName, 200);
5578
+ const modulePath = a.moduleName.replace('_', '/') + '/';
5579
+ const parts = a.moduleName.split('_');
5580
+ const vendorPath = parts.length === 2 ? `module-${parts[1].toLowerCase()}/` : '';
5581
+ const res = raw.map(normalizeResult).filter(r => {
5582
+ const p = r.path || '';
5583
+ const mod = r.module || '';
5584
+ return mod === a.moduleName || p.includes(modulePath) || (vendorPath && p.toLowerCase().includes(vendorPath));
5585
+ });
5586
+ text = `Module: ${a.moduleName} (${res.length} files)\n`;
5587
+ const cats = { controllers: '/Controller/', models: '/Model/', plugins: '/Plugin/', observers: '/Observer/', api: '/Api/' };
5588
+ for (const [cat, pattern] of Object.entries(cats)) {
5589
+ const matches = res.filter(r => r.path?.includes(pattern));
5590
+ if (matches.length > 0) {
5591
+ text += `${cat}: ${matches.length} (${matches.slice(0, 3).map(r => r.className || r.path?.split('/').pop()).join(', ')})\n`;
5592
+ }
5593
+ }
5594
+ break;
5595
+ }
5596
+ case 'magento_find_observer': {
5597
+ const flow = await traceEventFlow(a.eventName);
5598
+ text = `Observers: ${flow.observers.length}\n`;
5599
+ for (const o of flow.observers.slice(0, 10)) {
5600
+ text += `- ${o.name}: ${o.instance}::${o.method}() (${o.file})\n`;
5601
+ }
5602
+ break;
5603
+ }
5536
5604
  default:
5537
5605
  text = `Unsupported batch tool: ${q.tool}`;
5538
5606
  }
@@ -5625,6 +5693,16 @@ async function main() {
5625
5693
  try {
5626
5694
  let role = 'secondary';
5627
5695
 
5696
+ // Kill stale serve process if version mismatch (e.g., user upgraded Magector)
5697
+ const staleVersion = getServePidVersion();
5698
+ if (staleVersion && staleVersion !== __pkg.version) {
5699
+ logToFile('WARN', `Serve process version mismatch: ${staleVersion} vs ${__pkg.version} — killing stale process`);
5700
+ console.error(`Killing stale serve process (version ${staleVersion}, current ${__pkg.version})`);
5701
+ killStaleServeProcess();
5702
+ // Remove stale socket so we don't connect to it
5703
+ try { if (existsSync(SOCK_PATH)) unlinkSync(SOCK_PATH); } catch {}
5704
+ }
5705
+
5628
5706
  const connected = await tryConnectSocket();
5629
5707
  if (connected) {
5630
5708
  logToFile('INFO', 'Joined existing serve process via socket (secondary)');