@softerist/heuristic-mcp 2.1.46 → 3.0.0

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 (109) hide show
  1. package/.agent/workflows/code-review.md +60 -0
  2. package/.prettierrc +7 -0
  3. package/ARCHITECTURE.md +105 -170
  4. package/CONTRIBUTING.md +32 -113
  5. package/GEMINI.md +73 -0
  6. package/LICENSE +21 -21
  7. package/README.md +161 -54
  8. package/config.json +876 -76
  9. package/debug-pids.js +27 -0
  10. package/eslint.config.js +36 -0
  11. package/features/ann-config.js +37 -26
  12. package/features/clear-cache.js +28 -19
  13. package/features/find-similar-code.js +142 -66
  14. package/features/hybrid-search.js +253 -93
  15. package/features/index-codebase.js +1455 -394
  16. package/features/lifecycle.js +813 -180
  17. package/features/register.js +58 -52
  18. package/index.js +450 -306
  19. package/lib/cache-ops.js +22 -0
  20. package/lib/cache-utils.js +68 -0
  21. package/lib/cache.js +1392 -587
  22. package/lib/call-graph.js +165 -50
  23. package/lib/cli.js +154 -0
  24. package/lib/config.js +462 -121
  25. package/lib/embedding-process.js +77 -0
  26. package/lib/embedding-worker.js +545 -30
  27. package/lib/ignore-patterns.js +61 -59
  28. package/lib/json-worker.js +14 -0
  29. package/lib/json-writer.js +344 -0
  30. package/lib/logging.js +88 -0
  31. package/lib/memory-logger.js +13 -0
  32. package/lib/project-detector.js +13 -17
  33. package/lib/server-lifecycle.js +38 -0
  34. package/lib/settings-editor.js +645 -0
  35. package/lib/tokenizer.js +207 -104
  36. package/lib/utils.js +273 -198
  37. package/lib/vector-store-binary.js +592 -0
  38. package/mcp_config.example.json +13 -0
  39. package/package.json +13 -2
  40. package/scripts/clear-cache.js +6 -17
  41. package/scripts/download-model.js +14 -9
  42. package/scripts/postinstall.js +5 -5
  43. package/search-configs.js +36 -0
  44. package/test/ann-config.test.js +179 -0
  45. package/test/ann-fallback.test.js +6 -6
  46. package/test/binary-store.test.js +69 -0
  47. package/test/cache-branches.test.js +120 -0
  48. package/test/cache-errors.test.js +264 -0
  49. package/test/cache-extra.test.js +300 -0
  50. package/test/cache-helpers.test.js +205 -0
  51. package/test/cache-hnsw-failure.test.js +40 -0
  52. package/test/cache-json-worker.test.js +190 -0
  53. package/test/cache-worker.test.js +102 -0
  54. package/test/cache.test.js +443 -0
  55. package/test/call-graph.test.js +103 -4
  56. package/test/clear-cache.test.js +69 -68
  57. package/test/code-review-workflow.test.js +50 -0
  58. package/test/config.test.js +418 -0
  59. package/test/coverage-gap.test.js +497 -0
  60. package/test/coverage-maximizer.test.js +236 -0
  61. package/test/debug-analysis.js +107 -0
  62. package/test/embedding-model.test.js +173 -103
  63. package/test/embedding-worker-extra.test.js +272 -0
  64. package/test/embedding-worker.test.js +158 -0
  65. package/test/features.test.js +139 -0
  66. package/test/final-boost.test.js +271 -0
  67. package/test/final-polish.test.js +183 -0
  68. package/test/final.test.js +95 -0
  69. package/test/find-similar-code.test.js +191 -0
  70. package/test/helpers.js +92 -11
  71. package/test/helpers.test.js +46 -0
  72. package/test/hybrid-search-basic.test.js +62 -0
  73. package/test/hybrid-search-branch.test.js +202 -0
  74. package/test/hybrid-search-callgraph.test.js +229 -0
  75. package/test/hybrid-search-extra.test.js +81 -0
  76. package/test/hybrid-search.test.js +484 -71
  77. package/test/index-cli.test.js +520 -0
  78. package/test/index-codebase-batch.test.js +119 -0
  79. package/test/index-codebase-branches.test.js +585 -0
  80. package/test/index-codebase-core.test.js +1032 -0
  81. package/test/index-codebase-edge-cases.test.js +254 -0
  82. package/test/index-codebase-errors.test.js +132 -0
  83. package/test/index-codebase-gap.test.js +239 -0
  84. package/test/index-codebase-lines.test.js +151 -0
  85. package/test/index-codebase-watcher.test.js +259 -0
  86. package/test/index-codebase-zone.test.js +259 -0
  87. package/test/index-codebase.test.js +371 -69
  88. package/test/index-memory.test.js +220 -0
  89. package/test/indexer-detailed.test.js +176 -0
  90. package/test/integration.test.js +148 -92
  91. package/test/json-worker.test.js +50 -0
  92. package/test/lifecycle.test.js +541 -0
  93. package/test/master.test.js +198 -0
  94. package/test/perfection.test.js +349 -0
  95. package/test/project-detector.test.js +65 -0
  96. package/test/register.test.js +262 -0
  97. package/test/tokenizer.test.js +55 -93
  98. package/test/ultra-maximizer.test.js +116 -0
  99. package/test/utils-branches.test.js +161 -0
  100. package/test/utils-extra.test.js +116 -0
  101. package/test/utils.test.js +131 -0
  102. package/test/verify_fixes.js +76 -0
  103. package/test/worker-errors.test.js +96 -0
  104. package/test/worker-init.test.js +102 -0
  105. package/test/worker_throttling.test.js +93 -0
  106. package/tools/scripts/benchmark-search.js +95 -0
  107. package/tools/scripts/cache-stats.js +71 -0
  108. package/tools/scripts/manual-search.js +34 -0
  109. package/vitest.config.js +19 -9
@@ -1,37 +1,137 @@
1
-
2
1
  import { exec } from 'child_process';
3
2
  import util from 'util';
4
3
  import path from 'path';
5
4
  import os from 'os';
6
5
  import fs from 'fs/promises';
6
+ import fsSync from 'fs';
7
+ import { loadConfig } from '../lib/config.js';
8
+ import { getLogFilePath } from '../lib/logging.js';
9
+ import { clearStaleCaches } from '../lib/cache-utils.js';
10
+ import { findMcpServerEntry, parseJsonc, upsertMcpServerEntryInText } from '../lib/settings-editor.js';
7
11
 
8
12
  const execPromise = util.promisify(exec);
9
13
 
10
14
  export async function stop() {
11
- console.log('[Lifecycle] Stopping Heuristic MCP servers...');
15
+ console.info('[Lifecycle] Stopping Heuristic MCP servers...');
12
16
  try {
13
17
  const platform = process.platform;
14
18
  const currentPid = process.pid;
15
19
  let pids = [];
20
+ const cmdByPid = new Map();
21
+ const manualPid = process.env.HEURISTIC_MCP_PID;
16
22
 
17
23
  if (platform === 'win32') {
18
- const { stdout } = await execPromise(`wmic process where "CommandLine like '%heuristic-mcp/index.js%'" get ProcessId`);
19
- pids = stdout.trim().split(/\s+/).filter(p => p && !isNaN(p) && parseInt(p) !== currentPid);
24
+ // 1. Try PID file first for reliability
25
+ const home = os.homedir();
26
+ const pidFile = path.join(home, '.heuristic-mcp.pid');
27
+ try {
28
+ const content = await fs.readFile(pidFile, 'utf-8');
29
+ const p = content.trim();
30
+ if (p && !isNaN(p)) {
31
+ const pid = parseInt(p, 10);
32
+ if (pid !== currentPid) {
33
+ try {
34
+ process.kill(pid, 0);
35
+ pids.push(p);
36
+ } catch (e) {
37
+ // If we lack permission, still attempt to stop by PID.
38
+ if (e.code === 'EPERM') {
39
+ pids.push(p);
40
+ } else {
41
+ // Stale PID file
42
+ await fs.unlink(pidFile).catch(() => {});
43
+ }
44
+ }
45
+ }
46
+ }
47
+ } catch (_e) {
48
+ // Fallback to WMIC when CIM access is denied
49
+ try {
50
+ const { stdout } = await execPromise(
51
+ `wmic process where "CommandLine like '%heuristic-mcp%'" get ProcessId /FORMAT:LIST`
52
+ );
53
+ const matches = stdout.match(/ProcessId=(\d+)/g) || [];
54
+ for (const match of matches) {
55
+ const pid = match.replace('ProcessId=', '');
56
+ if (pid && !isNaN(pid) && parseInt(pid, 10) !== currentPid) {
57
+ if (!pids.includes(pid)) pids.push(pid);
58
+ }
59
+ }
60
+ } catch (_wmicErr) {
61
+ // ignore secondary failures
62
+ }
63
+ }
64
+
65
+ // 2. Fallback to process list with fuzzier matching (kill all heuristic-mcp instances)
66
+ try {
67
+ const { stdout } = await execPromise(
68
+ `powershell -NoProfile -Command "Get-CimInstance Win32_Process | Where-Object { $_.CommandLine -and ($_.CommandLine -like '*heuristic-mcp*' -or $_.CommandLine -like '*heuristic-mcp\\\\index.js*' -or $_.CommandLine -like '*heuristic-mcp/index.js*') } | Select-Object -ExpandProperty ProcessId"`
69
+ );
70
+ const listPids = stdout
71
+ .trim()
72
+ .split(/\s+/)
73
+ .filter((p) => p && !isNaN(p) && parseInt(p) !== currentPid);
74
+
75
+ // Retrieve command lines to filter out workers
76
+ if (listPids.length > 0) {
77
+ const { stdout: cmdOut } = await execPromise(
78
+ `powershell -NoProfile -Command "Get-CimInstance Win32_Process | Where-Object { $_.ProcessId -in @(${listPids.join(',')}) } | Select-Object ProcessId, CommandLine"`
79
+ );
80
+ const lines = cmdOut.trim().split(/\r?\n/);
81
+ for (const line of lines) {
82
+ const trimmed = line.trim();
83
+ if (!trimmed || trimmed.startsWith('ProcessId')) continue;
84
+ const match = trimmed.match(/^(\d+)\s+(.*)$/);
85
+ if (match) {
86
+ const pid = parseInt(match[1], 10);
87
+ const cmd = match[2];
88
+ if (cmd.includes('embedding-worker') || cmd.includes('embedding-process') || cmd.includes('json-worker')) {
89
+ continue;
90
+ }
91
+ if (pid && !pids.includes(String(pid))) {
92
+ pids.push(String(pid));
93
+ }
94
+ }
95
+ }
96
+ }
97
+ } catch (_e) { /* ignore */ }
20
98
  } else {
21
99
  // Unix: Use pgrep to get all matching PIDs
22
100
  try {
23
- const { stdout } = await execPromise(`pgrep -f "heuristic-mcp.*index.js"`);
24
- const allPids = stdout.trim().split(/\s+/).filter(p => p && !isNaN(p));
101
+ const { stdout } = await execPromise(`pgrep -fl "heuristic-mcp"`);
102
+ const lines = stdout.trim().split(/\r?\n/);
25
103
 
26
- // Filter out current PID and dead processes
104
+ // Filter out current PID, dead processes, and workers
27
105
  pids = [];
28
- for (const p of allPids) {
29
- const pid = parseInt(p);
30
- if (pid === currentPid) continue;
106
+ for (const line of lines) {
107
+ const tokens = line.trim().split(/\s+/).filter(Boolean);
108
+ if (tokens.length === 0) continue;
109
+
110
+ const allNumeric = tokens.every((token) => /^\d+$/.test(token));
111
+ const candidatePids = allNumeric ? tokens : [tokens[0]];
112
+
113
+ for (const candidate of candidatePids) {
114
+ const pid = parseInt(candidate, 10);
115
+ if (!Number.isFinite(pid) || pid === currentPid) continue;
116
+
117
+ // Exclude workers when command line is present
118
+ if (
119
+ !allNumeric &&
120
+ (line.includes('embedding-worker') ||
121
+ line.includes('embedding-process') ||
122
+ line.includes('json-worker'))
123
+ ) {
124
+ continue;
125
+ }
126
+
31
127
  try {
32
- process.kill(pid, 0);
33
- pids.push(p);
34
- } catch (e) {}
128
+ process.kill(pid, 0);
129
+ const pidValue = String(pid);
130
+ if (!pids.includes(pidValue)) {
131
+ pids.push(pidValue);
132
+ }
133
+ } catch (_e) { /* ignore */ }
134
+ }
35
135
  }
36
136
  } catch (e) {
37
137
  // pgrep returns code 1 if no processes found, which is fine
@@ -40,221 +140,754 @@ export async function stop() {
40
140
  }
41
141
  }
42
142
 
143
+ // Manual PID override (best-effort)
144
+ if (manualPid) {
145
+ const parts = String(manualPid)
146
+ .split(/[,\s]+/)
147
+ .map((part) => part.trim())
148
+ .filter(Boolean);
149
+ for (const part of parts) {
150
+ if (!isNaN(part)) {
151
+ const pidValue = String(parseInt(part, 10));
152
+ if (pidValue && !pids.includes(pidValue)) {
153
+ pids.push(pidValue);
154
+ }
155
+ }
156
+ }
157
+ }
158
+
43
159
  if (pids.length === 0) {
44
- console.log('[Lifecycle] No running instances found (already stopped).');
160
+ console.info('[Lifecycle] No running instances found (already stopped).');
161
+ await setMcpServerEnabled(false);
45
162
  return;
46
163
  }
47
164
 
48
- // Kill each process
165
+ // Capture command lines before killing (best-effort)
166
+ try {
167
+ if (platform === 'win32') {
168
+ const { stdout } = await execPromise(
169
+ `powershell -NoProfile -Command "Get-CimInstance Win32_Process | Where-Object { $_.ProcessId -in @(${pids.join(',')}) } | Select-Object ProcessId, CommandLine"`
170
+ );
171
+ const lines = stdout.trim().split(/\r?\n/);
172
+ for (const line of lines) {
173
+ const trimmed = line.trim();
174
+ if (!trimmed || trimmed.startsWith('ProcessId')) continue;
175
+ const match = trimmed.match(/^(\d+)\s+(.*)$/);
176
+ if (match) {
177
+ cmdByPid.set(parseInt(match[1], 10), match[2]);
178
+ }
179
+ }
180
+ } else {
181
+ const { stdout } = await execPromise(`ps -o pid=,command= -p ${pids.join(',')}`);
182
+ const lines = stdout.trim().split(/\r?\n/);
183
+ for (const line of lines) {
184
+ const match = line.trim().match(/^(\d+)\s+(.*)$/);
185
+ if (match) {
186
+ cmdByPid.set(parseInt(match[1], 10), match[2]);
187
+ }
188
+ }
189
+ }
190
+ } catch (_e) {
191
+ // ignore command line lookup failures
192
+ }
193
+
194
+ // Kill each process (Windows uses taskkill for compatibility)
49
195
  let killedCount = 0;
196
+ const killedPids = [];
197
+ const failedPids = [];
50
198
  for (const pid of pids) {
51
199
  try {
52
- process.kill(parseInt(pid), 'SIGTERM');
200
+ if (platform === 'win32') {
201
+ try {
202
+ await execPromise(`taskkill /PID ${pid} /T`);
203
+ } catch (e) {
204
+ const message = String(e?.message || '');
205
+ if (message.includes('not found') || message.includes('not be found')) {
206
+ // Process already exited; treat as success.
207
+ killedCount++;
208
+ killedPids.push(pid);
209
+ continue;
210
+ }
211
+ try {
212
+ await execPromise(`taskkill /PID ${pid} /T /F`);
213
+ } catch (e2) {
214
+ const message2 = String(e2?.message || '');
215
+ if (message2.includes('not found') || message2.includes('not be found')) {
216
+ killedCount++;
217
+ killedPids.push(pid);
218
+ continue;
219
+ }
220
+ throw e2;
221
+ }
222
+ }
223
+ } else {
224
+ process.kill(parseInt(pid), 'SIGTERM');
225
+ }
53
226
  killedCount++;
227
+ killedPids.push(pid);
54
228
  } catch (e) {
55
229
  // Ignore if process already gone
56
- if (e.code !== 'ESRCH') console.warn(`[Lifecycle] Failed to kill PID ${pid}: ${e.message}`);
230
+ if (e.code !== 'ESRCH') {
231
+ failedPids.push(pid);
232
+ console.warn(`[Lifecycle] Failed to kill PID ${pid}: ${e.message}`);
233
+ }
57
234
  }
58
235
  }
59
236
 
60
- console.log(`[Lifecycle] ✅ Stopped ${killedCount} running instance(s).`);
237
+ console.info(`[Lifecycle] ✅ Stopped ${killedCount} running instance(s).`);
238
+ if (killedPids.length > 0) {
239
+ console.info('[Lifecycle] Killed processes:');
240
+ for (const pid of killedPids) {
241
+ const cmd = cmdByPid.get(parseInt(pid, 10));
242
+ if (cmd) {
243
+ console.info(` ${pid}: ${cmd}`);
244
+ } else {
245
+ console.info(` ${pid}`);
246
+ }
247
+ }
248
+ }
249
+ if (failedPids.length > 0) {
250
+ console.info('[Lifecycle] Failed to kill:');
251
+ for (const pid of failedPids) {
252
+ const cmd = cmdByPid.get(parseInt(pid, 10));
253
+ if (cmd) {
254
+ console.info(` ${pid}: ${cmd}`);
255
+ } else {
256
+ console.info(` ${pid}`);
257
+ }
258
+ }
259
+ }
260
+
261
+ await setMcpServerEnabled(false);
61
262
  } catch (error) {
62
263
  console.warn(`[Lifecycle] Warning: Stop command encountered an error: ${error.message}`);
63
264
  }
64
265
  }
65
266
 
66
267
  export async function start() {
67
- console.log('[Lifecycle] Ensuring server is configured...');
68
- // Re-use the registration logic to ensure the config is present and correct
268
+ console.info('[Lifecycle] Ensuring server is configured...');
269
+ // Re-use the registration logic to ensure the config is present and correct
270
+ try {
271
+ const { register } = await import('./register.js');
272
+ await register();
273
+ await setMcpServerEnabled(true);
274
+ console.info('[Lifecycle] ✅ Configuration checked.');
275
+ console.info(
276
+ '[Lifecycle] To start the server, please reload your IDE window or restart the IDE.'
277
+ );
278
+ } catch (err) {
279
+ console.error(`[Lifecycle] Failed to configure server: ${err.message}`);
280
+ }
281
+ }
282
+
283
+ async function setMcpServerEnabled(enabled) {
284
+ const paths = getMcpConfigPaths();
285
+ const target = 'heuristic-mcp';
286
+ let changed = 0;
287
+
288
+ for (const { name, path: configPath } of paths) {
69
289
  try {
70
- const { register } = await import('./register.js');
71
- await register();
72
- console.log('[Lifecycle] ✅ Configuration checked.');
73
- console.log('[Lifecycle] To start the server, please reload your IDE window or restart the IDE.');
290
+ await fs.access(configPath);
291
+ } catch {
292
+ continue;
293
+ }
294
+
295
+ try {
296
+ const raw = await fs.readFile(configPath, 'utf-8');
297
+ if (!raw || !raw.trim()) {
298
+ continue;
299
+ }
300
+ const parsed = parseJsonc(raw);
301
+ if (!parsed) {
302
+ console.warn(
303
+ `[Lifecycle] Skipping ${name} config: not valid JSON/JSONC (won't overwrite).`
304
+ );
305
+ continue;
306
+ }
307
+
308
+ const found = findMcpServerEntry(parsed, target);
309
+ if (!found || !found.entry || typeof found.entry !== 'object') {
310
+ continue;
311
+ }
312
+
313
+ const updatedEntry = { ...found.entry, disabled: !enabled };
314
+ const updatedText = upsertMcpServerEntryInText(raw, target, updatedEntry);
315
+ if (!updatedText) {
316
+ console.warn(`[Lifecycle] Failed to update ${name} config (unparseable layout).`);
317
+ continue;
318
+ }
319
+
320
+ await fs.writeFile(configPath, updatedText);
321
+ changed++;
74
322
  } catch (err) {
75
- console.error(`[Lifecycle] Failed to configure server: ${err.message}`);
323
+ console.warn(`[Lifecycle] Failed to update ${name} config: ${err.message}`);
324
+ }
325
+ }
326
+
327
+ if (changed > 0) {
328
+ console.info(
329
+ `[Lifecycle] MCP server ${enabled ? 'enabled' : 'disabled'} in ${changed} config file(s).`
330
+ );
331
+ }
332
+ }
333
+
334
+ function getMcpConfigPaths() {
335
+ const home = os.homedir();
336
+ const configLocations = [
337
+ {
338
+ name: 'Antigravity',
339
+ path: path.join(home, '.gemini', 'antigravity', 'mcp_config.json'),
340
+ },
341
+ {
342
+ name: 'Claude Desktop',
343
+ path: path.join(home, '.config', 'Claude', 'claude_desktop_config.json'),
344
+ },
345
+ {
346
+ name: 'VS Code',
347
+ path: path.join(home, '.config', 'Code', 'User', 'settings.json'),
348
+ settingsMode: true,
349
+ },
350
+ {
351
+ name: 'Cursor',
352
+ path: path.join(home, '.config', 'Cursor', 'User', 'settings.json'),
353
+ },
354
+ ];
355
+
356
+ if (process.platform === 'darwin') {
357
+ configLocations[1].path = path.join(
358
+ home,
359
+ 'Library',
360
+ 'Application Support',
361
+ 'Claude',
362
+ 'claude_desktop_config.json'
363
+ );
364
+ configLocations[2].path = path.join(
365
+ home,
366
+ 'Library',
367
+ 'Application Support',
368
+ 'Code',
369
+ 'User',
370
+ 'settings.json'
371
+ );
372
+ configLocations[3].path = path.join(
373
+ home,
374
+ 'Library',
375
+ 'Application Support',
376
+ 'Cursor',
377
+ 'User',
378
+ 'settings.json'
379
+ );
380
+ } else if (process.platform === 'win32') {
381
+ configLocations[1].path = path.join(
382
+ process.env.APPDATA || '',
383
+ 'Claude',
384
+ 'claude_desktop_config.json'
385
+ );
386
+ configLocations[2].path = path.join(
387
+ process.env.APPDATA || '',
388
+ 'Code',
389
+ 'User',
390
+ 'settings.json'
391
+ );
392
+ configLocations[3].path = path.join(
393
+ process.env.APPDATA || '',
394
+ 'Cursor',
395
+ 'User',
396
+ 'settings.json'
397
+ );
398
+ }
399
+
400
+ return configLocations;
401
+ }
402
+
403
+ async function readTail(filePath, maxLines) {
404
+ const data = await fs.readFile(filePath, 'utf-8');
405
+ if (!data) return '';
406
+ const lines = data.split(/\r?\n/);
407
+ const tail = lines.slice(-maxLines).join('\n');
408
+ return tail.trimEnd();
409
+ }
410
+
411
+ async function followFile(filePath, startPosition) {
412
+ let position = startPosition;
413
+ const watcher = fsSync.watch(filePath, { persistent: true }, async (event) => {
414
+ if (event !== 'change') return;
415
+ try {
416
+ const stats = await fs.stat(filePath);
417
+ if (stats.size < position) {
418
+ position = 0;
419
+ }
420
+ if (stats.size === position) return;
421
+ const stream = fsSync.createReadStream(filePath, { start: position, end: stats.size - 1 });
422
+ stream.pipe(process.stdout, { end: false });
423
+ position = stats.size;
424
+ } catch {
425
+ // ignore read errors while watching
76
426
  }
427
+ });
428
+
429
+ const stop = () => {
430
+ watcher.close();
431
+ process.exit(0);
432
+ };
433
+
434
+ process.on('SIGINT', stop);
435
+ process.on('SIGTERM', stop);
436
+ }
437
+
438
+ function formatDurationMs(ms) {
439
+ if (!Number.isFinite(ms) || ms < 0) return null;
440
+ const totalSeconds = Math.round(ms / 1000);
441
+ const hours = Math.floor(totalSeconds / 3600);
442
+ const minutes = Math.floor((totalSeconds % 3600) / 60);
443
+ const seconds = totalSeconds % 60;
444
+
445
+ if (hours > 0) return `${hours}h ${minutes}m ${seconds}s`;
446
+ if (minutes > 0) return `${minutes}m ${seconds}s`;
447
+ return `${seconds}s`;
448
+ }
449
+
450
+ function formatDateTime(value) {
451
+ const date = value instanceof Date ? value : new Date(value);
452
+ if (Number.isNaN(date.getTime())) return null;
453
+ return `${date.toLocaleString()} (${date.toISOString()})`;
454
+ }
455
+
456
+ export async function logs({ workspaceDir = null, tailLines = 200, follow = true } = {}) {
457
+ const config = await loadConfig(workspaceDir);
458
+ const logPath = getLogFilePath(config);
459
+
460
+ try {
461
+ const stats = await fs.stat(logPath);
462
+ const tail = await readTail(logPath, tailLines);
463
+ if (tail) {
464
+ process.stdout.write(tail + '\n');
465
+ }
466
+
467
+ if (!follow) {
468
+ return;
469
+ }
470
+
471
+ console.info(`[Logs] Following ${logPath} (Ctrl+C to stop)...`);
472
+ await followFile(logPath, stats.size);
473
+ } catch (err) {
474
+ if (err.code === 'ENOENT') {
475
+ console.error(`[Logs] No log file found for workspace.`);
476
+ console.error(`[Logs] Expected location: ${logPath}`);
477
+ console.error(`[Logs] Start the server from your IDE, then run: heuristic-mcp --logs`);
478
+ return;
479
+ }
480
+ console.error(`[Logs] Failed to read log file: ${err.message}`);
481
+ }
77
482
  }
78
483
 
79
484
  // Helper to get global cache dir
80
485
  function getGlobalCacheDir() {
81
- if (process.platform === 'win32') {
82
- return process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local');
83
- } else if (process.platform === 'darwin') {
84
- return path.join(os.homedir(), 'Library', 'Caches');
85
- }
86
- return process.env.XDG_CACHE_HOME || path.join(os.homedir(), '.cache');
486
+ if (process.platform === 'win32') {
487
+ return process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local');
488
+ } else if (process.platform === 'darwin') {
489
+ return path.join(os.homedir(), 'Library', 'Caches');
490
+ }
491
+ return process.env.XDG_CACHE_HOME || path.join(os.homedir(), '.cache');
87
492
  }
88
493
 
89
- export async function status() {
90
- try {
91
- const home = os.homedir();
92
- const pids = [];
494
+ export async function status({ fix = false } = {}) {
495
+ try {
496
+ const home = os.homedir();
497
+ const pids = [];
498
+ const now = new Date();
93
499
 
94
- // 1. Check PID file first
95
- const pidFile = path.join(home, '.heuristic-mcp.pid');
500
+ // 1. Check PID file first
501
+ const pidFile = path.join(home, '.heuristic-mcp.pid');
96
502
 
503
+ try {
504
+ const content = await fs.readFile(pidFile, 'utf-8');
505
+ const pid = parseInt(content.trim(), 10);
506
+ if (pid && !isNaN(pid)) {
507
+ // Check if running
97
508
  try {
98
- const content = await fs.readFile(pidFile, 'utf-8');
99
- const pid = parseInt(content.trim(), 10);
100
- if (pid && !isNaN(pid)) {
101
- // Check if running
102
- try {
103
- process.kill(pid, 0);
104
- pids.push(pid);
105
- } catch (e) {
106
- // Stale PID file
107
- await fs.unlink(pidFile).catch(() => {});
108
- }
109
- }
110
- } catch (e) {
111
- // No pid file, ignore
509
+ process.kill(pid, 0);
510
+ pids.push(pid);
511
+ } catch (_e) {
512
+ // Stale PID file
513
+ await fs.unlink(pidFile).catch(() => {});
112
514
  }
515
+ }
516
+ } catch (_e) {
517
+ // No pid file, ignore
518
+ }
113
519
 
114
- // 2. Fallback to process list if no PID file found or process dead
115
- if (pids.length === 0) {
116
- try {
117
- // Simplified ps check for Linux/Mac
118
- let cmd = 'ps aux';
119
- if (process.platform === 'win32') {
120
- // Skip Win32 complex ps logic for now
121
- } else {
122
- const { stdout } = await execPromise('ps aux');
123
- const lines = stdout.split('\n');
124
-
125
- const validPids = [];
126
- const myPid = process.pid;
127
-
128
- for (const line of lines) {
129
- if (line.includes('heuristic-mcp/index.js') || line.includes('heuristic-mcp')) {
130
- const parts = line.trim().split(/\s+/);
131
- const pid = parseInt(parts[1], 10);
132
- if (pid && !isNaN(pid) && pid !== myPid && !line.includes(' grep ')) {
133
- validPids.push(pid);
134
- }
135
- }
136
- }
137
- // Merge validPids into pids if not already present
138
- for (const p of validPids) {
139
- if (!pids.includes(p)) pids.push(p);
140
- }
141
- }
142
- } catch (e) {}
520
+ // 2. Fallback to process list if no PID file found or process dead
521
+ if (pids.length === 0) {
522
+ try {
523
+ const myPid = process.pid;
524
+ if (process.platform === 'win32') {
525
+ const { stdout } = await execPromise(
526
+ `powershell -NoProfile -Command "Get-CimInstance Win32_Process -Filter \\"CommandLine LIKE '%heuristic-mcp%index.js%'\\" | Select-Object -ExpandProperty ProcessId"`
527
+ );
528
+ const winPids = stdout
529
+ .trim()
530
+ .split(/\s+/)
531
+ .filter((p) => p && !isNaN(p));
532
+
533
+ // Retrieve command lines to filter out workers
534
+ if (winPids.length > 0) {
535
+ const { stdout: cmdOut } = await execPromise(
536
+ `powershell -NoProfile -Command "Get-CimInstance Win32_Process | Where-Object { $_.ProcessId -in @(${winPids.join(',')}) } | Select-Object ProcessId, CommandLine"`
537
+ );
538
+ const lines = cmdOut.trim().split(/\r?\n/);
539
+ for (const line of lines) {
540
+ const trimmed = line.trim();
541
+ if (!trimmed || trimmed.startsWith('ProcessId')) continue;
542
+ const match = trimmed.match(/^(\d+)\s+(.*)$/);
543
+ if (match) {
544
+ const pid = parseInt(match[1], 10);
545
+ const cmd = match[2];
546
+ if (cmd.includes('embedding-worker') || cmd.includes('embedding-process') || cmd.includes('json-worker')) {
547
+ continue;
548
+ }
549
+ if (pid && pid !== myPid) {
550
+ if (!pids.includes(pid)) pids.push(pid);
551
+ }
552
+ }
553
+ }
554
+ }
555
+ } else {
556
+ const { stdout } = await execPromise('ps aux');
557
+ const lines = stdout.split('\n');
558
+ const validPids = [];
559
+
560
+ for (const line of lines) {
561
+ if (line.includes('heuristic-mcp/index.js') || line.includes('heuristic-mcp')) {
562
+ // Exclude workers
563
+ if (line.includes('embedding-worker') || line.includes('embedding-process') || line.includes('json-worker')) {
564
+ continue;
565
+ }
566
+ const parts = line.trim().split(/\s+/);
567
+ const pid = parseInt(parts[1], 10);
568
+ if (pid && !isNaN(pid) && pid !== myPid && !line.includes(' grep ')) {
569
+ validPids.push(pid);
570
+ }
571
+ }
572
+ }
573
+ // Merge validPids into pids if not already present
574
+ for (const p of validPids) {
575
+ if (!pids.includes(p)) pids.push(p);
576
+ }
143
577
  }
578
+ } catch (_e) { /* ignore */ }
579
+ }
144
580
 
145
- // STATUS OUTPUT
146
- console.log(''); // spacer
147
- if (pids.length > 0) {
148
- console.log(`[Lifecycle] 🟢 Server is RUNNING. PID(s): ${pids.join(', ')}`);
581
+ // STATUS OUTPUT
582
+ console.info(''); // spacer
583
+ if (pids.length > 0) {
584
+ console.info(`[Lifecycle] 🟢 Server is RUNNING. PID(s): ${pids.join(', ')}`);
585
+ } else {
586
+ console.info('[Lifecycle] ⚪ Server is STOPPED.');
587
+ }
588
+ if (pids.length > 1) {
589
+ console.info('[Lifecycle] ⚠️ Multiple servers detected; progress may be inconsistent.');
590
+ }
591
+ if (pids.length > 0) {
592
+ const cmdByPid = new Map();
593
+ try {
594
+ if (process.platform === 'win32') {
595
+ const { stdout } = await execPromise(
596
+ `powershell -NoProfile -Command "Get-CimInstance Win32_Process | Where-Object { $_.ProcessId -in @(${pids.join(',')}) } | Select-Object ProcessId, CommandLine"`
597
+ );
598
+ const lines = stdout.trim().split(/\r?\n/);
599
+ for (const line of lines) {
600
+ const trimmed = line.trim();
601
+ if (!trimmed || trimmed.startsWith('ProcessId')) continue;
602
+ const match = trimmed.match(/^(\d+)\s+(.*)$/);
603
+ if (match) {
604
+ cmdByPid.set(parseInt(match[1], 10), match[2]);
605
+ }
606
+ }
149
607
  } else {
150
- console.log('[Lifecycle] Server is STOPPED.');
608
+ const { stdout } = await execPromise(`ps -o pid=,command= -p ${pids.join(',')}`);
609
+ const lines = stdout.trim().split(/\r?\n/);
610
+ for (const line of lines) {
611
+ const match = line.trim().match(/^(\d+)\s+(.*)$/);
612
+ if (match) {
613
+ cmdByPid.set(parseInt(match[1], 10), match[2]);
614
+ }
615
+ }
616
+ }
617
+ } catch (_e) {
618
+ // ignore command line lookup failures
619
+ }
620
+ if (cmdByPid.size > 0) {
621
+ console.info('[Lifecycle] Active command lines:');
622
+ for (const pid of pids) {
623
+ const cmd = cmdByPid.get(pid);
624
+ if (cmd) {
625
+ console.info(` ${pid}: ${cmd}`);
626
+ }
151
627
  }
152
- console.log(''); // spacer
628
+ }
629
+ }
630
+ console.info(''); // spacer
153
631
 
154
- // APPEND LOGS INFO (Cache Status)
155
- const globalCacheRoot = path.join(getGlobalCacheDir(), 'heuristic-mcp');
156
- console.log('[Status] Inspecting cache status...\n');
632
+ // APPEND LOGS INFO (Cache Status)
633
+ const globalCacheRoot = path.join(getGlobalCacheDir(), 'heuristic-mcp');
634
+ console.info('[Status] Inspecting cache status...\n');
157
635
 
158
- const cacheDirs = await fs.readdir(globalCacheRoot).catch(() => []);
636
+ if (fix) {
637
+ console.info('[Status] Fixing stale caches...\n');
638
+ await clearStaleCaches();
639
+ }
159
640
 
160
- if (cacheDirs.length === 0) {
161
- console.log('[Status] No cache directories found.');
162
- console.log(`[Status] Expected location: ${globalCacheRoot}`);
163
- } else {
164
- console.log(`[Status] Found ${cacheDirs.length} cache director${cacheDirs.length === 1 ? 'y' : 'ies'} in ${globalCacheRoot}`);
165
-
166
- for (const dir of cacheDirs) {
167
- const cacheDir = path.join(globalCacheRoot, dir);
168
- const metaFile = path.join(cacheDir, 'meta.json');
169
-
170
- console.log(`${'─'.repeat(60)}`);
171
- console.log(`📁 Cache: ${dir}`);
172
- console.log(` Path: ${cacheDir}`);
173
-
174
- try {
175
- const metaData = JSON.parse(await fs.readFile(metaFile, 'utf-8'));
176
-
177
- console.log(` Status: ✅ Valid cache`);
178
- console.log(` Workspace: ${metaData.workspace || 'Unknown'}`);
179
- console.log(` Files indexed: ${metaData.filesIndexed ?? 'N/A'}`);
180
- console.log(` Chunks stored: ${metaData.chunksStored ?? 'N/A'}`);
181
-
182
- if (metaData.lastSaveTime) {
183
- const saveDate = new Date(metaData.lastSaveTime);
184
- const now = new Date();
185
- const ageMs = now - saveDate;
186
- const ageHours = Math.floor(ageMs / (1000 * 60 * 60));
187
- const ageMins = Math.floor((ageMs % (1000 * 60 * 60)) / (1000 * 60));
188
- console.log(` Last saved: ${saveDate.toLocaleString()} (${ageHours}h ${ageMins}m ago)`);
189
- }
190
-
191
- // Verify indexing completion
192
- if (metaData.filesIndexed && metaData.filesIndexed > 0) {
193
- console.log(` Indexing: ✅ COMPLETE (${metaData.filesIndexed} files)`);
194
- } else if (metaData.filesIndexed === 0) {
195
- console.log(` Indexing: ⚠️ NO FILES (check excludePatterns)`);
196
- } else {
197
- console.log(` Indexing: ⚠️ INCOMPLETE`);
198
- }
199
-
200
- } catch (err) {
201
- if (err.code === 'ENOENT') {
202
- try {
203
- const stats = await fs.stat(cacheDir);
204
- const ageMs = new Date() - stats.mtime;
205
- if (ageMs < 10 * 60 * 1000) {
206
- console.log(` Status: Initializing / Indexing in progress...`);
207
- console.log(` (Metadata file has not been written yet using ID ${dir})`);
208
- } else {
209
- console.log(` Status: ⚠️ Incomplete cache (stale)`);
210
- }
211
- } catch {
212
- console.log(` Status: Invalid cache directory`);
213
- }
214
- } else {
215
- console.log(` Status: ❌ Invalid or corrupted (${err.message})`);
216
- }
217
- }
641
+ const cacheDirs = await fs.readdir(globalCacheRoot).catch(() => []);
642
+
643
+ if (cacheDirs.length === 0) {
644
+ console.info('[Status] No cache directories found.');
645
+ console.info(`[Status] Expected location: ${globalCacheRoot}`);
646
+ } else {
647
+ console.info(
648
+ `[Status] Found ${cacheDirs.length} cache director${cacheDirs.length === 1 ? 'y' : 'ies'} in ${globalCacheRoot}`
649
+ );
650
+
651
+ for (const dir of cacheDirs) {
652
+ const cacheDir = path.join(globalCacheRoot, dir);
653
+ const metaFile = path.join(cacheDir, 'meta.json');
654
+ const progressFile = path.join(cacheDir, 'progress.json');
655
+
656
+ console.info(`${'─'.repeat(60)}`);
657
+ console.info(`📁 Cache: ${dir}`);
658
+ console.info(` Path: ${cacheDir}`);
659
+
660
+ let metaData = null;
661
+ try {
662
+ metaData = JSON.parse(await fs.readFile(metaFile, 'utf-8'));
663
+
664
+ console.info(` Status: Valid cache`);
665
+ console.info(` Workspace: ${metaData.workspace || 'Unknown'}`);
666
+ console.info(` Files indexed: ${metaData.filesIndexed ?? 'N/A'}`);
667
+ console.info(` Chunks stored: ${metaData.chunksStored ?? 'N/A'}`);
668
+
669
+ if (Number.isFinite(metaData.lastDiscoveredFiles)) {
670
+ console.info(` Files discovered (last run): ${metaData.lastDiscoveredFiles}`);
671
+ }
672
+ if (Number.isFinite(metaData.lastFilesProcessed)) {
673
+ console.info(` Files processed (last run): ${metaData.lastFilesProcessed}`);
674
+ }
675
+ if (
676
+ Number.isFinite(metaData.lastDiscoveredFiles) &&
677
+ Number.isFinite(metaData.lastFilesProcessed)
678
+ ) {
679
+ const delta = metaData.lastDiscoveredFiles - metaData.lastFilesProcessed;
680
+ console.info(` Discovery delta (last run): ${delta >= 0 ? delta : 0}`);
681
+ }
682
+
683
+ if (metaData.lastSaveTime) {
684
+ const saveDate = new Date(metaData.lastSaveTime);
685
+ const ageMs = now - saveDate;
686
+ const ageHours = Math.floor(ageMs / (1000 * 60 * 60));
687
+ const ageMins = Math.floor((ageMs % (1000 * 60 * 60)) / (1000 * 60));
688
+ console.info(
689
+ ` Cached snapshot saved: ${formatDateTime(saveDate)} (${ageHours}h ${ageMins}m ago)`
690
+ );
691
+ const ageLabel = formatDurationMs(ageMs);
692
+ if (ageLabel) {
693
+ console.info(` Cached snapshot age: ${ageLabel}`);
694
+ }
695
+ console.info(` Initial index complete at: ${formatDateTime(saveDate)}`);
696
+ }
697
+ if (metaData.lastIndexStartedAt) {
698
+ console.info(
699
+ ` Last index started: ${formatDateTime(metaData.lastIndexStartedAt)}`
700
+ );
701
+ }
702
+ if (metaData.lastIndexEndedAt) {
703
+ console.info(
704
+ ` Last index ended: ${formatDateTime(metaData.lastIndexEndedAt)}`
705
+ );
706
+ }
707
+ if (Number.isFinite(metaData.indexDurationMs)) {
708
+ const duration = formatDurationMs(metaData.indexDurationMs);
709
+ if (duration) {
710
+ console.info(` Last full index duration: ${duration}`);
711
+ }
712
+ }
713
+ if (metaData.lastIndexMode) {
714
+ console.info(` Last index mode: ${String(metaData.lastIndexMode)}`);
715
+ }
716
+ if (Number.isFinite(metaData.lastBatchSize)) {
717
+ console.info(` Last batch size: ${metaData.lastBatchSize}`);
718
+ }
719
+ if (Number.isFinite(metaData.lastWorkerThreads)) {
720
+ console.info(` Last worker threads: ${metaData.lastWorkerThreads}`);
721
+ }
722
+ try {
723
+ const dirStats = await fs.stat(cacheDir);
724
+ console.info(
725
+ ` Cache dir last write: ${formatDateTime(dirStats.mtime)}`
726
+ );
727
+ } catch {
728
+ // ignore cache dir stat errors
729
+ }
730
+
731
+ // Verify indexing completion
732
+ if (metaData.filesIndexed && metaData.filesIndexed > 0) {
733
+ console.info(` Cached index: ✅ COMPLETE (${metaData.filesIndexed} files)`);
734
+ } else if (metaData.filesIndexed === 0) {
735
+ console.info(` Cached index: ⚠️ NO FILES (check excludePatterns)`);
736
+ } else {
737
+ console.info(` Cached index: ⚠️ INCOMPLETE`);
738
+ }
739
+ } catch (err) {
740
+ if (err.code === 'ENOENT') {
741
+ try {
742
+ const stats = await fs.stat(cacheDir);
743
+ const ageMs = new Date() - stats.mtime;
744
+ if (ageMs < 10 * 60 * 1000) {
745
+ console.info(` Status: ⏳ Initializing / Indexing in progress...`);
746
+ console.info(` (Metadata file has not been written yet using ID ${dir})`);
747
+ console.info(' Initial index: ⏳ IN PROGRESS');
748
+ } else {
749
+ console.info(` Status: ⚠️ Incomplete cache (stale)`);
750
+ }
751
+ console.info(
752
+ ` Cache dir last write: ${stats.mtime.toLocaleString()}`
753
+ );
754
+ } catch {
755
+ console.info(` Status: ❌ Invalid cache directory`);
218
756
  }
219
- console.log(`${'─'.repeat(60)}`);
757
+ } else {
758
+ console.info(` Status: ❌ Invalid or corrupted (${err.message})`);
759
+ }
220
760
  }
221
761
 
222
- // SHOW PATHS
223
- console.log('\n[Paths] Important locations:');
224
-
225
- // Global npm bin
226
- let npmBin = 'unknown';
762
+ // Show latest indexing progress if available
763
+ let progressData = null;
227
764
  try {
228
- const { stdout } = await execPromise('npm config get prefix');
229
- npmBin = path.join(stdout.trim(), 'bin');
230
- } catch {}
231
- console.log(` 📦 Global npm bin: ${npmBin}`);
232
-
233
- // Configs
234
- const configLocations = [
235
- { name: 'Antigravity', path: path.join(os.homedir(), '.gemini', 'antigravity', 'mcp_config.json') },
236
- { name: 'Cursor', path: path.join(os.homedir(), '.config', 'Cursor', 'User', 'settings.json') }
237
- ];
238
-
239
- // Platform specific logic for Cursor
240
- if (process.platform === 'darwin') {
241
- configLocations[1].path = path.join(os.homedir(), 'Library', 'Application Support', 'Cursor', 'User', 'settings.json');
242
- } else if (process.platform === 'win32') {
243
- configLocations[1].path = path.join(process.env.APPDATA || '', 'Cursor', 'User', 'settings.json');
765
+ progressData = JSON.parse(await fs.readFile(progressFile, 'utf-8'));
766
+ } catch {
767
+ // no progress file
244
768
  }
245
769
 
246
- console.log(' ⚙️ MCP configs:');
247
- for (const loc of configLocations) {
248
- let status = '(not found)';
249
- try { await fs.access(loc.path); status = '(exists)'; } catch {}
250
- console.log(` - ${loc.name}: ${loc.path} ${status}`);
770
+ if (progressData && typeof progressData.progress === 'number') {
771
+ const updatedAt = progressData.updatedAt
772
+ ? formatDateTime(progressData.updatedAt)
773
+ : 'Unknown';
774
+ const progressLabel = metaData
775
+ ? 'Incremental update (post-snapshot)'
776
+ : 'Initial index progress';
777
+ console.info(
778
+ ` ${progressLabel}: ${progressData.progress}/${progressData.total} (${progressData.message || 'n/a'})`
779
+ );
780
+ console.info(` Progress updated: ${updatedAt}`);
781
+
782
+ if (progressData.updatedAt) {
783
+ const updatedDate = new Date(progressData.updatedAt);
784
+ const ageMs = now - updatedDate;
785
+ const staleMs = 5 * 60 * 1000;
786
+ const ageLabel = formatDurationMs(ageMs);
787
+ if (ageLabel) {
788
+ console.info(` Progress age: ${ageLabel}`);
789
+ }
790
+ if (Number.isFinite(ageMs) && ageMs > staleMs) {
791
+ const staleLabel = formatDurationMs(ageMs);
792
+ console.info(` Progress stale: last update ${staleLabel} ago`);
793
+ }
794
+ }
795
+
796
+ if (progressData.updatedAt && metaData?.lastSaveTime) {
797
+ const updatedDate = new Date(progressData.updatedAt);
798
+ const saveDate = new Date(metaData.lastSaveTime);
799
+ if (updatedDate > saveDate) {
800
+ console.info(' Note: Incremental update in progress; cached snapshot may lag.');
801
+ }
802
+ }
803
+ if (progressData.indexMode) {
804
+ console.info(` Current index mode: ${String(progressData.indexMode)}`);
805
+ }
806
+ if (progressData.workerCircuitOpen && Number.isFinite(progressData.workersDisabledUntil)) {
807
+ const remainingMs = progressData.workersDisabledUntil - Date.now();
808
+ const remainingLabel = formatDurationMs(Math.max(0, remainingMs));
809
+ console.info(` Workers paused: ${remainingLabel || '0s'} remaining`);
810
+ console.info(
811
+ ` Workers disabled until: ${formatDateTime(progressData.workersDisabledUntil)}`
812
+ );
813
+ }
814
+ } else {
815
+ if (metaData) {
816
+ console.info(' Summary: Cached snapshot available; no update running.');
817
+ } else {
818
+ console.info(' Summary: No cached snapshot yet; indexing has not started.');
819
+ }
820
+ }
821
+
822
+ if (metaData && progressData && typeof progressData.progress === 'number') {
823
+ console.info(' Indexing state: Cached snapshot available; incremental update running.');
824
+ } else if (metaData) {
825
+ console.info(' Indexing state: Cached snapshot available; idle.');
826
+ } else if (progressData && typeof progressData.progress === 'number') {
827
+ console.info(' Indexing state: Initial index in progress; no cached snapshot yet.');
828
+ } else {
829
+ console.info(' Indexing state: No cached snapshot; idle.');
251
830
  }
831
+ }
832
+ console.info(`${'─'.repeat(60)}`);
833
+ }
252
834
 
253
- console.log(` 💾 Cache root: ${globalCacheRoot}`);
254
- console.log(` 📁 Current dir: ${process.cwd()}`);
255
- console.log('');
835
+ // SHOW PATHS
836
+ console.info('\n[Paths] Important locations:');
256
837
 
257
- } catch (error) {
258
- console.error(`[Lifecycle] Failed to check status: ${error.message}`);
838
+ // Global npm bin
839
+ let npmBin = 'unknown';
840
+ try {
841
+ const { stdout } = await execPromise('npm config get prefix');
842
+ npmBin = path.join(stdout.trim(), 'bin');
843
+ } catch { /* ignore */ }
844
+ console.info(` 📦 Global npm bin: ${npmBin}`);
845
+
846
+ // Configs
847
+ const configLocations = [
848
+ {
849
+ name: 'Antigravity',
850
+ path: path.join(os.homedir(), '.gemini', 'antigravity', 'mcp_config.json'),
851
+ },
852
+ {
853
+ name: 'Cursor',
854
+ path: path.join(os.homedir(), '.config', 'Cursor', 'User', 'settings.json'),
855
+ },
856
+ ];
857
+
858
+ // Platform specific logic for Cursor
859
+ if (process.platform === 'darwin') {
860
+ configLocations[1].path = path.join(
861
+ os.homedir(),
862
+ 'Library',
863
+ 'Application Support',
864
+ 'Cursor',
865
+ 'User',
866
+ 'settings.json'
867
+ );
868
+ } else if (process.platform === 'win32') {
869
+ configLocations[1].path = path.join(
870
+ process.env.APPDATA || '',
871
+ 'Cursor',
872
+ 'User',
873
+ 'settings.json'
874
+ );
259
875
  }
876
+
877
+ console.info(' ⚙️ MCP configs:');
878
+ for (const loc of configLocations) {
879
+ let status = '(not found)';
880
+ try {
881
+ await fs.access(loc.path);
882
+ status = '(exists)';
883
+ } catch { /* ignore */ }
884
+ console.info(` - ${loc.name}: ${loc.path} ${status}`);
885
+ }
886
+
887
+ console.info(` 💾 Cache root: ${globalCacheRoot}`);
888
+ console.info(` 📁 Current dir: ${process.cwd()}`);
889
+ console.info('');
890
+ } catch (error) {
891
+ console.error(`[Lifecycle] Failed to check status: ${error.message}`);
892
+ }
260
893
  }