magector 1.5.0 → 1.5.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
@@ -66,23 +66,27 @@ The result: your AI assistant calls one MCP tool and gets ranked, pattern-enrich
66
66
  ## Architecture
67
67
 
68
68
  ```mermaid
69
- flowchart TD
70
- subgraph rust ["Rust Core"]
71
- A["AST Parser · PHP + JS"]
72
- B["Pattern Detection · 20+"]
73
- B2["Description Enrichment"]
74
- C["ONNX Embedder · 384d"]
75
- D["HNSW + Reranking"]
76
- A --> B --> B2 --> C --> D
77
- end
69
+ flowchart LR
78
70
  subgraph node ["Node.js Layer"]
79
- E["MCP Server · 21 tools"]
80
- F["Persistent Serve"]
81
- G["CLI · init/index/search/describe"]
82
- E --> F
71
+ direction TB
72
+ G["CLI<br/>init · index · search · describe"]
73
+ E["MCP Server<br/>21 tools · LRU cache"]
74
+ F["Persistent Serve Process"]
83
75
  G --> F
76
+ E --> F
77
+ end
78
+
79
+ F -->|"stdin/stdout JSON"| rust
80
+
81
+ subgraph rust ["Rust Core"]
82
+ direction TB
83
+ A["AST Parser<br/>PHP · JS · XML"]
84
+ B["Pattern Detection<br/>20+ Magento patterns"]
85
+ B2["Description Enrichment<br/>LLM-powered di.xml summaries"]
86
+ C["ONNX Embedder<br/>all-MiniLM-L6-v2 · 384d"]
87
+ D["HNSW Vector Search<br/>hybrid reranking · SONA"]
88
+ A --> B --> B2 --> C --> D
84
89
  end
85
- node -->|stdin/stdout JSON| rust
86
90
 
87
91
  style rust fill:#f4a460,color:#000
88
92
  style node fill:#68b684,color:#000
@@ -91,29 +95,28 @@ flowchart TD
91
95
  ### Indexing Pipeline
92
96
 
93
97
  ```mermaid
94
- flowchart TD
95
- A[Source File] --> B[AST Parser]
96
- B --> C[Pattern Detection]
97
- C --> D[Text Enrichment]
98
- D --> D2{Description DB?}
99
- D2 -->|Yes| D3["Prepend Description"]
100
- D2 -->|No| E[ONNX Embedding]
98
+ flowchart LR
99
+ A["Source File"] --> B["AST Parser"]
100
+ B --> C["Pattern Detection"]
101
+ C --> D["Text Enrichment"]
102
+ D --> D2{"Descriptions DB?"}
103
+ D2 -->|Yes| D3["Prepend LLM Description"]
104
+ D2 -->|No| E["ONNX Embedding"]
101
105
  D3 --> E
102
- E --> F[(HNSW Index)]
103
- A --> G[Metadata]
104
- G --> F
106
+ E --> F[("HNSW Index")]
107
+ A --> G["Metadata"] --> F
105
108
  ```
106
109
 
107
110
  ### Search Pipeline
108
111
 
109
112
  ```mermaid
110
- flowchart TD
111
- Q[Query] --> E1[Synonym Enrichment]
112
- E1 --> E2[ONNX Embedding]
113
- E2 --> H[HNSW Search]
114
- H --> R[Hybrid Reranking]
115
- R --> SA[SONA Adjustment + MicroLoRA]
116
- SA --> J[Structured JSON]
113
+ flowchart LR
114
+ Q["Query"] --> E1["Synonym Enrichment"]
115
+ E1 --> E2["ONNX Embedding"]
116
+ E2 --> H["HNSW Search"]
117
+ H --> R["Hybrid Reranking"]
118
+ R --> SA["SONA Adjustment"]
119
+ SA --> J["Structured JSON"]
117
120
  ```
118
121
 
119
122
  ### Components
@@ -148,13 +151,14 @@ npx magector init
148
151
  This single command handles the entire setup:
149
152
 
150
153
  ```mermaid
151
- flowchart TD
152
- A["npx magector init"] --> B[Verify Project]
153
- B --> C[Download Model]
154
- C --> D[Index Codebase]
155
- D --> E[Detect IDE]
156
- E --> F[Write Config]
157
- F --> G[Update .gitignore]
154
+ flowchart LR
155
+ A["npx magector init"] --> B["Verify<br/>Project"]
156
+ B --> C["Download<br/>ONNX Model"]
157
+ C --> D["Index<br/>Codebase"]
158
+ D --> E["Detect IDE<br/>Cursor · Claude Code"]
159
+ E --> E2["API Key<br/>(optional)"]
160
+ E2 --> F["Write MCP<br/>Config"]
161
+ F --> G["Update<br/>.gitignore"]
158
162
  ```
159
163
 
160
164
  ### 2. Search
@@ -304,7 +308,7 @@ npx magector mcp # Start MCP server
304
308
  npx magector help # Show help
305
309
  ```
306
310
 
307
- The `describe` command requires `ANTHROPIC_API_KEY`. After running `describe`, the next `index` automatically picks up the descriptions DB and embeds them into the vectors.
311
+ The `describe` command and `magento_describe` MCP tool require an Anthropic API key. During `npx magector init`, you are prompted to paste your key (optional). If provided, it is stored in the MCP config file as the `ANTHROPIC_API_KEY` environment variable so the MCP server can use it automatically. You can also set it manually later by adding `"ANTHROPIC_API_KEY": "sk-..."` to the `env` section in `.mcp.json` or `~/.cursor/mcp.json`.
308
312
 
309
313
  ### Environment Variables
310
314
 
@@ -405,7 +409,7 @@ Auto-detects entry type from pattern (`/V1/...` → API, `snake_case` → event,
405
409
  Each tool description includes "See also" hints to help AI clients chain tools effectively:
406
410
 
407
411
  ```mermaid
408
- graph TD
412
+ graph LR
409
413
  cls["find_class"] --> plg["find_plugin"]
410
414
  cls --> prf["find_preference"]
411
415
  cls --> mtd["find_method"]
@@ -635,20 +639,16 @@ Magector scans every `.php`, `.js`, `.xml`, `.phtml`, and `.graphqls` file in a
635
639
  The MCP server spawns a persistent Rust process (`magector-core serve`) that keeps the ONNX model and HNSW index loaded in memory. Queries are sent as JSON over stdin and responses returned via stdout -- eliminating the ~2.6s cold-start overhead of loading the model per query. Falls back to single-shot `execFileSync` if the serve process is unavailable.
636
640
 
637
641
  ```mermaid
638
- flowchart TD
642
+ flowchart LR
639
643
  subgraph startup ["Startup (once)"]
640
- S1[Load Model] --> S2[Load Index]
641
- S2 --> S3[Ready Signal]
644
+ S1["Load Model"] --> S2["Load Index"] --> S3["Ready Signal"]
642
645
  end
646
+ startup --> query
643
647
  subgraph query ["Per Query (10-45ms)"]
644
- Q1[stdin JSON] --> Q2[Embed]
645
- Q2 --> Q3[HNSW Search]
646
- Q3 --> Q4[Rerank]
647
- Q4 --> Q5[stdout JSON]
648
+ Q1["stdin JSON"] --> Q2["Embed"] --> Q3["HNSW Search"] --> Q4["Rerank"] --> Q5["stdout JSON"]
648
649
  end
649
- startup --> query
650
650
  subgraph fallback ["Fallback"]
651
- F1[execFileSync ~2.6s]
651
+ F1["execFileSync ~2.6s"]
652
652
  end
653
653
 
654
654
  style startup fill:#e8f4e8,color:#000
@@ -663,17 +663,12 @@ When the serve process is started with `--magento-root`, a background thread pol
663
663
  Since `hnsw_rs` does not support point deletion, Magector uses a **tombstone** strategy: old vectors for modified/deleted files are marked as tombstoned and filtered out of search results. New vectors are appended. When tombstoned entries exceed 20% of total vectors, the HNSW graph is automatically rebuilt (compacted) to reclaim memory and restore search performance.
664
664
 
665
665
  ```mermaid
666
- flowchart TD
667
- W1[Sleep 60s] --> W2[Scan Filesystem]
668
- W2 --> W3{Changes?}
666
+ flowchart LR
667
+ W1["Sleep 60s"] --> W2["Scan Filesystem"] --> W3{"Changes?"}
669
668
  W3 -->|No| W1
670
- W3 -->|Yes| W4[Tombstone Old Vectors]
671
- W4 --> W5[Parse + Embed New Files]
672
- W5 --> W6[Append to HNSW]
673
- W6 --> W7{Tombstone > 20%?}
674
- W7 -->|Yes| W8[Compact / Rebuild HNSW]
675
- W7 -->|No| W9[Save to Disk]
676
- W8 --> W9
669
+ W3 -->|Yes| W4["Tombstone Old Vectors"] --> W5["Parse + Embed New Files"] --> W6["Append to HNSW"] --> W7{"Tombstone > 20%?"}
670
+ W7 -->|Yes| W8["Compact / Rebuild HNSW"] --> W9["Save to Disk"]
671
+ W7 -->|No| W9
677
672
  W9 --> W1
678
673
 
679
674
  style W4 fill:#f4e8e8,color:#000
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "magector",
3
- "version": "1.5.0",
3
+ "version": "1.5.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",
@@ -37,10 +37,10 @@
37
37
  "ruvector": "^0.1.96"
38
38
  },
39
39
  "optionalDependencies": {
40
- "@magector/cli-darwin-arm64": "1.5.0",
41
- "@magector/cli-linux-x64": "1.5.0",
42
- "@magector/cli-linux-arm64": "1.5.0",
43
- "@magector/cli-win32-x64": "1.5.0"
40
+ "@magector/cli-darwin-arm64": "1.5.1",
41
+ "@magector/cli-linux-x64": "1.5.1",
42
+ "@magector/cli-linux-arm64": "1.5.1",
43
+ "@magector/cli-win32-x64": "1.5.1"
44
44
  },
45
45
  "keywords": [
46
46
  "magento",
package/src/cli.js CHANGED
@@ -11,6 +11,7 @@ import path from 'path';
11
11
  import { resolveBinary } from './binary.js';
12
12
  import { ensureModels, resolveModels } from './model.js';
13
13
  import { init, setup } from './init.js';
14
+ import { checkForUpdate } from './update.js';
14
15
 
15
16
  const args = process.argv.slice(2);
16
17
  const command = args[0];
@@ -192,6 +193,9 @@ async function runDescribe(targetPath) {
192
193
  }
193
194
 
194
195
  async function main() {
196
+ // Auto-update: check npm for newer version, re-exec if found
197
+ await checkForUpdate(command, args);
198
+
195
199
  switch (command) {
196
200
  case 'init':
197
201
  await init(args[1]);
package/src/init.js CHANGED
@@ -3,13 +3,29 @@
3
3
  */
4
4
  import { existsSync, readFileSync, writeFileSync, mkdirSync, appendFileSync } from 'fs';
5
5
  import { execFileSync } from 'child_process';
6
+ import { createInterface } from 'readline';
6
7
  import { homedir } from 'os';
7
8
  import path from 'path';
9
+ import { fileURLToPath } from 'url';
8
10
  import { resolveBinary } from './binary.js';
9
11
  import { ensureModels } from './model.js';
10
12
  import { CURSOR_RULES_MDC } from './templates/cursor-rules-mdc.js';
11
13
  import { CLAUDE_MD } from './templates/claude-md.js';
12
14
 
15
+ /**
16
+ * Prompt the user for input. Returns empty string if stdin is not a TTY.
17
+ */
18
+ function askQuestion(question) {
19
+ if (!process.stdin.isTTY) return Promise.resolve('');
20
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
21
+ return new Promise(resolve => {
22
+ rl.question(question, answer => {
23
+ rl.close();
24
+ resolve(answer.trim());
25
+ });
26
+ });
27
+ }
28
+
13
29
  /**
14
30
  * Detect if the given path is a Magento 2 project root.
15
31
  */
@@ -51,14 +67,18 @@ function detectIDEs(projectPath) {
51
67
  /**
52
68
  * Write MCP server configuration for the given IDE(s).
53
69
  */
54
- function writeMcpConfig(projectPath, ides, dbPath) {
70
+ function writeMcpConfig(projectPath, ides, dbPath, { anthropicApiKey } = {}) {
71
+ const env = {
72
+ MAGENTO_ROOT: projectPath,
73
+ MAGECTOR_DB: dbPath
74
+ };
75
+ if (anthropicApiKey) {
76
+ env.ANTHROPIC_API_KEY = anthropicApiKey;
77
+ }
55
78
  const mcpEntry = {
56
79
  command: 'npx',
57
80
  args: ['-y', 'magector@latest', 'mcp'],
58
- env: {
59
- MAGENTO_ROOT: projectPath,
60
- MAGECTOR_DB: dbPath
61
- }
81
+ env
62
82
  };
63
83
 
64
84
  const written = [];
@@ -184,7 +204,9 @@ export async function init(projectPath) {
184
204
  mkdirSync(path.join(projectPath, '.magector'), { recursive: true });
185
205
  const dbPath = path.join(projectPath, '.magector', 'index.db');
186
206
 
187
- console.log('\nMagector Init\n');
207
+ const pkgPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'package.json');
208
+ const version = existsSync(pkgPath) ? JSON.parse(readFileSync(pkgPath, 'utf-8')).version : 'dev';
209
+ console.log(`\nMagector Init v${version}\n`);
188
210
 
189
211
  // 1. Verify Magento project
190
212
  console.log('Checking Magento project...');
@@ -248,23 +270,32 @@ export async function init(projectPath) {
248
270
  if (ideNames.length === 0) ideNames.push('Cursor', 'Claude Code');
249
271
  console.log(` Detected: ${ideNames.join(' + ') || 'none (configuring both)'}`);
250
272
 
251
- // 6. Write MCP config
273
+ // 6. Optional: Anthropic API key for LLM description enrichment
274
+ let anthropicApiKey = '';
275
+ anthropicApiKey = await askQuestion('\nAnthropic API key (optional, enables LLM enrichment — press Enter to skip): ');
276
+ if (anthropicApiKey) {
277
+ console.log(' API key will be stored in MCP config.');
278
+ } else {
279
+ console.log(' Skipped. You can add ANTHROPIC_API_KEY to MCP config later.');
280
+ }
281
+
282
+ // 7. Write MCP config
252
283
  console.log('\nWriting MCP config...');
253
- const mcpFiles = writeMcpConfig(projectPath, ides, dbPath);
284
+ const mcpFiles = writeMcpConfig(projectPath, ides, dbPath, { anthropicApiKey });
254
285
  mcpFiles.forEach(f => console.log(` ${f}`));
255
286
 
256
- // 7. Write rules
287
+ // 8. Write rules
257
288
  console.log('\nWriting IDE rules...');
258
289
  const rulesFiles = writeRules(projectPath, ides);
259
290
  rulesFiles.forEach(f => console.log(` ${f}`));
260
291
 
261
- // 8. Update .gitignore
292
+ // 9. Update .gitignore
262
293
  const giUpdated = updateGitignore(projectPath);
263
294
  if (giUpdated) {
264
295
  console.log('\nUpdated .gitignore with .magector/');
265
296
  }
266
297
 
267
- // 9. Get stats and print summary
298
+ // 10. Get stats and print summary
268
299
  let vectorCount = '?';
269
300
  try {
270
301
  const statsOutput = execFileSync(binary, ['stats', '-d', dbPath], {
@@ -303,7 +334,14 @@ export async function setup(projectPath) {
303
334
 
304
335
  console.log(`Detected: ${ideNames.join(' + ')}`);
305
336
 
306
- const mcpFiles = writeMcpConfig(projectPath, ides, dbPath);
337
+ let anthropicApiKey = await askQuestion('\nAnthropic API key (optional, enables LLM enrichment — press Enter to skip): ');
338
+ if (anthropicApiKey) {
339
+ console.log(' API key will be stored in MCP config.');
340
+ } else {
341
+ console.log(' Skipped. You can add ANTHROPIC_API_KEY to MCP config later.');
342
+ }
343
+
344
+ const mcpFiles = writeMcpConfig(projectPath, ides, dbPath, { anthropicApiKey });
307
345
  console.log('\nMCP config:');
308
346
  mcpFiles.forEach(f => console.log(` ${f}`));
309
347
 
package/src/mcp-server.js CHANGED
@@ -55,9 +55,11 @@ async function loadDescriptions() {
55
55
  logToFile('INFO', `Loaded ${Object.keys(descriptionMap).length} LLM descriptions via serve`);
56
56
  return;
57
57
  }
58
- } catch {
59
- // Fall through
58
+ } catch (err) {
59
+ logToFile('WARN', `Failed to load descriptions via serve: ${err.message}`);
60
60
  }
61
+ } else {
62
+ logToFile('INFO', 'Skipping description load (serve process not ready)');
61
63
  }
62
64
 
63
65
  }
@@ -82,13 +84,22 @@ function logToFile(level, message) {
82
84
  // Initialize log file on startup
83
85
  try { writeFileSync(LOG_PATH, `[${new Date().toISOString()}] [INFO] Magector MCP server starting\n`); } catch {}
84
86
 
87
+ // Log resolved configuration so the log file is self-contained for debugging
88
+ logToFile('INFO', `Config: MAGENTO_ROOT=${config.magentoRoot}`);
89
+ logToFile('INFO', `Config: MAGECTOR_DB=${config.dbPath}`);
90
+ logToFile('INFO', `Config: watchInterval=${config.watchInterval}s`);
91
+ try { logToFile('INFO', `Config: rustBinary=${config.rustBinary}`); } catch (e) { logToFile('ERR', `Config: rustBinary resolution failed: ${e.message}`); }
92
+ try { logToFile('INFO', `Config: modelCache=${config.modelCache}`); } catch (e) { logToFile('ERR', `Config: modelCache resolution failed: ${e.message}`); }
93
+ logToFile('INFO', `Config: PID=${process.pid}`);
94
+
85
95
  // ─── Rust Core Integration ──────────────────────────────────────
86
96
 
87
- // Env vars to suppress ONNX Runtime native logs that would pollute stdout/JSON-RPC
97
+ // Env vars for Rust subprocess logging ORT_LOG_LEVEL suppresses ONNX native noise,
98
+ // RUST_LOG=info surfaces watcher events, indexing progress, model loading, HNSW ops.
88
99
  const rustEnv = {
89
100
  ...process.env,
90
101
  ORT_LOG_LEVEL: 'error',
91
- RUST_LOG: 'error',
102
+ RUST_LOG: 'info',
92
103
  };
93
104
 
94
105
  /**
@@ -120,6 +131,62 @@ function extractJson(stdout) {
120
131
  throw new SyntaxError('No valid JSON found in command output');
121
132
  }
122
133
 
134
+ // ─── PID File & Orphan Cleanup ──────────────────────────────────
135
+ // Track the serve process PID to clean up orphans on restart.
136
+
137
+ const PID_PATH = path.join(config.magentoRoot, '.magector', 'serve.pid');
138
+
139
+ /**
140
+ * Write the serve process PID to disk so future instances can clean up orphans.
141
+ */
142
+ function writePidFile(pid) {
143
+ try { writeFileSync(PID_PATH, String(pid)); } catch {}
144
+ }
145
+
146
+ function removePidFile() {
147
+ try { if (existsSync(PID_PATH)) unlinkSync(PID_PATH); } catch {}
148
+ }
149
+
150
+ /**
151
+ * Kill any stale serve process from a previous MCP server instance.
152
+ * This handles the common case where the MCP server was killed without
153
+ * triggering its exit handler (SIGKILL, crash, IDE disconnect).
154
+ */
155
+ function killStaleServeProcess() {
156
+ try {
157
+ if (!existsSync(PID_PATH)) return;
158
+ const stalePid = parseInt(readFileSync(PID_PATH, 'utf-8').trim(), 10);
159
+ if (!stalePid || isNaN(stalePid)) return;
160
+
161
+ // Check if the process is still alive
162
+ try {
163
+ process.kill(stalePid, 0); // signal 0 = existence check
164
+ } catch {
165
+ // Process doesn't exist, clean up stale PID file
166
+ removePidFile();
167
+ return;
168
+ }
169
+
170
+ logToFile('WARN', `Killing stale serve process (PID ${stalePid}) from previous session`);
171
+ console.error(`Killing stale serve process (PID ${stalePid})`);
172
+ try { process.kill(stalePid, 'SIGTERM'); } catch {}
173
+
174
+ // Give it a moment, then force kill if still alive
175
+ setTimeout(() => {
176
+ try {
177
+ process.kill(stalePid, 0);
178
+ process.kill(stalePid, 'SIGKILL');
179
+ } catch {
180
+ // Already dead, good
181
+ }
182
+ }, 2000);
183
+
184
+ removePidFile();
185
+ } catch {
186
+ // Non-fatal
187
+ }
188
+ }
189
+
123
190
  // ─── Database Format Check & Background Re-index ────────────────
124
191
 
125
192
  let reindexInProgress = false;
@@ -180,6 +247,7 @@ function startBackgroundReindex() {
180
247
  if (existsSync(bgDescDbPath)) {
181
248
  reindexArgs.push('--descriptions-db', bgDescDbPath);
182
249
  }
250
+ logToFile('INFO', `Starting background reindex: ${config.rustBinary} ${reindexArgs.join(' ')}`);
183
251
  reindexProcess = spawn(config.rustBinary, reindexArgs, {
184
252
  stdio: ['pipe', 'pipe', 'pipe'],
185
253
  env: rustEnv,
@@ -326,11 +394,20 @@ function startServeProcess() {
326
394
  if (existsSync(descDbPath)) {
327
395
  args.push('--descriptions-db', descDbPath);
328
396
  }
397
+ logToFile('INFO', `Starting serve process: ${config.rustBinary} ${args.join(' ')}`);
329
398
  const proc = spawn(config.rustBinary, args,
330
399
  { stdio: ['pipe', 'pipe', 'pipe'], env: rustEnv });
331
400
 
332
- proc.on('error', () => { serveProcess = null; serveReady = false; if (serveReadyResolve) { serveReadyResolve(false); serveReadyResolve = null; } });
333
- proc.on('exit', () => { serveProcess = null; serveReady = false; if (serveReadyResolve) { serveReadyResolve(false); serveReadyResolve = null; } });
401
+ proc.on('error', (err) => {
402
+ logToFile('ERR', `Serve process error: ${err.message}`);
403
+ serveProcess = null; serveReady = false; removePidFile();
404
+ if (serveReadyResolve) { serveReadyResolve(false); serveReadyResolve = null; }
405
+ });
406
+ proc.on('exit', (code, signal) => {
407
+ logToFile('WARN', `Serve process exited (code=${code}, signal=${signal})`);
408
+ serveProcess = null; serveReady = false; removePidFile();
409
+ if (serveReadyResolve) { serveReadyResolve(false); serveReadyResolve = null; }
410
+ });
334
411
  proc.stderr.on('data', (d) => {
335
412
  // Log serve process stderr (watcher events, tracing, errors) to .magector/magector.log
336
413
  // Strip ANSI escape codes for clean log output
@@ -341,11 +418,15 @@ function startServeProcess() {
341
418
  serveReadline = createInterface({ input: proc.stdout });
342
419
  serveReadline.on('line', (line) => {
343
420
  let parsed;
344
- try { parsed = JSON.parse(line); } catch { return; }
421
+ try { parsed = JSON.parse(line); } catch {
422
+ logToFile('WARN', `Unparseable serve stdout: ${line.slice(0, 200)}`);
423
+ return;
424
+ }
345
425
 
346
426
  // First line is ready signal
347
427
  if (parsed.ready) {
348
428
  serveReady = true;
429
+ logToFile('INFO', `Serve process ready (PID ${proc.pid})`);
349
430
  if (serveReadyResolve) { serveReadyResolve(true); serveReadyResolve = null; }
350
431
  return;
351
432
  }
@@ -359,7 +440,10 @@ function startServeProcess() {
359
440
  });
360
441
 
361
442
  serveProcess = proc;
362
- } catch {
443
+ writePidFile(proc.pid);
444
+ logToFile('INFO', `Serve process spawned (PID ${proc.pid})`);
445
+ } catch (err) {
446
+ logToFile('ERR', `Failed to start serve process: ${err.message}`);
363
447
  serveProcess = null;
364
448
  serveReady = false;
365
449
  if (serveReadyResolve) { serveReadyResolve(false); serveReadyResolve = null; }
@@ -369,8 +453,10 @@ function startServeProcess() {
369
453
  function serveQuery(command, params = {}, timeoutMs = 30000) {
370
454
  return new Promise((resolve, reject) => {
371
455
  const id = serveNextId++;
456
+ logToFile('QUERY', `[${id}] → ${command}(${JSON.stringify(params).slice(0, 200)})`);
372
457
  const timer = setTimeout(() => {
373
458
  servePending.delete(id);
459
+ logToFile('ERR', `[${id}] ← ${command} TIMEOUT after ${timeoutMs}ms`);
374
460
  reject(new Error('Serve query timeout'));
375
461
  }, timeoutMs);
376
462
  servePending.set(id, {
@@ -384,11 +470,13 @@ function serveQuery(command, params = {}, timeoutMs = 30000) {
384
470
  async function rustSearchAsync(query, limit = 10) {
385
471
  const cacheKey = `${query}|${limit}`;
386
472
  if (searchCache.has(cacheKey)) {
473
+ logToFile('CACHE', `HIT: "${query}" (limit=${limit})`);
387
474
  return searchCache.get(cacheKey);
388
475
  }
389
476
 
390
477
  // Wait for serve process if it's starting up but not yet ready
391
478
  if (serveProcess && !serveReady && serveReadyPromise) {
479
+ logToFile('INFO', `Waiting for serve process to become ready...`);
392
480
  await Promise.race([serveReadyPromise, new Promise(r => setTimeout(() => r(false), 10000))]);
393
481
  }
394
482
 
@@ -400,12 +488,13 @@ async function rustSearchAsync(query, limit = 10) {
400
488
  cacheSet(cacheKey, resp.data);
401
489
  return resp.data;
402
490
  }
403
- } catch {
404
- // Fall through to execFileSync
491
+ } catch (err) {
492
+ logToFile('WARN', `Serve query failed, falling back to execFileSync: ${err.message}`);
405
493
  }
406
494
  }
407
495
 
408
496
  // Fallback: cold-start execFileSync
497
+ logToFile('INFO', `Using execFileSync fallback for search: "${query}"`);
409
498
  return rustSearchSync(query, limit);
410
499
  }
411
500
 
@@ -1837,7 +1926,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1837
1926
  // SONA: flush accumulated feedback signals to Rust core
1838
1927
  const signals = sessionTracker.flush();
1839
1928
  if (signals.length > 0 && serveProcess && serveReady) {
1840
- serveQuery('feedback', { signals }).catch(() => {});
1929
+ serveQuery('feedback', { signals }).catch((err) => logToFile('WARN', `Feedback signal send failed: ${err.message}`));
1841
1930
  }
1842
1931
  }
1843
1932
  });
@@ -1871,6 +1960,9 @@ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
1871
1960
  });
1872
1961
 
1873
1962
  async function main() {
1963
+ // Kill any orphaned serve process from a previous session
1964
+ killStaleServeProcess();
1965
+
1874
1966
  // Check database format compatibility before starting serve process
1875
1967
  if (existsSync(config.dbPath) && !checkDbFormat()) {
1876
1968
  startBackgroundReindex();
@@ -1908,11 +2000,28 @@ async function main() {
1908
2000
  console.error('Magector MCP server started (Rust core backend)');
1909
2001
  }
1910
2002
 
1911
- // Cleanup on exit
1912
- process.on('exit', () => {
2003
+ // Cleanup on exit — kill all child processes and remove PID file
2004
+ function cleanup(reason) {
2005
+ logToFile('INFO', `Cleanup: ${reason || 'exit'}`);
1913
2006
  if (serveProcess) {
1914
- serveProcess.kill();
2007
+ logToFile('INFO', `Cleanup: killing serve process (PID ${serveProcess.pid})`);
2008
+ try { serveProcess.kill(); } catch {}
2009
+ serveProcess = null;
1915
2010
  }
1916
- });
2011
+ if (reindexProcess) {
2012
+ logToFile('INFO', `Cleanup: killing reindex process (PID ${reindexProcess.pid})`);
2013
+ try { reindexProcess.kill(); } catch {}
2014
+ reindexProcess = null;
2015
+ }
2016
+ removePidFile();
2017
+ }
1917
2018
 
1918
- main().catch(console.error);
2019
+ process.on('exit', () => cleanup('process exit'));
2020
+ process.on('SIGTERM', () => { cleanup('SIGTERM'); process.exit(0); });
2021
+ process.on('SIGINT', () => { cleanup('SIGINT'); process.exit(0); });
2022
+ process.on('SIGHUP', () => { cleanup('SIGHUP'); process.exit(0); });
2023
+
2024
+ main().catch((err) => {
2025
+ logToFile('FATAL', `Startup failed: ${err.message}\n${err.stack}`);
2026
+ console.error(err);
2027
+ });
package/src/update.js ADDED
@@ -0,0 +1,158 @@
1
+ /**
2
+ * Auto-update check for Magector CLI.
3
+ *
4
+ * On each CLI run (except help/mcp), checks the npm registry for a newer version.
5
+ * If found, re-execs the current command via `npx magector@<latest>` so npx
6
+ * downloads the new version and runs it. Results are cached for 1 hour.
7
+ *
8
+ * Never blocks the CLI on failure — network errors are silently ignored.
9
+ */
10
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
11
+ import { execSync } from 'child_process';
12
+ import { homedir } from 'os';
13
+ import path from 'path';
14
+ import { fileURLToPath } from 'url';
15
+
16
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
17
+ const CACHE_TTL = 3600000; // 1 hour
18
+ const REGISTRY_TIMEOUT = 3000; // 3 seconds
19
+ const SKIP_COMMANDS = new Set(['help', '--help', '-h', 'mcp', undefined]);
20
+
21
+ /**
22
+ * Read current package version.
23
+ */
24
+ function getCurrentVersion() {
25
+ const pkgPath = path.resolve(__dirname, '..', 'package.json');
26
+ if (!existsSync(pkgPath)) return null;
27
+ return JSON.parse(readFileSync(pkgPath, 'utf-8')).version;
28
+ }
29
+
30
+ /**
31
+ * Resolve cache file path — prefer project .magector/ if it exists, else ~/.magector/
32
+ */
33
+ function getCachePath() {
34
+ const projectDir = path.join(process.cwd(), '.magector');
35
+ if (existsSync(projectDir)) {
36
+ return path.join(projectDir, 'version-check.json');
37
+ }
38
+ const globalDir = path.join(homedir(), '.magector');
39
+ mkdirSync(globalDir, { recursive: true });
40
+ return path.join(globalDir, 'version-check.json');
41
+ }
42
+
43
+ /**
44
+ * Read cached version check result.
45
+ */
46
+ function readCache(cachePath) {
47
+ try {
48
+ if (!existsSync(cachePath)) return null;
49
+ return JSON.parse(readFileSync(cachePath, 'utf-8'));
50
+ } catch {
51
+ return null;
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Write cache.
57
+ */
58
+ function writeCache(cachePath, latest) {
59
+ try {
60
+ writeFileSync(cachePath, JSON.stringify({ latest, checkedAt: Date.now() }));
61
+ } catch {
62
+ // Non-critical
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Compare two semver strings. Returns true if a > b.
68
+ */
69
+ function isNewer(a, b) {
70
+ const pa = a.split('.').map(Number);
71
+ const pb = b.split('.').map(Number);
72
+ for (let i = 0; i < 3; i++) {
73
+ if ((pa[i] || 0) > (pb[i] || 0)) return true;
74
+ if ((pa[i] || 0) < (pb[i] || 0)) return false;
75
+ }
76
+ return false;
77
+ }
78
+
79
+ /**
80
+ * Fetch latest version from npm registry using native fetch (Node 18+).
81
+ */
82
+ async function fetchLatestVersion() {
83
+ const controller = new AbortController();
84
+ const timer = setTimeout(() => controller.abort(), REGISTRY_TIMEOUT);
85
+ try {
86
+ const resp = await fetch('https://registry.npmjs.org/magector/latest', {
87
+ signal: controller.signal,
88
+ headers: { 'Accept': 'application/json' }
89
+ });
90
+ if (!resp.ok) return null;
91
+ const data = await resp.json();
92
+ return data.version || null;
93
+ } finally {
94
+ clearTimeout(timer);
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Check for updates and self-update if a newer version is available.
100
+ *
101
+ * When a newer version is found, re-execs the CLI command via
102
+ * `npx -y magector@<latest> <original args>` so npx downloads
103
+ * the new version automatically. The current process exits after.
104
+ *
105
+ * @param {string} command - The CLI command being run
106
+ * @param {string[]} originalArgs - The original process.argv.slice(2)
107
+ */
108
+ export async function checkForUpdate(command, originalArgs) {
109
+ // Skip for commands that don't need update checks
110
+ if (SKIP_COMMANDS.has(command)) return;
111
+
112
+ // Skip if MAGECTOR_NO_UPDATE is set (for CI, testing, or re-exec guard)
113
+ if (process.env.MAGECTOR_NO_UPDATE) return;
114
+
115
+ try {
116
+ const current = getCurrentVersion();
117
+ if (!current) return;
118
+
119
+ const cachePath = getCachePath();
120
+ const cached = readCache(cachePath);
121
+
122
+ // Cache hit — check if we already know about an update
123
+ if (cached && (Date.now() - cached.checkedAt) < CACHE_TTL) {
124
+ if (!isNewer(cached.latest, current)) return; // up to date
125
+ // Cached says there's an update — proceed to re-exec
126
+ return reExec(current, cached.latest, originalArgs);
127
+ }
128
+
129
+ // Fetch from registry
130
+ const latest = await fetchLatestVersion();
131
+ if (!latest) return;
132
+
133
+ writeCache(cachePath, latest);
134
+
135
+ if (!isNewer(latest, current)) return; // up to date
136
+
137
+ return reExec(current, latest, originalArgs);
138
+ } catch {
139
+ // Never block CLI on update check failure
140
+ }
141
+ }
142
+
143
+ /**
144
+ * Re-exec the current command with the latest version.
145
+ */
146
+ function reExec(current, latest, originalArgs) {
147
+ console.log(`\n⬆ Updating magector: v${current} → v${latest}...\n`);
148
+ try {
149
+ const cmd = `npx -y magector@${latest} ${originalArgs.join(' ')}`;
150
+ execSync(cmd, {
151
+ stdio: 'inherit',
152
+ env: { ...process.env, MAGECTOR_NO_UPDATE: '1' }
153
+ });
154
+ } catch (err) {
155
+ process.exit(err.status || 1);
156
+ }
157
+ process.exit(0);
158
+ }