magector 2.6.0 → 2.6.2
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/cli.js +8 -0
- package/src/mcp-server.js +188 -26
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "magector",
|
|
3
|
-
"version": "2.6.
|
|
3
|
+
"version": "2.6.2",
|
|
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.
|
|
37
|
-
"@magector/cli-linux-x64": "2.6.
|
|
38
|
-
"@magector/cli-linux-arm64": "2.6.
|
|
39
|
-
"@magector/cli-win32-x64": "2.6.
|
|
36
|
+
"@magector/cli-darwin-arm64": "2.6.2",
|
|
37
|
+
"@magector/cli-linux-x64": "2.6.2",
|
|
38
|
+
"@magector/cli-linux-arm64": "2.6.2",
|
|
39
|
+
"@magector/cli-win32-x64": "2.6.2"
|
|
40
40
|
},
|
|
41
41
|
"keywords": [
|
|
42
42
|
"magento",
|
package/src/cli.js
CHANGED
|
@@ -12,6 +12,8 @@ import { resolveBinary } from './binary.js';
|
|
|
12
12
|
import { ensureModels, resolveModels } from './model.js';
|
|
13
13
|
import { init, setup } from './init.js';
|
|
14
14
|
import { checkForUpdate } from './update.js';
|
|
15
|
+
import { createRequire } from 'module';
|
|
16
|
+
const __cliPkg = createRequire(import.meta.url)('../package.json');
|
|
15
17
|
|
|
16
18
|
const args = process.argv.slice(2);
|
|
17
19
|
const command = args[0];
|
|
@@ -327,6 +329,12 @@ async function main() {
|
|
|
327
329
|
await import('./validation/benchmark.js');
|
|
328
330
|
break;
|
|
329
331
|
|
|
332
|
+
case 'version':
|
|
333
|
+
case '--version':
|
|
334
|
+
case '-V':
|
|
335
|
+
console.log(`magector v${__cliPkg.version}`);
|
|
336
|
+
break;
|
|
337
|
+
|
|
330
338
|
case 'help':
|
|
331
339
|
case '--help':
|
|
332
340
|
case '-h':
|
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,
|
|
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
|
-
|
|
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 [];
|
|
@@ -2261,7 +2279,20 @@ async function analyzeImpact(className) {
|
|
|
2261
2279
|
// Use vector search to find candidate files (much faster than globbing all PHP)
|
|
2262
2280
|
const rawSearch = await rustSearchAsync(`${shortName} ${className}`, 50).catch(() => []);
|
|
2263
2281
|
const raw = Array.isArray(rawSearch) ? rawSearch : [];
|
|
2264
|
-
|
|
2282
|
+
let relatedPaths = raw.map(r => normalizeResult(r)).filter(r => r.path);
|
|
2283
|
+
|
|
2284
|
+
// Filesystem fallback: if vector search found too few files, find the class file via glob
|
|
2285
|
+
if (relatedPaths.length < 5 && root) {
|
|
2286
|
+
try {
|
|
2287
|
+
const classFiles = await glob(`**/${shortName}.php`, { cwd: root, absolute: false, nodir: true });
|
|
2288
|
+
const existingPaths = new Set(relatedPaths.map(r => r.path));
|
|
2289
|
+
for (const f of classFiles) {
|
|
2290
|
+
if (!existingPaths.has(f)) {
|
|
2291
|
+
relatedPaths.push({ path: f, className: shortName, score: 0.3 });
|
|
2292
|
+
}
|
|
2293
|
+
}
|
|
2294
|
+
} catch {}
|
|
2295
|
+
}
|
|
2265
2296
|
|
|
2266
2297
|
// Check DI references via xml parsing
|
|
2267
2298
|
const diTrace = await traceDependency(className, 'both');
|
|
@@ -4057,10 +4088,49 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
4057
4088
|
const query = `${args.className} ${ns}`.trim();
|
|
4058
4089
|
const raw = await rustSearchAsync(query, 30);
|
|
4059
4090
|
const classLower = args.className.toLowerCase();
|
|
4060
|
-
|
|
4091
|
+
let results = raw.map(normalizeResult).filter(r =>
|
|
4061
4092
|
r.className?.toLowerCase().includes(classLower) ||
|
|
4062
4093
|
r.path?.toLowerCase().includes(classLower.replace(/\\/g, '/'))
|
|
4063
4094
|
);
|
|
4095
|
+
|
|
4096
|
+
// Filesystem fallback: if vector search found nothing, glob for ClassName.php
|
|
4097
|
+
if (results.length === 0 && config.magentoRoot) {
|
|
4098
|
+
const shortName = args.className.split('\\').pop();
|
|
4099
|
+
const globPattern = `**/${shortName}.php`;
|
|
4100
|
+
try {
|
|
4101
|
+
const files = await glob(globPattern, { cwd: config.magentoRoot, absolute: false, nodir: true, ignore: ['**/test/**', '**/tests/**', '**/Test/**'] });
|
|
4102
|
+
// Filter by namespace if provided
|
|
4103
|
+
const nsLower = ns.toLowerCase().replace(/\\\\/g, '/').replace(/\\/g, '/');
|
|
4104
|
+
const matched = files.filter(f => {
|
|
4105
|
+
if (!nsLower) return true;
|
|
4106
|
+
return f.toLowerCase().includes(nsLower);
|
|
4107
|
+
}).slice(0, 10);
|
|
4108
|
+
// Build result entries from file paths
|
|
4109
|
+
for (const filePath of matched) {
|
|
4110
|
+
const absPath = path.join(config.magentoRoot, filePath);
|
|
4111
|
+
let className = shortName;
|
|
4112
|
+
try {
|
|
4113
|
+
const content = readFileSync(absPath, 'utf-8');
|
|
4114
|
+
const nsMatch = content.match(/namespace\s+([\w\\]+)/);
|
|
4115
|
+
if (nsMatch) className = nsMatch[1] + '\\' + shortName;
|
|
4116
|
+
const methodsFound = [];
|
|
4117
|
+
const methodRegex = /public\s+function\s+(\w+)\s*\(/g;
|
|
4118
|
+
let mm;
|
|
4119
|
+
while ((mm = methodRegex.exec(content)) !== null) methodsFound.push(mm[1]);
|
|
4120
|
+
results.push({
|
|
4121
|
+
path: filePath,
|
|
4122
|
+
className,
|
|
4123
|
+
methods: methodsFound,
|
|
4124
|
+
score: 0.5,
|
|
4125
|
+
searchText: content.slice(0, 300)
|
|
4126
|
+
});
|
|
4127
|
+
} catch {
|
|
4128
|
+
results.push({ path: filePath, className, score: 0.5 });
|
|
4129
|
+
}
|
|
4130
|
+
}
|
|
4131
|
+
} catch {}
|
|
4132
|
+
}
|
|
4133
|
+
|
|
4064
4134
|
return {
|
|
4065
4135
|
content: [{
|
|
4066
4136
|
type: 'text',
|
|
@@ -4209,14 +4279,20 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
4209
4279
|
const diFiles = await getDiXmlFiles(fpRoot);
|
|
4210
4280
|
// Normalize target class for matching (both \ and \\)
|
|
4211
4281
|
const normalizedTarget = args.targetClass.replace(/\\\\/g, '\\');
|
|
4282
|
+
const isFqcn = normalizedTarget.includes('\\');
|
|
4283
|
+
const shortTarget = normalizedTarget.split('\\').pop().toLowerCase();
|
|
4212
4284
|
for (const { content, relPath } of diFiles) {
|
|
4213
|
-
if (!content.includes(normalizedTarget)) continue;
|
|
4285
|
+
if (!content.includes(isFqcn ? normalizedTarget : args.targetClass)) continue;
|
|
4214
4286
|
// Find plugin registrations for this target
|
|
4215
4287
|
const typeBlockRegex = /<type\s+name="([^"]+)"[^>]*>([\s\S]*?)<\/type>/g;
|
|
4216
4288
|
let tm;
|
|
4217
4289
|
while ((tm = typeBlockRegex.exec(content)) !== null) {
|
|
4218
4290
|
const typeName = tm[1].replace(/\\\\/g, '\\');
|
|
4219
|
-
if
|
|
4291
|
+
// FQCN: exact match. Short name: match if type ends with the short name
|
|
4292
|
+
const typeMatches = isFqcn
|
|
4293
|
+
? typeName === normalizedTarget
|
|
4294
|
+
: typeName.split('\\').pop().toLowerCase() === shortTarget;
|
|
4295
|
+
if (!typeMatches) continue;
|
|
4220
4296
|
const block = tm[2];
|
|
4221
4297
|
const pluginRegex = /<plugin\s+([^/>]*)\/?>/g;
|
|
4222
4298
|
let pm;
|
|
@@ -4288,19 +4364,35 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
4288
4364
|
}
|
|
4289
4365
|
|
|
4290
4366
|
case 'magento_find_observer': {
|
|
4291
|
-
|
|
4292
|
-
const
|
|
4293
|
-
let
|
|
4294
|
-
|
|
4295
|
-
)
|
|
4296
|
-
|
|
4367
|
+
// Primary: parse events.xml for exact event name match (structural, not semantic)
|
|
4368
|
+
const eventFlow = await traceEventFlow(args.eventName);
|
|
4369
|
+
let text = '';
|
|
4370
|
+
|
|
4371
|
+
if (eventFlow.observers.length > 0) {
|
|
4372
|
+
text += `### Observers for \`${args.eventName}\` (${eventFlow.observers.length})\n\n`;
|
|
4373
|
+
for (const obs of eventFlow.observers) {
|
|
4374
|
+
text += `- **${obs.name}** → \`${obs.instance}::${obs.method}()\` (${obs.file})\n`;
|
|
4375
|
+
}
|
|
4376
|
+
if (eventFlow.observerDetails.length > 0) {
|
|
4377
|
+
text += `\n### Observer PHP Files\n`;
|
|
4378
|
+
for (const det of eventFlow.observerDetails) {
|
|
4379
|
+
text += `- \`${det.instance}\` → ${det.path}\n`;
|
|
4380
|
+
}
|
|
4381
|
+
}
|
|
4382
|
+
}
|
|
4297
4383
|
|
|
4298
|
-
|
|
4299
|
-
|
|
4300
|
-
|
|
4301
|
-
|
|
4302
|
-
|
|
4303
|
-
|
|
4384
|
+
// Fallback: semantic search if events.xml parsing found nothing
|
|
4385
|
+
if (eventFlow.observers.length === 0) {
|
|
4386
|
+
const query = `event ${args.eventName} observer`;
|
|
4387
|
+
const raw = await rustSearchAsync(query, 30);
|
|
4388
|
+
let results = raw.map(normalizeResult).filter(r =>
|
|
4389
|
+
r.isObserver || r.path?.includes('/Observer/') || r.path?.includes('events.xml')
|
|
4390
|
+
);
|
|
4391
|
+
results = rerank(results, { isObserver: true, pathContains: ['events.xml', '/Observer/'] });
|
|
4392
|
+
text = formatSearchResults(results.slice(0, 15));
|
|
4393
|
+
}
|
|
4394
|
+
|
|
4395
|
+
return { content: [{ type: 'text', text }] };
|
|
4304
4396
|
}
|
|
4305
4397
|
|
|
4306
4398
|
case 'magento_find_preference': {
|
|
@@ -4592,18 +4684,40 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
4592
4684
|
// Support both app/code (Magento/Catalog/) and vendor (module-catalog/) paths
|
|
4593
4685
|
const modulePath = args.moduleName.replace('_', '/') + '/';
|
|
4594
4686
|
const parts = args.moduleName.split('_');
|
|
4687
|
+
// Hyphenate camelCase for vendor path: OrderSplit → order-split
|
|
4595
4688
|
const vendorPath = parts.length === 2
|
|
4596
|
-
? `module-${parts[1].toLowerCase()}/`
|
|
4689
|
+
? `module-${parts[1].replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase()}/`
|
|
4597
4690
|
: '';
|
|
4598
|
-
|
|
4599
|
-
const
|
|
4691
|
+
let results = raw.map(normalizeResult).filter(r => {
|
|
4692
|
+
const p = r.path || '';
|
|
4600
4693
|
const mod = r.module || '';
|
|
4601
4694
|
// Exact module match or directory-level path match (trailing slash prevents Catalog matching CatalogRule)
|
|
4602
4695
|
return mod === args.moduleName ||
|
|
4603
|
-
|
|
4604
|
-
(vendorPath &&
|
|
4696
|
+
p.includes(modulePath) ||
|
|
4697
|
+
(vendorPath && p.toLowerCase().includes(vendorPath));
|
|
4605
4698
|
});
|
|
4606
4699
|
|
|
4700
|
+
// Filesystem fallback: if vector search found nothing, glob the module directory
|
|
4701
|
+
if (results.length === 0 && config.magentoRoot && vendorPath) {
|
|
4702
|
+
try {
|
|
4703
|
+
const vendorGlob = `**/${vendorPath}**/*.{php,xml,phtml}`;
|
|
4704
|
+
const files = await glob(vendorGlob, { cwd: config.magentoRoot, absolute: false, nodir: true });
|
|
4705
|
+
for (const f of files.slice(0, 100)) {
|
|
4706
|
+
const entry = { path: f, score: 0.5 };
|
|
4707
|
+
if (f.includes('/Controller/')) entry.isController = true;
|
|
4708
|
+
if (f.includes('/Model/')) entry.isModel = true;
|
|
4709
|
+
if (f.includes('/Block/')) entry.isBlock = true;
|
|
4710
|
+
if (f.includes('/Plugin/')) entry.isPlugin = true;
|
|
4711
|
+
if (f.includes('/Observer/')) entry.isObserver = true;
|
|
4712
|
+
if (f.endsWith('.xml')) entry.type = 'xml';
|
|
4713
|
+
// Extract class name from path
|
|
4714
|
+
const phpMatch = f.match(/\/([A-Z]\w+)\.php$/);
|
|
4715
|
+
if (phpMatch) entry.className = phpMatch[1];
|
|
4716
|
+
results.push(entry);
|
|
4717
|
+
}
|
|
4718
|
+
} catch {}
|
|
4719
|
+
}
|
|
4720
|
+
|
|
4607
4721
|
const structure = {
|
|
4608
4722
|
controllers: results.filter(r => r.isController || r.path?.includes('/Controller/')),
|
|
4609
4723
|
models: results.filter(r => r.isModel || (r.path?.includes('/Model/') && !r.path?.includes('ResourceModel'))),
|
|
@@ -5450,9 +5564,19 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
5450
5564
|
const qr = `${a.className} ${ns}`.trim();
|
|
5451
5565
|
const raw = await rustSearchAsync(qr, 30);
|
|
5452
5566
|
const cl = a.className.toLowerCase();
|
|
5453
|
-
|
|
5567
|
+
let res = raw.map(normalizeResult).filter(r =>
|
|
5454
5568
|
r.className?.toLowerCase().includes(cl) || r.path?.toLowerCase().includes(cl.replace(/\\/g, '/'))
|
|
5455
5569
|
);
|
|
5570
|
+
// Filesystem fallback for batch find_class
|
|
5571
|
+
if (res.length === 0 && config.magentoRoot) {
|
|
5572
|
+
const shortName = a.className.split('\\').pop();
|
|
5573
|
+
try {
|
|
5574
|
+
const files = await glob(`**/${shortName}.php`, { cwd: config.magentoRoot, absolute: false, nodir: true });
|
|
5575
|
+
for (const f of files.slice(0, 5)) {
|
|
5576
|
+
res.push({ path: f, className: shortName, score: 0.5 });
|
|
5577
|
+
}
|
|
5578
|
+
} catch {}
|
|
5579
|
+
}
|
|
5456
5580
|
text = formatSearchResults(res.slice(0, 5));
|
|
5457
5581
|
break;
|
|
5458
5582
|
}
|
|
@@ -5533,6 +5657,34 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
5533
5657
|
text = formatSearchResults(res.slice(0, 5));
|
|
5534
5658
|
break;
|
|
5535
5659
|
}
|
|
5660
|
+
case 'magento_module_structure': {
|
|
5661
|
+
const raw = await rustSearchAsync(a.moduleName, 200);
|
|
5662
|
+
const modulePath = a.moduleName.replace('_', '/') + '/';
|
|
5663
|
+
const parts = a.moduleName.split('_');
|
|
5664
|
+
const vendorPath = parts.length === 2 ? `module-${parts[1].toLowerCase()}/` : '';
|
|
5665
|
+
const res = raw.map(normalizeResult).filter(r => {
|
|
5666
|
+
const p = r.path || '';
|
|
5667
|
+
const mod = r.module || '';
|
|
5668
|
+
return mod === a.moduleName || p.includes(modulePath) || (vendorPath && p.toLowerCase().includes(vendorPath));
|
|
5669
|
+
});
|
|
5670
|
+
text = `Module: ${a.moduleName} (${res.length} files)\n`;
|
|
5671
|
+
const cats = { controllers: '/Controller/', models: '/Model/', plugins: '/Plugin/', observers: '/Observer/', api: '/Api/' };
|
|
5672
|
+
for (const [cat, pattern] of Object.entries(cats)) {
|
|
5673
|
+
const matches = res.filter(r => r.path?.includes(pattern));
|
|
5674
|
+
if (matches.length > 0) {
|
|
5675
|
+
text += `${cat}: ${matches.length} (${matches.slice(0, 3).map(r => r.className || r.path?.split('/').pop()).join(', ')})\n`;
|
|
5676
|
+
}
|
|
5677
|
+
}
|
|
5678
|
+
break;
|
|
5679
|
+
}
|
|
5680
|
+
case 'magento_find_observer': {
|
|
5681
|
+
const flow = await traceEventFlow(a.eventName);
|
|
5682
|
+
text = `Observers: ${flow.observers.length}\n`;
|
|
5683
|
+
for (const o of flow.observers.slice(0, 10)) {
|
|
5684
|
+
text += `- ${o.name}: ${o.instance}::${o.method}() (${o.file})\n`;
|
|
5685
|
+
}
|
|
5686
|
+
break;
|
|
5687
|
+
}
|
|
5536
5688
|
default:
|
|
5537
5689
|
text = `Unsupported batch tool: ${q.tool}`;
|
|
5538
5690
|
}
|
|
@@ -5625,6 +5777,16 @@ async function main() {
|
|
|
5625
5777
|
try {
|
|
5626
5778
|
let role = 'secondary';
|
|
5627
5779
|
|
|
5780
|
+
// Kill stale serve process if version mismatch (e.g., user upgraded Magector)
|
|
5781
|
+
const staleVersion = getServePidVersion();
|
|
5782
|
+
if (staleVersion && staleVersion !== __pkg.version) {
|
|
5783
|
+
logToFile('WARN', `Serve process version mismatch: ${staleVersion} vs ${__pkg.version} — killing stale process`);
|
|
5784
|
+
console.error(`Killing stale serve process (version ${staleVersion}, current ${__pkg.version})`);
|
|
5785
|
+
killStaleServeProcess();
|
|
5786
|
+
// Remove stale socket so we don't connect to it
|
|
5787
|
+
try { if (existsSync(SOCK_PATH)) unlinkSync(SOCK_PATH); } catch {}
|
|
5788
|
+
}
|
|
5789
|
+
|
|
5628
5790
|
const connected = await tryConnectSocket();
|
|
5629
5791
|
if (connected) {
|
|
5630
5792
|
logToFile('INFO', 'Joined existing serve process via socket (secondary)');
|