magector 1.3.5 → 1.4.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.
package/README.md CHANGED
@@ -67,7 +67,7 @@ Without Magector, asking Claude Code or Cursor *"how are checkout totals calcula
67
67
  - **Diff analysis** -- risk scoring and change classification for git commits and staged changes
68
68
  - **Complexity analysis** -- cyclomatic complexity, function count, and hotspot detection across modules
69
69
  - **Fast** -- 10-45ms queries via persistent serve process, batched ONNX embedding with adaptive thread scaling
70
- - **MCP server** -- 19 tools integrating with Claude Code, Cursor, and any MCP-compatible AI tool
70
+ - **MCP server** -- 20 tools integrating with Claude Code, Cursor, and any MCP-compatible AI tool
71
71
  - **Clean architecture** -- Rust core handles all indexing/search, Node.js MCP server delegates to it
72
72
 
73
73
  ---
@@ -84,7 +84,7 @@ flowchart TD
84
84
  A --> B --> C --> D
85
85
  end
86
86
  subgraph node ["Node.js Layer"]
87
- E["MCP Server · 19 tools"]
87
+ E["MCP Server · 20 tools"]
88
88
  F["Persistent Serve"]
89
89
  G["CLI · init/index/search"]
90
90
  E --> F
@@ -279,7 +279,7 @@ npx magector help # Show help
279
279
 
280
280
  ## MCP Server Tools
281
281
 
282
- The MCP server exposes 19 tools for AI-assisted Magento 2 and Adobe Commerce development. All search tools return **structured JSON** with file paths, class names, methods, role badges, and content snippets -- enabling AI clients to parse results programmatically and minimize file-read round-trips.
282
+ The MCP server exposes 20 tools for AI-assisted Magento 2 and Adobe Commerce development. All search tools return **structured JSON** with file paths, class names, methods, role badges, and content snippets -- enabling AI clients to parse results programmatically and minimize file-read round-trips.
283
283
 
284
284
  ### Output Format
285
285
 
@@ -335,6 +335,14 @@ All search tools return structured JSON:
335
335
  | `magento_find_cron` | Find cron job definitions in crontab.xml |
336
336
  | `magento_find_db_schema` | Find database table definitions in db_schema.xml (declarative schema) |
337
337
 
338
+ ### Flow Tracing
339
+
340
+ | Tool | Description |
341
+ |------|-------------|
342
+ | `magento_trace_flow` | Trace execution flow from an entry point (route, API, GraphQL, event, cron) -- maps controller → plugins → observers → templates in one call |
343
+
344
+ Auto-detects entry type from pattern (`/V1/...` → API, `snake_case` → event, `camelCase` → GraphQL, `path/segments` → route), or override with `entryType`. Use `depth: "shallow"` (entry + config + plugins) or `depth: "deep"` (adds observers, layout, templates, DI preferences).
345
+
338
346
  ### Analysis Tools
339
347
 
340
348
  | Tool | Description |
@@ -371,6 +379,12 @@ graph TD
371
379
  gql["find_graphql"] --> cls
372
380
  gql --> mtd
373
381
  ctl["find_controller"] --> cfg
382
+ trc["trace_flow"] -.-> ctl
383
+ trc -.-> plg
384
+ trc -.-> obs
385
+ trc -.-> tpl
386
+ trc -.-> api
387
+ trc -.-> gql
374
388
 
375
389
  style cls fill:#4a90d9,color:#fff
376
390
  style mtd fill:#4a90d9,color:#fff
@@ -384,6 +398,7 @@ graph TD
384
398
  style dbs fill:#9b59b6,color:#fff
385
399
  style gql fill:#9b59b6,color:#fff
386
400
  style ctl fill:#4a90d9,color:#fff
401
+ style trc fill:#2ecc71,color:#000
387
402
  ```
388
403
 
389
404
  ### Query Examples
@@ -406,6 +421,10 @@ magento_find_block("cart totals")
406
421
  magento_find_template("minicart")
407
422
  magento_analyze_diff({ commitHash: "abc123" })
408
423
  magento_complexity({ module: "Magento_Catalog", threshold: 10 })
424
+ magento_trace_flow({ entryPoint: "checkout/cart/add", depth: "deep" })
425
+ magento_trace_flow({ entryPoint: "/V1/products" })
426
+ magento_trace_flow({ entryPoint: "placeOrder", entryType: "graphql" })
427
+ magento_trace_flow({ entryPoint: "sales_order_place_after" })
409
428
  ```
410
429
 
411
430
  ---
@@ -502,7 +521,7 @@ cd rust-core && cargo run --release -- validate -m ./magento2 --skip-index
502
521
  magector/
503
522
  ├── src/ # Node.js source
504
523
  │ ├── cli.js # CLI entry point (npx magector <command>)
505
- │ ├── mcp-server.js # MCP server (19 tools, structured JSON output)
524
+ │ ├── mcp-server.js # MCP server (20 tools, structured JSON output)
506
525
  │ ├── binary.js # Platform binary resolver
507
526
  │ ├── model.js # ONNX model resolver/downloader
508
527
  │ ├── init.js # Full init command (index + IDE config)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "magector",
3
- "version": "1.3.5",
3
+ "version": "1.4.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": "1.3.5",
37
- "@magector/cli-linux-x64": "1.3.5",
38
- "@magector/cli-linux-arm64": "1.3.5",
39
- "@magector/cli-win32-x64": "1.3.5"
36
+ "@magector/cli-darwin-arm64": "1.4.1",
37
+ "@magector/cli-linux-x64": "1.4.1",
38
+ "@magector/cli-linux-arm64": "1.4.1",
39
+ "@magector/cli-win32-x64": "1.4.1"
40
40
  },
41
41
  "keywords": [
42
42
  "magento",
package/src/init.js CHANGED
@@ -54,7 +54,7 @@ function detectIDEs(projectPath) {
54
54
  function writeMcpConfig(projectPath, ides, dbPath) {
55
55
  const mcpEntry = {
56
56
  command: 'npx',
57
- args: ['-y', 'magector', 'mcp'],
57
+ args: ['-y', 'magector@latest', 'mcp'],
58
58
  env: {
59
59
  MAGENTO_ROOT: projectPath,
60
60
  MAGECTOR_DB: dbPath
package/src/mcp-server.js CHANGED
@@ -302,6 +302,285 @@ function rerank(results, boosts = {}, weight = 0.3) {
302
302
  }).sort((a, b) => b.score - a.score);
303
303
  }
304
304
 
305
+ // ─── Trace Flow helpers ─────────────────────────────────────────
306
+
307
+ function detectEntryType(entryPoint) {
308
+ if (/^\/?V\d/.test(entryPoint)) return 'api';
309
+ if (!entryPoint.includes('/') && /^[a-z][a-z0-9]*(_[a-z0-9]+)+$/.test(entryPoint)) return 'event';
310
+ if (!entryPoint.includes('/') && /^[a-z]/.test(entryPoint) && /[A-Z]/.test(entryPoint)) return 'graphql';
311
+ if (entryPoint.includes('/')) return 'route';
312
+ return 'route';
313
+ }
314
+
315
+ /** Wrapper that never throws — returns [] on failure so trace steps are independent. */
316
+ async function safeSearch(query, limit = 10) {
317
+ try {
318
+ return await rustSearchAsync(query, limit);
319
+ } catch {
320
+ return [];
321
+ }
322
+ }
323
+
324
+ async function traceRoute(entryPoint, depth) {
325
+ const parts = entryPoint.replace(/^\//, '').split('/');
326
+ const nameParts = parts.map(p => p.charAt(0).toUpperCase() + p.slice(1));
327
+ const trace = {};
328
+
329
+ // Controller + route config (independent, run in parallel)
330
+ const [controllerRaw, routeRaw] = await Promise.all([
331
+ safeSearch(`${nameParts.join(' ')} controller execute action`, 30),
332
+ safeSearch(`routes.xml ${parts[0]}`, 20)
333
+ ]);
334
+ const controllers = controllerRaw.map(normalizeResult).filter(r => r.path?.includes('/Controller/'));
335
+ // Boost results matching route path segments
336
+ const ranked = controllers.map(r => {
337
+ const bonus = nameParts.filter(p => r.path?.includes(p)).length * 0.3;
338
+ return { ...r, score: (r.score || 0) + bonus };
339
+ }).sort((a, b) => b.score - a.score);
340
+ const best = ranked[0];
341
+ if (best) {
342
+ trace.controller = { path: best.path, className: best.className || null, methods: best.methods || [] };
343
+ }
344
+
345
+ const routeConfigs = routeRaw.map(normalizeResult).filter(r => r.path?.includes('routes.xml'));
346
+ if (routeConfigs.length > 0) {
347
+ trace.routeConfig = routeConfigs.slice(0, 5).map(r => ({ path: r.path, snippet: (r.searchText || '').slice(0, 200) }));
348
+ }
349
+
350
+ // Plugins on controller
351
+ if (best?.className) {
352
+ const pluginRaw = await safeSearch(`plugin interceptor ${best.className}`, 20);
353
+ const plugins = pluginRaw.map(normalizeResult).filter(r => r.isPlugin || r.path?.includes('/Plugin/') || r.path?.includes('di.xml'));
354
+ if (plugins.length > 0) {
355
+ trace.plugins = plugins.slice(0, 10).map(r => ({ path: r.path, className: r.className || null, methods: r.methods || [] }));
356
+ }
357
+ }
358
+
359
+ if (depth === 'deep') {
360
+ const moduleName = best?.module || nameParts[0];
361
+ const eventName = parts.join('_');
362
+ const layoutHandle = parts.join('_');
363
+
364
+ // All deep searches are independent — run in parallel
365
+ const [diRaw, obsRaw, layoutRaw, tplRaw] = await Promise.all([
366
+ safeSearch(`di.xml preference ${moduleName}`, 20),
367
+ safeSearch(`event ${eventName} observer`, 20),
368
+ safeSearch(`layout ${layoutHandle}`, 20),
369
+ safeSearch(`${nameParts.join(' ')} template phtml`, 20)
370
+ ]);
371
+
372
+ const prefs = diRaw.map(normalizeResult).filter(r => r.path?.includes('di.xml'));
373
+ if (prefs.length > 0) {
374
+ trace.preferences = prefs.slice(0, 10).map(r => ({ path: r.path, snippet: (r.searchText || '').slice(0, 200) }));
375
+ }
376
+
377
+ const observers = obsRaw.map(normalizeResult).filter(r => r.isObserver || r.path?.includes('/Observer/') || r.path?.includes('events.xml'));
378
+ if (observers.length > 0) {
379
+ trace.observers = observers.slice(0, 10).map(r => ({ eventName: r.searchText?.match(/event\s+name="([^"]+)"/)?.[1] || eventName, path: r.path, className: r.className || null }));
380
+ }
381
+
382
+ const layouts = layoutRaw.map(normalizeResult).filter(r => r.path?.includes('/layout/'));
383
+ if (layouts.length > 0) {
384
+ trace.layout = layouts.slice(0, 10).map(r => ({ path: r.path }));
385
+ }
386
+
387
+ const templates = tplRaw.map(normalizeResult).filter(r => r.path?.includes('.phtml'));
388
+ if (templates.length > 0) {
389
+ trace.templates = templates.slice(0, 10).map(r => ({ path: r.path }));
390
+ }
391
+ }
392
+
393
+ return trace;
394
+ }
395
+
396
+ async function traceApi(entryPoint, depth) {
397
+ const trace = {};
398
+
399
+ // webapi.xml
400
+ const webapiRaw = await safeSearch(`webapi route ${entryPoint}`, 20);
401
+ const webapis = webapiRaw.map(normalizeResult).filter(r => r.path?.includes('webapi.xml'));
402
+ if (webapis.length > 0) {
403
+ trace.webapiConfig = webapis.slice(0, 5).map(r => ({ path: r.path, snippet: (r.searchText || '').slice(0, 300) }));
404
+ }
405
+
406
+ // Service class — extract from webapi searchText
407
+ let serviceClassName = null;
408
+ for (const w of webapis) {
409
+ const match = (w.searchText || '').match(/service\s+class="([^"]+)"/);
410
+ if (match) { serviceClassName = match[1]; break; }
411
+ }
412
+ let serviceShortName = null;
413
+ if (serviceClassName) {
414
+ serviceShortName = serviceClassName.split('\\').pop();
415
+ const svcRaw = await safeSearch(serviceShortName, 10);
416
+ const svcs = svcRaw.map(normalizeResult).filter(r => r.className?.includes(serviceShortName));
417
+ if (svcs.length > 0) {
418
+ trace.serviceClass = { path: svcs[0].path, className: svcs[0].className || serviceClassName, methods: svcs[0].methods || [] };
419
+ }
420
+ }
421
+
422
+ if (depth === 'deep') {
423
+ const resource = entryPoint.replace(/^\/?V\d+\//, '').split('/')[0];
424
+
425
+ // Plugins + observers are independent — run in parallel
426
+ const searches = [safeSearch(`event ${resource} observer`, 20)];
427
+ if (serviceClassName) {
428
+ searches.push(safeSearch(`plugin interceptor ${serviceClassName}`, 20));
429
+ }
430
+ const [obsRaw, pluginRaw] = await Promise.all(searches);
431
+
432
+ if (pluginRaw) {
433
+ const plugins = pluginRaw.map(normalizeResult).filter(r => r.isPlugin || r.path?.includes('/Plugin/') || r.path?.includes('di.xml'));
434
+ if (plugins.length > 0) {
435
+ trace.plugins = plugins.slice(0, 10).map(r => ({ path: r.path, className: r.className || null, methods: r.methods || [] }));
436
+ }
437
+ }
438
+
439
+ const observers = obsRaw.map(normalizeResult).filter(r => r.isObserver || r.path?.includes('/Observer/') || r.path?.includes('events.xml'));
440
+ if (observers.length > 0) {
441
+ trace.observers = observers.slice(0, 10).map(r => ({ eventName: resource, path: r.path, className: r.className || null }));
442
+ }
443
+ }
444
+
445
+ return trace;
446
+ }
447
+
448
+ async function traceGraphql(entryPoint, depth) {
449
+ const trace = {};
450
+
451
+ // Schema + resolver (independent, run in parallel)
452
+ const [schemaRaw, resolverRaw] = await Promise.all([
453
+ safeSearch(`graphql ${entryPoint} mutation query`, 20),
454
+ safeSearch(`${entryPoint} resolver`, 20)
455
+ ]);
456
+ const schemas = schemaRaw.map(normalizeResult).filter(r => r.path?.includes('.graphqls') || r.type === 'graphql');
457
+ if (schemas.length > 0) {
458
+ trace.schema = schemas.slice(0, 5).map(r => ({ path: r.path, snippet: (r.searchText || '').slice(0, 300) }));
459
+ }
460
+
461
+ const resolvers = resolverRaw.map(normalizeResult).filter(r => r.isResolver || r.path?.includes('/Resolver/'));
462
+ if (resolvers.length > 0) {
463
+ trace.resolver = { path: resolvers[0].path, className: resolvers[0].className || null, methods: resolvers[0].methods || [] };
464
+ }
465
+
466
+ if (depth === 'deep' && resolvers[0]?.className) {
467
+ const pluginRaw = await safeSearch(`plugin interceptor ${resolvers[0].className}`, 20);
468
+ const plugins = pluginRaw.map(normalizeResult).filter(r => r.isPlugin || r.path?.includes('/Plugin/') || r.path?.includes('di.xml'));
469
+ if (plugins.length > 0) {
470
+ trace.plugins = plugins.slice(0, 10).map(r => ({ path: r.path, className: r.className || null, methods: r.methods || [] }));
471
+ }
472
+ }
473
+
474
+ return trace;
475
+ }
476
+
477
+ async function traceEvent(entryPoint, depth) {
478
+ const trace = {};
479
+
480
+ // Observers
481
+ const obsRaw = await safeSearch(`event ${entryPoint} observer`, 30);
482
+ const observers = obsRaw.map(normalizeResult).filter(r => r.isObserver || r.path?.includes('/Observer/') || r.path?.includes('events.xml'));
483
+ if (observers.length > 0) {
484
+ trace.observers = observers.slice(0, 15).map(r => ({ eventName: entryPoint, path: r.path, className: r.className || null }));
485
+ }
486
+
487
+ if (depth === 'deep') {
488
+ // Origin — infer source model from event prefix
489
+ const prefix = entryPoint.split('_').slice(0, 2).join('_');
490
+ const originParts = prefix.split('_').map(p => p.charAt(0).toUpperCase() + p.slice(1));
491
+ const originRaw = await safeSearch(`${originParts.join(' ')} model`, 10);
492
+ const origins = originRaw.map(normalizeResult).filter(r => r.isModel || r.path?.includes('/Model/'));
493
+ if (origins.length > 0) {
494
+ trace.origin = { path: origins[0].path, className: origins[0].className || null, methods: origins[0].methods || [] };
495
+ }
496
+ }
497
+
498
+ return trace;
499
+ }
500
+
501
+ async function traceCron(entryPoint, depth) {
502
+ const trace = {};
503
+
504
+ // crontab.xml + handler class (independent, run in parallel)
505
+ const handlerParts = entryPoint.split('_').map(p => p.charAt(0).toUpperCase() + p.slice(1));
506
+ const [cronRaw, handlerRaw] = await Promise.all([
507
+ safeSearch(`cron job ${entryPoint}`, 20),
508
+ safeSearch(`${handlerParts.join(' ')} cron`, 20)
509
+ ]);
510
+ const cronConfigs = cronRaw.map(normalizeResult).filter(r => r.path?.includes('crontab.xml'));
511
+ if (cronConfigs.length > 0) {
512
+ trace.cronConfig = cronConfigs.slice(0, 5).map(r => ({ path: r.path, snippet: (r.searchText || '').slice(0, 200) }));
513
+ }
514
+
515
+ const handlers = handlerRaw.map(normalizeResult).filter(r => r.path?.includes('/Cron/'));
516
+ if (handlers.length > 0) {
517
+ trace.handler = { path: handlers[0].path, className: handlers[0].className || null, methods: handlers[0].methods || [] };
518
+ }
519
+
520
+ if (depth === 'deep' && handlers[0]?.className) {
521
+ const pluginRaw = await safeSearch(`plugin interceptor ${handlers[0].className}`, 20);
522
+ const plugins = pluginRaw.map(normalizeResult).filter(r => r.isPlugin || r.path?.includes('/Plugin/') || r.path?.includes('di.xml'));
523
+ if (plugins.length > 0) {
524
+ trace.plugins = plugins.slice(0, 10).map(r => ({ path: r.path, className: r.className || null, methods: r.methods || [] }));
525
+ }
526
+ }
527
+
528
+ return trace;
529
+ }
530
+
531
+ async function traceFlow(entryPoint, entryType, depth) {
532
+ const type = entryType === 'auto' ? detectEntryType(entryPoint) : entryType;
533
+
534
+ let trace;
535
+ switch (type) {
536
+ case 'route': trace = await traceRoute(entryPoint, depth); break;
537
+ case 'api': trace = await traceApi(entryPoint, depth); break;
538
+ case 'graphql': trace = await traceGraphql(entryPoint, depth); break;
539
+ case 'event': trace = await traceEvent(entryPoint, depth); break;
540
+ case 'cron': trace = await traceCron(entryPoint, depth); break;
541
+ default: trace = await traceRoute(entryPoint, depth); break;
542
+ }
543
+
544
+ return { entryPoint, entryType: type, trace };
545
+ }
546
+
547
+ function buildTraceSummary(result) {
548
+ const { entryPoint, entryType, trace } = result;
549
+ const parts = [];
550
+
551
+ switch (entryType) {
552
+ case 'route':
553
+ parts.push(`Route ${entryPoint}`);
554
+ if (trace.controller) parts.push(trace.controller.className ? `${trace.controller.className}::execute()` : trace.controller.path);
555
+ break;
556
+ case 'api':
557
+ parts.push(`API ${entryPoint}`);
558
+ if (trace.serviceClass) parts.push(trace.serviceClass.className || trace.serviceClass.path);
559
+ break;
560
+ case 'graphql':
561
+ parts.push(`GraphQL ${entryPoint}`);
562
+ if (trace.resolver) parts.push(trace.resolver.className || trace.resolver.path);
563
+ break;
564
+ case 'event':
565
+ parts.push(`Event ${entryPoint}`);
566
+ break;
567
+ case 'cron':
568
+ parts.push(`Cron ${entryPoint}`);
569
+ if (trace.handler) parts.push(trace.handler.className || trace.handler.path);
570
+ break;
571
+ }
572
+
573
+ const counts = [];
574
+ if (trace.plugins?.length) counts.push(`${trace.plugins.length} plugin${trace.plugins.length > 1 ? 's' : ''}`);
575
+ if (trace.observers?.length) counts.push(`${trace.observers.length} observer${trace.observers.length > 1 ? 's' : ''}`);
576
+ if (trace.templates?.length) counts.push(`${trace.templates.length} template${trace.templates.length > 1 ? 's' : ''}`);
577
+ if (trace.layout?.length) counts.push(`${trace.layout.length} layout${trace.layout.length > 1 ? 's' : ''}`);
578
+ if (trace.preferences?.length) counts.push(`${trace.preferences.length} preference${trace.preferences.length > 1 ? 's' : ''}`);
579
+ if (counts.length > 0) parts.push(counts.join(', '));
580
+
581
+ return parts.join(' → ');
582
+ }
583
+
305
584
  function formatSearchResults(results) {
306
585
  if (!results || results.length === 0) {
307
586
  return JSON.stringify({ results: [], count: 0 });
@@ -674,6 +953,32 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
674
953
  }
675
954
  }
676
955
  }
956
+ },
957
+ {
958
+ name: 'magento_trace_flow',
959
+ description: 'Trace Magento execution flow from an entry point (route, API endpoint, GraphQL mutation, event, or cron job). Chains multiple searches to map controller → plugins → observers → templates for a given request path. Use this to understand how a request is processed end-to-end.',
960
+ inputSchema: {
961
+ type: 'object',
962
+ properties: {
963
+ entryPoint: {
964
+ type: 'string',
965
+ description: 'The entry point to trace. Examples: "checkout/cart/add" (route), "/V1/products" (API), "placeOrder" (GraphQL), "sales_order_place_after" (event), "catalog_product_reindex" (cron)'
966
+ },
967
+ entryType: {
968
+ type: 'string',
969
+ enum: ['auto', 'route', 'api', 'graphql', 'event', 'cron'],
970
+ description: 'Type of entry point. "auto" detects from the pattern (default). Override when auto-detection is wrong.',
971
+ default: 'auto'
972
+ },
973
+ depth: {
974
+ type: 'string',
975
+ enum: ['shallow', 'deep'],
976
+ description: 'Trace depth. "shallow" traces entry point + config + direct plugins (faster). "deep" adds observers, layout, templates, and DI preferences (more complete). Default: shallow.',
977
+ default: 'shallow'
978
+ }
979
+ },
980
+ required: ['entryPoint']
981
+ }
677
982
  }
678
983
  ]
679
984
  }));
@@ -1134,6 +1439,22 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1134
1439
  return { content: [{ type: 'text', text }] };
1135
1440
  }
1136
1441
 
1442
+ case 'magento_trace_flow': {
1443
+ const entryPoint = args.entryPoint;
1444
+ const entryType = args.entryType || 'auto';
1445
+ const depth = args.depth || 'shallow';
1446
+
1447
+ const result = await traceFlow(entryPoint, entryType, depth);
1448
+ result.summary = buildTraceSummary(result);
1449
+
1450
+ return {
1451
+ content: [{
1452
+ type: 'text',
1453
+ text: JSON.stringify(result)
1454
+ }]
1455
+ };
1456
+ }
1457
+
1137
1458
  default:
1138
1459
  return {
1139
1460
  content: [{