@softerist/heuristic-mcp 3.2.3 → 3.2.4

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 (46) hide show
  1. package/README.md +387 -376
  2. package/config.jsonc +800 -800
  3. package/features/ann-config.js +102 -110
  4. package/features/clear-cache.js +81 -84
  5. package/features/find-similar-code.js +265 -286
  6. package/features/hybrid-search.js +487 -536
  7. package/features/index-codebase.js +3139 -3270
  8. package/features/lifecycle.js +1011 -1063
  9. package/features/package-version.js +277 -291
  10. package/features/register.js +351 -370
  11. package/features/resources.js +115 -130
  12. package/features/set-workspace.js +214 -240
  13. package/index.js +693 -758
  14. package/lib/cache-ops.js +22 -22
  15. package/lib/cache-utils.js +465 -519
  16. package/lib/cache.js +1749 -1849
  17. package/lib/call-graph.js +396 -396
  18. package/lib/cli.js +232 -226
  19. package/lib/config.js +1483 -1495
  20. package/lib/constants.js +511 -493
  21. package/lib/embed-query-process.js +206 -212
  22. package/lib/embedding-process.js +434 -451
  23. package/lib/embedding-worker.js +862 -934
  24. package/lib/ignore-patterns.js +276 -316
  25. package/lib/json-worker.js +14 -14
  26. package/lib/json-writer.js +302 -310
  27. package/lib/logging.js +116 -127
  28. package/lib/memory-logger.js +13 -13
  29. package/lib/onnx-backend.js +188 -193
  30. package/lib/path-utils.js +18 -23
  31. package/lib/project-detector.js +82 -84
  32. package/lib/server-lifecycle.js +133 -145
  33. package/lib/settings-editor.js +738 -739
  34. package/lib/slice-normalize.js +25 -31
  35. package/lib/tokenizer.js +168 -203
  36. package/lib/utils.js +364 -409
  37. package/lib/vector-store-binary.js +973 -991
  38. package/lib/vector-store-sqlite.js +377 -414
  39. package/lib/workspace-env.js +32 -34
  40. package/mcp_config.json +9 -9
  41. package/package.json +86 -86
  42. package/scripts/clear-cache.js +20 -20
  43. package/scripts/download-model.js +43 -43
  44. package/scripts/mcp-launcher.js +49 -49
  45. package/scripts/postinstall.js +12 -12
  46. package/search-configs.js +36 -36
package/index.js CHANGED
@@ -1,44 +1,48 @@
1
1
  #!/usr/bin/env node
2
- import { Server } from '@modelcontextprotocol/sdk/server/index.js';
3
- import { stop, start, status, logs } from './features/lifecycle.js';
4
- import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
5
- import {
6
- CallToolRequestSchema,
7
- ListToolsRequestSchema,
8
- ListResourcesRequestSchema,
9
- ReadResourceRequestSchema,
10
- RootsListChangedNotificationSchema,
11
- } from '@modelcontextprotocol/sdk/types.js';
12
- let transformersModule = null;
13
- async function getTransformers() {
14
- if (!transformersModule) {
15
- transformersModule = await import('@huggingface/transformers');
16
- if (transformersModule?.env) {
17
- transformersModule.env.cacheDir = path.join(getGlobalCacheDir(), 'xenova');
18
- }
19
- if (transformersModule?.env) {
20
- transformersModule.env.cacheDir = path.join(getGlobalCacheDir(), 'xenova');
21
- }
22
- }
23
- return transformersModule;
24
- }
25
- import { configureNativeOnnxBackend, getNativeOnnxStatus } from './lib/onnx-backend.js';
26
-
27
- import fs from 'fs/promises';
28
- import path from 'path';
29
- import os from 'os';
30
-
31
- import { createRequire } from 'module';
2
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
3
+ import { stop, start, status, logs } from './features/lifecycle.js';
4
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
5
+ import {
6
+ CallToolRequestSchema,
7
+ ListToolsRequestSchema,
8
+ ListResourcesRequestSchema,
9
+ ReadResourceRequestSchema,
10
+ RootsListChangedNotificationSchema,
11
+ } from '@modelcontextprotocol/sdk/types.js';
12
+ let transformersModule = null;
13
+ async function getTransformers() {
14
+ if (!transformersModule) {
15
+ transformersModule = await import('@huggingface/transformers');
16
+ if (transformersModule?.env) {
17
+ transformersModule.env.cacheDir = path.join(getGlobalCacheDir(), 'xenova');
18
+ }
19
+ if (transformersModule?.env) {
20
+ transformersModule.env.cacheDir = path.join(getGlobalCacheDir(), 'xenova');
21
+ }
22
+ }
23
+ return transformersModule;
24
+ }
25
+ import { configureNativeOnnxBackend, getNativeOnnxStatus } from './lib/onnx-backend.js';
26
+
27
+ import fs from 'fs/promises';
28
+ import path from 'path';
29
+ import os from 'os';
30
+
31
+ import { createRequire } from 'module';
32
32
  import { fileURLToPath } from 'url';
33
33
  import { getWorkspaceCachePath } from './lib/workspace-cache-key.js';
34
-
35
-
36
- const require = createRequire(import.meta.url);
37
- const packageJson = require('./package.json');
38
-
34
+
35
+ const require = createRequire(import.meta.url);
36
+ const packageJson = require('./package.json');
37
+
39
38
  import { loadConfig, getGlobalCacheDir, isNonProjectDirectory } from './lib/config.js';
40
39
  import { clearStaleCaches } from './lib/cache-utils.js';
41
- import { enableStderrOnlyLogging, setupFileLogging, getLogFilePath, flushLogs } from './lib/logging.js';
40
+ import {
41
+ enableStderrOnlyLogging,
42
+ setupFileLogging,
43
+ getLogFilePath,
44
+ flushLogs,
45
+ } from './lib/logging.js';
42
46
  import { parseArgs, printHelp } from './lib/cli.js';
43
47
  import { clearCache } from './lib/cache-ops.js';
44
48
  import { logMemory, startMemoryLogger } from './lib/memory-logger.js';
@@ -48,89 +52,90 @@ import {
48
52
  acquireWorkspaceLock,
49
53
  stopOtherHeuristicServers,
50
54
  } from './lib/server-lifecycle.js';
51
-
55
+
52
56
  import { EmbeddingsCache } from './lib/cache.js';
53
- import { cleanupStaleBinaryArtifacts, recordBinaryStoreCorruption } from './lib/vector-store-binary.js';
57
+ import {
58
+ cleanupStaleBinaryArtifacts,
59
+ recordBinaryStoreCorruption,
60
+ } from './lib/vector-store-binary.js';
54
61
  import { CodebaseIndexer } from './features/index-codebase.js';
55
62
  import { HybridSearch } from './features/hybrid-search.js';
56
-
57
- import * as IndexCodebaseFeature from './features/index-codebase.js';
58
- import * as HybridSearchFeature from './features/hybrid-search.js';
59
- import * as ClearCacheFeature from './features/clear-cache.js';
60
- import * as FindSimilarCodeFeature from './features/find-similar-code.js';
61
- import * as AnnConfigFeature from './features/ann-config.js';
62
- import * as PackageVersionFeature from './features/package-version.js';
63
- import * as SetWorkspaceFeature from './features/set-workspace.js';
64
- import { handleListResources, handleReadResource } from './features/resources.js';
65
- import { getWorkspaceEnvKeys } from './lib/workspace-env.js';
66
-
67
- import {
68
- MEMORY_LOG_INTERVAL_MS,
69
- ONNX_THREAD_LIMIT,
70
- BACKGROUND_INDEX_DELAY_MS,
71
- } from './lib/constants.js';
72
- const PID_FILE_NAME = '.heuristic-mcp.pid';
73
-
74
- async function readLogTail(logPath, maxLines = 2000) {
75
- const data = await fs.readFile(logPath, 'utf-8');
76
- if (!data) return [];
77
- const lines = data.split(/\r?\n/).filter(Boolean);
78
- return lines.slice(-maxLines);
79
- }
80
-
81
- async function printMemorySnapshot(workspaceDir) {
82
- const activeConfig = await loadConfig(workspaceDir);
83
- const logPath = getLogFilePath(activeConfig);
84
-
85
- let lines;
86
- try {
87
- lines = await readLogTail(logPath);
88
- } catch (err) {
89
- if (err.code === 'ENOENT') {
90
- console.error(`[Memory] No log file found for workspace.`);
91
- console.error(`[Memory] Expected location: ${logPath}`);
92
- console.error(
93
- '[Memory] Start the server with verbose logging (set "verbose": true), then try again.'
94
- );
95
- return false;
96
- }
97
- console.error(`[Memory] Failed to read log file: ${err.message}`);
98
- return false;
99
- }
100
-
101
- const memoryLines = lines.filter((line) => /Memory\s*\(/.test(line) || /Memory.*rss=/.test(line));
102
- if (memoryLines.length === 0) {
103
- console.info('[Memory] No memory snapshots found in logs.');
104
- console.info('[Memory] Ensure "verbose": true in config and restart the server.');
105
- return true;
106
- }
107
-
108
- const idleLine =
109
- [...memoryLines].reverse().find((line) => line.includes('after cache load')) ??
110
- memoryLines[memoryLines.length - 1];
111
-
112
- const logLine = (line) => {
113
- console.info(line);
114
- if (process.env.VITEST === 'true' || process.env.NODE_ENV === 'test') {
115
- console.error(line);
116
- }
117
- };
118
-
119
- logLine(`[Memory] Idle snapshot: ${idleLine}`);
120
-
121
- const latestLine = memoryLines[memoryLines.length - 1];
122
- if (latestLine !== idleLine) {
123
- logLine(`[Memory] Latest snapshot: ${latestLine}`);
124
- }
125
-
126
- return true;
127
- }
128
-
129
-
130
-
131
-
63
+
64
+ import * as IndexCodebaseFeature from './features/index-codebase.js';
65
+ import * as HybridSearchFeature from './features/hybrid-search.js';
66
+ import * as ClearCacheFeature from './features/clear-cache.js';
67
+ import * as FindSimilarCodeFeature from './features/find-similar-code.js';
68
+ import * as AnnConfigFeature from './features/ann-config.js';
69
+ import * as PackageVersionFeature from './features/package-version.js';
70
+ import * as SetWorkspaceFeature from './features/set-workspace.js';
71
+ import { handleListResources, handleReadResource } from './features/resources.js';
72
+ import { getWorkspaceEnvKeys } from './lib/workspace-env.js';
73
+
74
+ import {
75
+ MEMORY_LOG_INTERVAL_MS,
76
+ ONNX_THREAD_LIMIT,
77
+ BACKGROUND_INDEX_DELAY_MS,
78
+ SERVER_KEEP_ALIVE_INTERVAL_MS,
79
+ } from './lib/constants.js';
80
+ const PID_FILE_NAME = '.heuristic-mcp.pid';
81
+
82
+ async function readLogTail(logPath, maxLines = 2000) {
83
+ const data = await fs.readFile(logPath, 'utf-8');
84
+ if (!data) return [];
85
+ const lines = data.split(/\r?\n/).filter(Boolean);
86
+ return lines.slice(-maxLines);
87
+ }
88
+
89
+ async function printMemorySnapshot(workspaceDir) {
90
+ const activeConfig = await loadConfig(workspaceDir);
91
+ const logPath = getLogFilePath(activeConfig);
92
+
93
+ let lines;
94
+ try {
95
+ lines = await readLogTail(logPath);
96
+ } catch (err) {
97
+ if (err.code === 'ENOENT') {
98
+ console.error(`[Memory] No log file found for workspace.`);
99
+ console.error(`[Memory] Expected location: ${logPath}`);
100
+ console.error(
101
+ '[Memory] Start the server with verbose logging (set "verbose": true), then try again.'
102
+ );
103
+ return false;
104
+ }
105
+ console.error(`[Memory] Failed to read log file: ${err.message}`);
106
+ return false;
107
+ }
108
+
109
+ const memoryLines = lines.filter((line) => /Memory\s*\(/.test(line) || /Memory.*rss=/.test(line));
110
+ if (memoryLines.length === 0) {
111
+ console.info('[Memory] No memory snapshots found in logs.');
112
+ console.info('[Memory] Ensure "verbose": true in config and restart the server.');
113
+ return true;
114
+ }
115
+
116
+ const idleLine =
117
+ [...memoryLines].reverse().find((line) => line.includes('after cache load')) ??
118
+ memoryLines[memoryLines.length - 1];
119
+
120
+ const logLine = (line) => {
121
+ console.info(line);
122
+ if (process.env.VITEST === 'true' || process.env.NODE_ENV === 'test') {
123
+ console.error(line);
124
+ }
125
+ };
126
+
127
+ logLine(`[Memory] Idle snapshot: ${idleLine}`);
128
+
129
+ const latestLine = memoryLines[memoryLines.length - 1];
130
+ if (latestLine !== idleLine) {
131
+ logLine(`[Memory] Latest snapshot: ${latestLine}`);
132
+ }
133
+
134
+ return true;
135
+ }
136
+
132
137
  let embedder = null;
133
- let unloadMainEmbedder = null;
138
+ let unloadMainEmbedder = null;
134
139
  let cache = null;
135
140
  let indexer = null;
136
141
  let hybridSearch = null;
@@ -235,27 +240,27 @@ function registerProcessDiagnostics({ isServerMode, requestShutdown, getShutdown
235
240
  handleFatalError('unhandledRejection', reason);
236
241
  });
237
242
  }
238
-
239
- async function resolveWorkspaceFromEnvValue(rawValue) {
240
- if (!rawValue || rawValue.includes('${')) return null;
241
- const resolved = path.resolve(rawValue);
242
- try {
243
- const stats = await fs.stat(resolved);
244
- if (!stats.isDirectory()) return null;
245
- return resolved;
246
- } catch {
247
- return null;
248
- }
249
- }
250
-
243
+
244
+ async function resolveWorkspaceFromEnvValue(rawValue) {
245
+ if (!rawValue || rawValue.includes('${')) return null;
246
+ const resolved = path.resolve(rawValue);
247
+ try {
248
+ const stats = await fs.stat(resolved);
249
+ if (!stats.isDirectory()) return null;
250
+ return resolved;
251
+ } catch {
252
+ return null;
253
+ }
254
+ }
255
+
251
256
  async function detectRuntimeWorkspaceFromEnv() {
252
257
  for (const key of getWorkspaceEnvKeys()) {
253
258
  const workspacePath = await resolveWorkspaceFromEnvValue(process.env[key]);
254
- if (workspacePath) {
255
- return { workspacePath, envKey: key };
256
- }
257
- }
258
-
259
+ if (workspacePath) {
260
+ return { workspacePath, envKey: key };
261
+ }
262
+ }
263
+
259
264
  return null;
260
265
  }
261
266
 
@@ -301,7 +306,8 @@ async function findAutoAttachWorkspaceCandidate({ excludeCacheDirectory = null }
301
306
  for (const entry of cacheDirs) {
302
307
  if (!entry.isDirectory()) continue;
303
308
  const cacheDirectory = path.join(cacheRoot, entry.name);
304
- if (normalizedExclude && normalizePathForCompare(cacheDirectory) === normalizedExclude) continue;
309
+ if (normalizedExclude && normalizePathForCompare(cacheDirectory) === normalizedExclude)
310
+ continue;
305
311
 
306
312
  const lockPath = path.join(cacheDirectory, 'server.lock.json');
307
313
  try {
@@ -320,9 +326,7 @@ async function findAutoAttachWorkspaceCandidate({ excludeCacheDirectory = null }
320
326
  rank,
321
327
  });
322
328
  continue;
323
- } catch {
324
-
325
- }
329
+ } catch {}
326
330
 
327
331
  const metaPath = path.join(cacheDirectory, 'meta.json');
328
332
  try {
@@ -341,9 +345,7 @@ async function findAutoAttachWorkspaceCandidate({ excludeCacheDirectory = null }
341
345
  source: 'meta',
342
346
  rank,
343
347
  });
344
- } catch {
345
-
346
- }
348
+ } catch {}
347
349
  }
348
350
 
349
351
  const candidates = Array.from(candidatesByWorkspace.values());
@@ -357,7 +359,7 @@ async function findAutoAttachWorkspaceCandidate({ excludeCacheDirectory = null }
357
359
  if (candidates.length === 1) return candidates[0];
358
360
  return null;
359
361
  }
360
-
362
+
361
363
  async function maybeAutoSwitchWorkspace(request) {
362
364
  if (process.env.VITEST === 'true' || process.env.NODE_ENV === 'test') return null;
363
365
  if (!setWorkspaceFeatureInstance || !config?.searchDirectory) return null;
@@ -383,8 +385,6 @@ async function maybeAutoSwitchWorkspace(request) {
383
385
  return detected.workspacePath;
384
386
  }
385
387
 
386
-
387
-
388
388
  async function detectWorkspaceFromRoots({ quiet = false } = {}) {
389
389
  try {
390
390
  const caps = server.getClientCapabilities();
@@ -408,14 +408,13 @@ async function detectWorkspaceFromRoots({ quiet = false } = {}) {
408
408
  }
409
409
 
410
410
  if (!quiet) {
411
- console.info(`[Server] MCP roots received: ${result.roots.map(r => r.uri).join(', ')}`);
411
+ console.info(`[Server] MCP roots received: ${result.roots.map((r) => r.uri).join(', ')}`);
412
412
  }
413
-
414
-
413
+
415
414
  const rootPaths = result.roots
416
- .map(r => r.uri)
417
- .filter(uri => uri.startsWith('file://'))
418
- .map(uri => {
415
+ .map((r) => r.uri)
416
+ .filter((uri) => uri.startsWith('file://'))
417
+ .map((uri) => {
419
418
  try {
420
419
  return fileURLToPath(uri);
421
420
  } catch {
@@ -423,7 +422,7 @@ async function detectWorkspaceFromRoots({ quiet = false } = {}) {
423
422
  }
424
423
  })
425
424
  .filter(Boolean);
426
-
425
+
427
426
  if (rootPaths.length === 0) {
428
427
  if (!quiet) {
429
428
  console.info('[Server] No valid file:// roots found.');
@@ -440,7 +439,10 @@ async function detectWorkspaceFromRoots({ quiet = false } = {}) {
440
439
  }
441
440
  }
442
441
 
443
- async function maybeAutoSwitchWorkspaceToPath(targetWorkspacePath, { source, reindex = false } = {}) {
442
+ async function maybeAutoSwitchWorkspaceToPath(
443
+ targetWorkspacePath,
444
+ { source, reindex = false } = {}
445
+ ) {
444
446
  if (!setWorkspaceFeatureInstance || !config?.searchDirectory) return;
445
447
  if (!targetWorkspacePath) return;
446
448
  if (isNonProjectDirectory(targetWorkspacePath)) {
@@ -470,9 +472,7 @@ async function maybeAutoSwitchWorkspaceToPath(targetWorkspacePath, { source, rei
470
472
  reindex,
471
473
  });
472
474
  if (!result.success) {
473
- console.warn(
474
- `[Server] Auto workspace switch failed (${source || 'auto'}): ${result.error}`
475
- );
475
+ console.warn(`[Server] Auto workspace switch failed (${source || 'auto'}): ${result.error}`);
476
476
  return;
477
477
  }
478
478
  trustWorkspacePath(targetWorkspacePath);
@@ -511,119 +511,108 @@ async function maybeAutoSwitchWorkspaceFromRoots(request) {
511
511
  rootsProbeInFlight = null;
512
512
  }
513
513
  }
514
-
515
-
516
-
517
- const features = [
518
- {
519
- module: HybridSearchFeature,
520
- instance: null,
521
- handler: HybridSearchFeature.handleToolCall,
522
- },
523
- {
524
- module: IndexCodebaseFeature,
525
- instance: null,
526
- handler: IndexCodebaseFeature.handleToolCall,
527
- },
528
- {
529
- module: ClearCacheFeature,
530
- instance: null,
531
- handler: ClearCacheFeature.handleToolCall,
532
- },
533
- {
534
- module: FindSimilarCodeFeature,
535
- instance: null,
536
- handler: FindSimilarCodeFeature.handleToolCall,
537
- },
538
- {
539
- module: AnnConfigFeature,
540
- instance: null,
541
- handler: AnnConfigFeature.handleToolCall,
542
- },
543
- {
544
- module: PackageVersionFeature,
545
- instance: null,
546
- handler: PackageVersionFeature.handleToolCall,
547
- },
514
+
515
+ const features = [
516
+ {
517
+ module: HybridSearchFeature,
518
+ instance: null,
519
+ handler: HybridSearchFeature.handleToolCall,
520
+ },
521
+ {
522
+ module: IndexCodebaseFeature,
523
+ instance: null,
524
+ handler: IndexCodebaseFeature.handleToolCall,
525
+ },
526
+ {
527
+ module: ClearCacheFeature,
528
+ instance: null,
529
+ handler: ClearCacheFeature.handleToolCall,
530
+ },
531
+ {
532
+ module: FindSimilarCodeFeature,
533
+ instance: null,
534
+ handler: FindSimilarCodeFeature.handleToolCall,
535
+ },
536
+ {
537
+ module: AnnConfigFeature,
538
+ instance: null,
539
+ handler: AnnConfigFeature.handleToolCall,
540
+ },
541
+ {
542
+ module: PackageVersionFeature,
543
+ instance: null,
544
+ handler: PackageVersionFeature.handleToolCall,
545
+ },
548
546
  {
549
547
  module: SetWorkspaceFeature,
550
548
  instance: null,
551
549
  handler: null,
552
550
  },
553
551
  ];
554
-
555
-
556
-
552
+
557
553
  async function initialize(workspaceDir) {
558
-
559
-
560
- config = await loadConfig(workspaceDir);
561
-
562
-
563
-
564
- if (config.enableCache && config.cacheCleanup?.autoCleanup) {
565
- console.info('[Server] Running automatic cache cleanup...');
566
- const results = await clearStaleCaches({
567
- ...config.cacheCleanup,
568
- logger: console,
569
- });
570
- if (results.removed > 0) {
571
- console.info(`[Server] Removed ${results.removed} stale cache ${results.removed === 1 ? 'directory' : 'directories'}`);
572
- }
573
- }
574
-
575
-
576
-
577
- const isTest = Boolean(process.env.VITEST || process.env.VITEST_WORKER_ID);
578
- if (config.enableExplicitGc && typeof global.gc !== 'function' && !isTest) {
579
- console.warn(
580
- '[Server] enableExplicitGc=true but this process was not started with --expose-gc; continuing with explicit GC disabled.'
581
- );
582
- console.warn(
583
- '[Server] Tip: start with "npm start" or add --expose-gc to enable explicit GC again.'
584
- );
585
- config.enableExplicitGc = false;
586
- }
587
-
588
- let mainBackendConfigured = false;
589
- let nativeOnnxAvailable = null;
590
- const ensureMainOnnxBackend = () => {
591
- if (mainBackendConfigured) return;
592
- nativeOnnxAvailable = configureNativeOnnxBackend({
593
- log: config.verbose ? console.info : null,
594
- label: '[Server]',
595
- threads: {
596
- intraOpNumThreads: ONNX_THREAD_LIMIT,
597
- interOpNumThreads: 1,
598
- },
599
- });
600
- mainBackendConfigured = true;
601
- };
602
-
603
- ensureMainOnnxBackend();
604
- if (nativeOnnxAvailable === false) {
605
- try {
606
- const { env } = await getTransformers();
607
- if (env?.backends?.onnx?.wasm) {
608
- env.backends.onnx.wasm.numThreads = ONNX_THREAD_LIMIT;
609
- }
610
- } catch {
611
-
612
-
613
- }
614
- const status = getNativeOnnxStatus();
615
- const reason = status?.message || 'onnxruntime-node not available';
616
- console.warn(`[Server] Native ONNX backend unavailable (${reason}); using WASM backend.`);
617
- console.warn(
618
- '[Server] Auto-safety: disabling workers and forcing embeddingProcessPerBatch for memory isolation.'
619
- );
620
- if (config.workerThreads !== 0) {
621
- config.workerThreads = 0;
622
- }
623
- if (!config.embeddingProcessPerBatch) {
624
- config.embeddingProcessPerBatch = true;
625
- }
626
- }
554
+ config = await loadConfig(workspaceDir);
555
+
556
+ if (config.enableCache && config.cacheCleanup?.autoCleanup) {
557
+ console.info('[Server] Running automatic cache cleanup...');
558
+ const results = await clearStaleCaches({
559
+ ...config.cacheCleanup,
560
+ logger: console,
561
+ });
562
+ if (results.removed > 0) {
563
+ console.info(
564
+ `[Server] Removed ${results.removed} stale cache ${results.removed === 1 ? 'directory' : 'directories'}`
565
+ );
566
+ }
567
+ }
568
+
569
+ const isTest = Boolean(process.env.VITEST || process.env.VITEST_WORKER_ID);
570
+ if (config.enableExplicitGc && typeof global.gc !== 'function' && !isTest) {
571
+ console.warn(
572
+ '[Server] enableExplicitGc=true but this process was not started with --expose-gc; continuing with explicit GC disabled.'
573
+ );
574
+ console.warn(
575
+ '[Server] Tip: start with "npm start" or add --expose-gc to enable explicit GC again.'
576
+ );
577
+ config.enableExplicitGc = false;
578
+ }
579
+
580
+ let mainBackendConfigured = false;
581
+ let nativeOnnxAvailable = null;
582
+ const ensureMainOnnxBackend = () => {
583
+ if (mainBackendConfigured) return;
584
+ nativeOnnxAvailable = configureNativeOnnxBackend({
585
+ log: config.verbose ? console.info : null,
586
+ label: '[Server]',
587
+ threads: {
588
+ intraOpNumThreads: ONNX_THREAD_LIMIT,
589
+ interOpNumThreads: 1,
590
+ },
591
+ });
592
+ mainBackendConfigured = true;
593
+ };
594
+
595
+ ensureMainOnnxBackend();
596
+ if (nativeOnnxAvailable === false) {
597
+ try {
598
+ const { env } = await getTransformers();
599
+ if (env?.backends?.onnx?.wasm) {
600
+ env.backends.onnx.wasm.numThreads = ONNX_THREAD_LIMIT;
601
+ }
602
+ } catch {}
603
+ const status = getNativeOnnxStatus();
604
+ const reason = status?.message || 'onnxruntime-node not available';
605
+ console.warn(`[Server] Native ONNX backend unavailable (${reason}); using WASM backend.`);
606
+ console.warn(
607
+ '[Server] Auto-safety: disabling workers and forcing embeddingProcessPerBatch for memory isolation.'
608
+ );
609
+ if (config.workerThreads !== 0) {
610
+ config.workerThreads = 0;
611
+ }
612
+ if (!config.embeddingProcessPerBatch) {
613
+ config.embeddingProcessPerBatch = true;
614
+ }
615
+ }
627
616
  const resolutionSource = config.workspaceResolution?.source || 'unknown';
628
617
  if (resolutionSource === 'workspace-arg' || resolutionSource === 'env') {
629
618
  trustWorkspacePath(config.searchDirectory);
@@ -651,7 +640,9 @@ async function initialize(workspaceDir) {
651
640
  const details = killed
652
641
  .map((entry) => `${entry.pid}${entry.workspace ? ` (${entry.workspace})` : ''}`)
653
642
  .join(', ');
654
- console.info(`[Server] Auto-stopped ${killed.length} stale heuristic-mcp server(s): ${details}`);
643
+ console.info(
644
+ `[Server] Auto-stopped ${killed.length} stale heuristic-mcp server(s): ${details}`
645
+ );
655
646
  }
656
647
  if (failed.length > 0) {
657
648
  const details = failed
@@ -683,155 +674,143 @@ async function initialize(workspaceDir) {
683
674
  ])
684
675
  : [null, await setupFileLogging(config)];
685
676
  }
686
- if (logPath) {
687
- console.info(`[Logs] Writing server logs to ${logPath}`);
688
- console.info(`[Logs] Log viewer: heuristic-mcp --logs --workspace "${config.searchDirectory}"`);
689
- }
690
- {
691
- const resolution = config.workspaceResolution || {};
692
- const sourceLabel =
693
- resolution.source === 'env' && resolution.envKey
694
- ? `env:${resolution.envKey}`
695
- : resolution.source || 'unknown';
696
- const baseLabel = resolution.baseDirectory || '(unknown)';
697
- const searchLabel = resolution.searchDirectory || config.searchDirectory;
698
- const overrideLabel = resolution.searchDirectoryFromConfig ? 'yes' : 'no';
699
- console.info(
700
- `[Server] Workspace resolved: source=${sourceLabel}, base=${baseLabel}, search=${searchLabel}, configOverride=${overrideLabel}`
701
- );
702
- if (resolution.fromPath) {
703
- console.info(`[Server] Workspace resolution origin cwd: ${resolution.fromPath}`);
704
- }
705
-
706
- const workspaceEnvProbe = Array.isArray(resolution.workspaceEnvProbe)
707
- ? resolution.workspaceEnvProbe
708
- : [];
709
- if (workspaceEnvProbe.length > 0) {
710
- const probePreview = workspaceEnvProbe.slice(0, 8).map((entry) => {
711
- const scope = entry?.priority ? 'priority' : 'diagnostic';
712
- const status = entry?.resolvedPath ? `valid:${entry.resolvedPath}` : `invalid:${entry?.value}`;
713
- return `${entry?.key}[${scope}]=${status}`;
714
- });
715
- const suffix = workspaceEnvProbe.length > 8 ? ` (+${workspaceEnvProbe.length - 8} more)` : '';
716
- console.info(`[Server] Workspace env probe: ${probePreview.join('; ')}${suffix}`);
717
- }
718
- }
719
-
720
-
721
-
722
- console.info(
723
- `[Server] Config: workerThreads=${config.workerThreads}, embeddingProcessPerBatch=${config.embeddingProcessPerBatch}`
724
- );
725
- console.info(
726
- `[Server] Config: vectorStoreLoadMode=${config.vectorStoreLoadMode}, vectorCacheEntries=${config.vectorCacheEntries}`
727
- );
728
-
729
- if (pidPath) {
730
- console.info(`[Server] PID file: ${pidPath}`);
731
- }
732
-
733
-
734
-
735
- try {
736
- const globalCache = path.join(getGlobalCacheDir(), 'heuristic-mcp');
737
- const localCache = path.join(process.cwd(), '.heuristic-mcp');
738
- console.info(`[Server] Cache debug: Global=${globalCache}, Local=${localCache}`);
739
- console.info(`[Server] Process CWD: ${process.cwd()}`);
740
- console.info(`[Server] Resolved workspace: ${config.searchDirectory} (via ${config.workspaceResolution?.source || 'unknown'})`);
741
- } catch (_e) {
742
-
743
-
744
- }
745
-
746
- let stopStartupMemory = null;
747
- if (config.verbose) {
748
- logMemory('[Server] Memory (startup)');
749
- stopStartupMemory = startMemoryLogger('[Server] Memory (startup)', MEMORY_LOG_INTERVAL_MS);
750
- }
751
-
752
-
753
-
754
- try {
755
- await fs.access(config.searchDirectory);
756
- } catch {
757
- console.error(`[Server] Error: Search directory "${config.searchDirectory}" does not exist`);
758
- process.exit(1);
759
- }
760
-
761
-
762
-
763
- console.info('[Server] Initializing features...');
764
- let cachedEmbedderPromise = null;
765
- const lazyEmbedder = async (...args) => {
766
- if (!cachedEmbedderPromise) {
767
- ensureMainOnnxBackend();
768
- console.info(`[Server] Loading AI embedding model: ${config.embeddingModel}...`);
769
- const modelLoadStart = Date.now();
770
- const { pipeline } = await getTransformers();
771
- cachedEmbedderPromise = pipeline('feature-extraction', config.embeddingModel, {
772
- quantized: true,
773
- dtype: 'fp32',
774
- session_options: {
775
- numThreads: 2,
776
- intraOpNumThreads: 2,
777
- interOpNumThreads: 2,
778
- },
779
- }).then((model) => {
780
- const loadSeconds = ((Date.now() - modelLoadStart) / 1000).toFixed(1);
781
- console.info(
782
- `[Server] Embedding model loaded (${loadSeconds}s). Starting intensive indexing (expect high CPU)...`
783
- );
784
- console.info(`[Server] Embedding model ready: ${config.embeddingModel}`);
785
- if (config.verbose) {
786
- logMemory('[Server] Memory (after model load)');
787
- }
788
- return model;
789
- });
790
- }
791
- const model = await cachedEmbedderPromise;
792
- return model(...args);
793
- };
794
-
795
-
796
- const unloader = async () => {
797
- if (!cachedEmbedderPromise) return false;
798
- try {
799
- const model = await cachedEmbedderPromise;
800
- if (model && typeof model.dispose === 'function') {
801
- await model.dispose();
802
- }
803
- cachedEmbedderPromise = null;
804
- if (typeof global.gc === 'function') {
805
- global.gc();
806
- }
807
- if (config.verbose) {
808
- logMemory('[Server] Memory (after model unload)');
809
- }
810
- console.info('[Server] Embedding model unloaded to free memory.');
811
- return true;
812
- } catch (err) {
813
- console.warn(`[Server] Error unloading embedding model: ${err.message}`);
814
- cachedEmbedderPromise = null;
815
- return false;
816
- }
817
- };
818
-
819
- embedder = lazyEmbedder;
820
- unloadMainEmbedder = unloader;
821
- const preloadEmbeddingModel = async () => {
822
- if (config.preloadEmbeddingModel === false) return;
823
- try {
824
- console.info('[Server] Preloading embedding model (background)...');
825
- await embedder(' ');
826
- } catch (err) {
827
- console.warn(`[Server] Embedding model preload failed: ${err.message}`);
828
- }
829
- };
830
-
831
-
832
-
833
-
834
-
677
+ if (logPath) {
678
+ console.info(`[Logs] Writing server logs to ${logPath}`);
679
+ console.info(`[Logs] Log viewer: heuristic-mcp --logs --workspace "${config.searchDirectory}"`);
680
+ }
681
+ {
682
+ const resolution = config.workspaceResolution || {};
683
+ const sourceLabel =
684
+ resolution.source === 'env' && resolution.envKey
685
+ ? `env:${resolution.envKey}`
686
+ : resolution.source || 'unknown';
687
+ const baseLabel = resolution.baseDirectory || '(unknown)';
688
+ const searchLabel = resolution.searchDirectory || config.searchDirectory;
689
+ const overrideLabel = resolution.searchDirectoryFromConfig ? 'yes' : 'no';
690
+ console.info(
691
+ `[Server] Workspace resolved: source=${sourceLabel}, base=${baseLabel}, search=${searchLabel}, configOverride=${overrideLabel}`
692
+ );
693
+ if (resolution.fromPath) {
694
+ console.info(`[Server] Workspace resolution origin cwd: ${resolution.fromPath}`);
695
+ }
696
+
697
+ const workspaceEnvProbe = Array.isArray(resolution.workspaceEnvProbe)
698
+ ? resolution.workspaceEnvProbe
699
+ : [];
700
+ if (workspaceEnvProbe.length > 0) {
701
+ const probePreview = workspaceEnvProbe.slice(0, 8).map((entry) => {
702
+ const scope = entry?.priority ? 'priority' : 'diagnostic';
703
+ const status = entry?.resolvedPath
704
+ ? `valid:${entry.resolvedPath}`
705
+ : `invalid:${entry?.value}`;
706
+ return `${entry?.key}[${scope}]=${status}`;
707
+ });
708
+ const suffix = workspaceEnvProbe.length > 8 ? ` (+${workspaceEnvProbe.length - 8} more)` : '';
709
+ console.info(`[Server] Workspace env probe: ${probePreview.join('; ')}${suffix}`);
710
+ }
711
+ }
712
+
713
+ console.info(
714
+ `[Server] Config: workerThreads=${config.workerThreads}, embeddingProcessPerBatch=${config.embeddingProcessPerBatch}`
715
+ );
716
+ console.info(
717
+ `[Server] Config: vectorStoreLoadMode=${config.vectorStoreLoadMode}, vectorCacheEntries=${config.vectorCacheEntries}`
718
+ );
719
+
720
+ if (pidPath) {
721
+ console.info(`[Server] PID file: ${pidPath}`);
722
+ }
723
+
724
+ try {
725
+ const globalCache = path.join(getGlobalCacheDir(), 'heuristic-mcp');
726
+ const localCache = path.join(process.cwd(), '.heuristic-mcp');
727
+ console.info(`[Server] Cache debug: Global=${globalCache}, Local=${localCache}`);
728
+ console.info(`[Server] Process CWD: ${process.cwd()}`);
729
+ console.info(
730
+ `[Server] Resolved workspace: ${config.searchDirectory} (via ${config.workspaceResolution?.source || 'unknown'})`
731
+ );
732
+ } catch (_e) {}
733
+
734
+ let stopStartupMemory = null;
735
+ if (config.verbose) {
736
+ logMemory('[Server] Memory (startup)');
737
+ stopStartupMemory = startMemoryLogger('[Server] Memory (startup)', MEMORY_LOG_INTERVAL_MS);
738
+ }
739
+
740
+ try {
741
+ await fs.access(config.searchDirectory);
742
+ } catch {
743
+ console.error(`[Server] Error: Search directory "${config.searchDirectory}" does not exist`);
744
+ process.exit(1);
745
+ }
746
+
747
+ console.info('[Server] Initializing features...');
748
+ let cachedEmbedderPromise = null;
749
+ const lazyEmbedder = async (...args) => {
750
+ if (!cachedEmbedderPromise) {
751
+ ensureMainOnnxBackend();
752
+ console.info(`[Server] Loading AI embedding model: ${config.embeddingModel}...`);
753
+ const modelLoadStart = Date.now();
754
+ const { pipeline } = await getTransformers();
755
+ cachedEmbedderPromise = pipeline('feature-extraction', config.embeddingModel, {
756
+ quantized: true,
757
+ dtype: 'fp32',
758
+ session_options: {
759
+ numThreads: 2,
760
+ intraOpNumThreads: 2,
761
+ interOpNumThreads: 2,
762
+ },
763
+ }).then((model) => {
764
+ const loadSeconds = ((Date.now() - modelLoadStart) / 1000).toFixed(1);
765
+ console.info(
766
+ `[Server] Embedding model loaded (${loadSeconds}s). Starting intensive indexing (expect high CPU)...`
767
+ );
768
+ console.info(`[Server] Embedding model ready: ${config.embeddingModel}`);
769
+ if (config.verbose) {
770
+ logMemory('[Server] Memory (after model load)');
771
+ }
772
+ return model;
773
+ });
774
+ }
775
+ const model = await cachedEmbedderPromise;
776
+ return model(...args);
777
+ };
778
+
779
+ const unloader = async () => {
780
+ if (!cachedEmbedderPromise) return false;
781
+ try {
782
+ const model = await cachedEmbedderPromise;
783
+ if (model && typeof model.dispose === 'function') {
784
+ await model.dispose();
785
+ }
786
+ cachedEmbedderPromise = null;
787
+ if (typeof global.gc === 'function') {
788
+ global.gc();
789
+ }
790
+ if (config.verbose) {
791
+ logMemory('[Server] Memory (after model unload)');
792
+ }
793
+ console.info('[Server] Embedding model unloaded to free memory.');
794
+ return true;
795
+ } catch (err) {
796
+ console.warn(`[Server] Error unloading embedding model: ${err.message}`);
797
+ cachedEmbedderPromise = null;
798
+ return false;
799
+ }
800
+ };
801
+
802
+ embedder = lazyEmbedder;
803
+ unloadMainEmbedder = unloader;
804
+ const preloadEmbeddingModel = async () => {
805
+ if (config.preloadEmbeddingModel === false) return;
806
+ try {
807
+ console.info('[Server] Preloading embedding model (background)...');
808
+ await embedder(' ');
809
+ } catch (err) {
810
+ console.warn(`[Server] Embedding model preload failed: ${err.message}`);
811
+ }
812
+ };
813
+
835
814
  if (config.vectorStoreFormat === 'binary') {
836
815
  try {
837
816
  await cleanupStaleBinaryArtifacts(config.cacheDirectory, { logger: console });
@@ -840,37 +819,31 @@ async function initialize(workspaceDir) {
840
819
  }
841
820
  }
842
821
 
843
-
844
822
  cache = new EmbeddingsCache(config);
845
823
  console.info(`[Server] Cache directory: ${config.cacheDirectory}`);
846
-
847
-
848
- indexer = new CodebaseIndexer(embedder, cache, config, server);
849
- hybridSearch = new HybridSearch(embedder, cache, config);
850
- const cacheClearer = new ClearCacheFeature.CacheClearer(embedder, cache, config, indexer);
851
- const findSimilarCode = new FindSimilarCodeFeature.FindSimilarCode(embedder, cache, config);
852
- const annConfig = new AnnConfigFeature.AnnConfigTool(cache, config);
853
-
854
-
855
- features[0].instance = hybridSearch;
856
- features[1].instance = indexer;
857
- features[2].instance = cacheClearer;
858
- features[3].instance = findSimilarCode;
859
- features[4].instance = annConfig;
860
-
861
-
862
-
863
- const setWorkspaceInstance = new SetWorkspaceFeature.SetWorkspaceFeature(
864
- config,
865
- cache,
866
- indexer,
867
- getGlobalCacheDir
868
- );
869
- setWorkspaceFeatureInstance = setWorkspaceInstance;
870
- features[6].instance = setWorkspaceInstance;
871
- features[6].handler = SetWorkspaceFeature.createHandleToolCall(setWorkspaceInstance);
872
-
873
-
824
+
825
+ indexer = new CodebaseIndexer(embedder, cache, config, server);
826
+ hybridSearch = new HybridSearch(embedder, cache, config);
827
+ const cacheClearer = new ClearCacheFeature.CacheClearer(embedder, cache, config, indexer);
828
+ const findSimilarCode = new FindSimilarCodeFeature.FindSimilarCode(embedder, cache, config);
829
+ const annConfig = new AnnConfigFeature.AnnConfigTool(cache, config);
830
+
831
+ features[0].instance = hybridSearch;
832
+ features[1].instance = indexer;
833
+ features[2].instance = cacheClearer;
834
+ features[3].instance = findSimilarCode;
835
+ features[4].instance = annConfig;
836
+
837
+ const setWorkspaceInstance = new SetWorkspaceFeature.SetWorkspaceFeature(
838
+ config,
839
+ cache,
840
+ indexer,
841
+ getGlobalCacheDir
842
+ );
843
+ setWorkspaceFeatureInstance = setWorkspaceInstance;
844
+ features[6].instance = setWorkspaceInstance;
845
+ features[6].handler = SetWorkspaceFeature.createHandleToolCall(setWorkspaceInstance);
846
+
874
847
  server.hybridSearch = hybridSearch;
875
848
 
876
849
  const startBackgroundTasks = async () => {
@@ -897,7 +870,10 @@ async function initialize(workspaceDir) {
897
870
  }
898
871
  return true;
899
872
  };
900
- const tryAutoAttachWorkspaceCache = async (reason, { canReindex = workspaceLockAcquired } = {}) => {
873
+ const tryAutoAttachWorkspaceCache = async (
874
+ reason,
875
+ { canReindex = workspaceLockAcquired } = {}
876
+ ) => {
901
877
  const candidate = await findAutoAttachWorkspaceCandidate({
902
878
  excludeCacheDirectory: config.cacheDirectory,
903
879
  });
@@ -985,44 +961,39 @@ async function initialize(workspaceDir) {
985
961
  } finally {
986
962
  stopStartupMemoryLogger();
987
963
  }
988
-
989
-
990
- console.info('[Server] Starting background indexing (delayed)...');
991
-
992
-
993
- setTimeout(() => {
994
- indexer
995
- .indexAll()
996
- .then(() => {
997
-
998
- if (config.watchFiles) {
999
- indexer.setupFileWatcher();
1000
- }
1001
- })
1002
- .catch((err) => {
1003
- console.error('[Server] Background indexing error:', err.message);
1004
- });
1005
- }, BACKGROUND_INDEX_DELAY_MS);
1006
- };
1007
-
1008
- return { startBackgroundTasks, config };
1009
- }
1010
-
1011
-
1012
- const server = new Server(
1013
- {
1014
- name: 'heuristic-mcp',
1015
- version: packageJson.version,
1016
- },
1017
- {
1018
- capabilities: {
1019
- tools: {},
1020
- resources: {},
1021
- },
1022
- }
1023
- );
1024
-
1025
-
964
+
965
+ console.info('[Server] Starting background indexing (delayed)...');
966
+
967
+ setTimeout(() => {
968
+ indexer
969
+ .indexAll()
970
+ .then(() => {
971
+ if (config.watchFiles) {
972
+ indexer.setupFileWatcher();
973
+ }
974
+ })
975
+ .catch((err) => {
976
+ console.error('[Server] Background indexing error:', err.message);
977
+ });
978
+ }, BACKGROUND_INDEX_DELAY_MS);
979
+ };
980
+
981
+ return { startBackgroundTasks, config };
982
+ }
983
+
984
+ const server = new Server(
985
+ {
986
+ name: 'heuristic-mcp',
987
+ version: packageJson.version,
988
+ },
989
+ {
990
+ capabilities: {
991
+ tools: {},
992
+ resources: {},
993
+ },
994
+ }
995
+ );
996
+
1026
997
  server.setNotificationHandler(RootsListChangedNotificationSchema, async () => {
1027
998
  console.info('[Server] Received roots/list_changed notification from client.');
1028
999
  const newRoot = await detectWorkspaceFromRoots();
@@ -1033,9 +1004,7 @@ server.setNotificationHandler(RootsListChangedNotificationSchema, async () => {
1033
1004
  });
1034
1005
  }
1035
1006
  });
1036
-
1037
-
1038
-
1007
+
1039
1008
  server.setRequestHandler(ListResourcesRequestSchema, async () => {
1040
1009
  await configReadyPromise;
1041
1010
  if (configInitError || !config) {
@@ -1043,9 +1012,7 @@ server.setRequestHandler(ListResourcesRequestSchema, async () => {
1043
1012
  }
1044
1013
  return await handleListResources(config);
1045
1014
  });
1046
-
1047
-
1048
-
1015
+
1049
1016
  server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
1050
1017
  await configReadyPromise;
1051
1018
  if (configInitError || !config) {
@@ -1053,26 +1020,22 @@ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
1053
1020
  }
1054
1021
  return await handleReadResource(request.params.uri, config);
1055
1022
  });
1056
-
1057
-
1058
-
1023
+
1059
1024
  server.setRequestHandler(ListToolsRequestSchema, async () => {
1060
1025
  await configReadyPromise;
1061
1026
  if (configInitError || !config) {
1062
1027
  throw configInitError ?? new Error('Server configuration is not initialized');
1063
1028
  }
1064
1029
  const tools = [];
1065
-
1066
- for (const feature of features) {
1067
- const toolDef = feature.module.getToolDefinition(config);
1068
- tools.push(toolDef);
1069
- }
1070
-
1071
- return { tools };
1072
- });
1073
-
1074
-
1075
-
1030
+
1031
+ for (const feature of features) {
1032
+ const toolDef = feature.module.getToolDefinition(config);
1033
+ tools.push(toolDef);
1034
+ }
1035
+
1036
+ return { tools };
1037
+ });
1038
+
1076
1039
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
1077
1040
  await configReadyPromise;
1078
1041
  if (configInitError || !config) {
@@ -1218,17 +1181,17 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1218
1181
  }
1219
1182
 
1220
1183
  for (const feature of features) {
1221
- const toolDef = feature.module.getToolDefinition(config);
1222
-
1184
+ const toolDef = feature.module.getToolDefinition(config);
1185
+
1223
1186
  if (request.params.name === toolDef.name) {
1224
-
1225
-
1226
1187
  if (typeof feature.handler !== 'function') {
1227
1188
  return {
1228
- content: [{
1229
- type: 'text',
1230
- text: `Tool "${toolDef.name}" is not ready. Server may still be initializing.`,
1231
- }],
1189
+ content: [
1190
+ {
1191
+ type: 'text',
1192
+ text: `Tool "${toolDef.name}" is not ready. Server may still be initializing.`,
1193
+ },
1194
+ ],
1232
1195
  isError: true,
1233
1196
  };
1234
1197
  }
@@ -1251,61 +1214,53 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1251
1214
  if (toolDef.name === 'f_set_workspace' && !isToolResponseError(result)) {
1252
1215
  trustWorkspacePath(config.searchDirectory);
1253
1216
  }
1254
-
1255
-
1256
-
1257
-
1258
-
1259
- const searchTools = ['a_semantic_search', 'd_find_similar_code'];
1260
- if (config.unloadModelAfterSearch && searchTools.includes(toolDef.name)) {
1261
-
1262
-
1263
- setImmediate(async () => {
1264
- if (typeof unloadMainEmbedder === 'function') {
1265
- await unloadMainEmbedder();
1266
- }
1267
- });
1268
- }
1269
-
1270
- return result;
1271
- }
1272
- }
1273
-
1274
- return {
1275
- content: [
1276
- {
1277
- type: 'text',
1278
- text: `Unknown tool: ${request.params.name}`,
1279
- },
1280
- ],
1281
- isError: true,
1282
- };
1283
- });
1284
-
1285
-
1286
-
1217
+
1218
+ const searchTools = ['a_semantic_search', 'd_find_similar_code'];
1219
+ if (config.unloadModelAfterSearch && searchTools.includes(toolDef.name)) {
1220
+ setImmediate(async () => {
1221
+ if (typeof unloadMainEmbedder === 'function') {
1222
+ await unloadMainEmbedder();
1223
+ }
1224
+ });
1225
+ }
1226
+
1227
+ return result;
1228
+ }
1229
+ }
1230
+
1231
+ return {
1232
+ content: [
1233
+ {
1234
+ type: 'text',
1235
+ text: `Unknown tool: ${request.params.name}`,
1236
+ },
1237
+ ],
1238
+ isError: true,
1239
+ };
1240
+ });
1241
+
1287
1242
  export async function main(argv = process.argv) {
1288
- const parsed = parseArgs(argv);
1289
- const {
1290
- isServerMode,
1291
- workspaceDir,
1292
- wantsVersion,
1293
- wantsHelp,
1294
- wantsLogs,
1295
- wantsMem,
1296
- wantsNoFollow,
1297
- tailLines,
1298
- wantsStop,
1299
- wantsStart,
1300
- wantsCache,
1301
- wantsClean,
1302
- wantsStatus,
1303
- wantsClearCache,
1304
- startFilter,
1305
- wantsFix,
1306
- unknownFlags,
1307
- } = parsed;
1308
-
1243
+ const parsed = parseArgs(argv);
1244
+ const {
1245
+ isServerMode,
1246
+ workspaceDir,
1247
+ wantsVersion,
1248
+ wantsHelp,
1249
+ wantsLogs,
1250
+ wantsMem,
1251
+ wantsNoFollow,
1252
+ tailLines,
1253
+ wantsStop,
1254
+ wantsStart,
1255
+ wantsCache,
1256
+ wantsClean,
1257
+ wantsStatus,
1258
+ wantsClearCache,
1259
+ startFilter,
1260
+ wantsFix,
1261
+ unknownFlags,
1262
+ } = parsed;
1263
+
1309
1264
  let shutdownRequested = false;
1310
1265
  let shutdownReason = 'natural';
1311
1266
  const requestShutdown = (reason) => {
@@ -1324,146 +1279,130 @@ export async function main(argv = process.argv) {
1324
1279
  if (isServerMode && !(process.env.VITEST === 'true' || process.env.NODE_ENV === 'test')) {
1325
1280
  enableStderrOnlyLogging();
1326
1281
  }
1327
- if (wantsVersion) {
1328
- console.info(packageJson.version);
1329
- process.exit(0);
1330
- }
1331
-
1332
- if (wantsHelp) {
1333
- printHelp();
1334
- process.exit(0);
1335
- }
1336
-
1337
- if (workspaceDir) {
1338
- console.info(`[Server] Workspace mode: ${workspaceDir}`);
1339
- }
1340
-
1341
-
1342
- if (wantsStop) {
1343
- await stop();
1344
- process.exit(0);
1345
- }
1346
-
1347
- if (wantsStart) {
1348
- await start(startFilter);
1349
- process.exit(0);
1350
- }
1351
-
1352
- if (wantsStatus) {
1353
- await status({ fix: wantsFix, workspaceDir });
1354
- process.exit(0);
1355
- }
1356
-
1357
-
1358
-
1359
- if (wantsCache) {
1360
- await status({ fix: wantsClean, cacheOnly: true, workspaceDir });
1361
- process.exit(0);
1362
- }
1363
-
1364
-
1365
-
1366
- const clearIndex = parsed.rawArgs.indexOf('--clear');
1367
- if (clearIndex !== -1) {
1368
- const cacheId = parsed.rawArgs[clearIndex + 1];
1369
- if (cacheId && !cacheId.startsWith('--')) {
1370
-
1371
-
1372
-
1373
-
1374
- let cacheHome;
1375
- if (process.platform === 'win32') {
1376
- cacheHome = process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local');
1377
- } else if (process.platform === 'darwin') {
1378
- cacheHome = path.join(os.homedir(), 'Library', 'Caches');
1379
- } else {
1380
- cacheHome = process.env.XDG_CACHE_HOME || path.join(os.homedir(), '.cache');
1381
- }
1382
- const globalCacheRoot = path.join(cacheHome, 'heuristic-mcp');
1383
- const trimmedId = String(cacheId).trim();
1384
- const hasSeparators = trimmedId.includes('/') || trimmedId.includes('\\');
1385
- const resolvedCachePath = path.resolve(globalCacheRoot, trimmedId);
1386
- const relPath = path.relative(globalCacheRoot, resolvedCachePath);
1387
- const isWithinRoot = relPath && !relPath.startsWith('..') && !path.isAbsolute(relPath);
1388
-
1389
- if (!trimmedId || hasSeparators || !isWithinRoot) {
1390
- console.error(`[Cache] Invalid cache id: ${cacheId}`);
1391
- console.error('[Cache] Cache id must be a direct child of the cache root.');
1392
- process.exit(1);
1393
- }
1394
-
1395
- const cachePath = resolvedCachePath;
1396
-
1397
- try {
1398
- await fs.access(cachePath);
1399
- console.info(`[Cache] Removing cache: ${cacheId}`);
1400
- console.info(`[Cache] Path: ${cachePath}`);
1401
- await fs.rm(cachePath, { recursive: true, force: true });
1402
- console.info(`[Cache] Successfully removed cache ${cacheId}`);
1403
- } catch (error) {
1404
- if (error.code === 'ENOENT') {
1405
- console.error(`[Cache] ❌ Cache not found: ${cacheId}`);
1406
- console.error(`[Cache] Available caches in ${globalCacheRoot}:`);
1407
- const dirs = await fs.readdir(globalCacheRoot).catch(() => []);
1408
- dirs.forEach(dir => console.error(` - ${dir}`));
1409
- process.exit(1);
1410
- } else {
1411
- console.error(`[Cache] ❌ Failed to remove cache: ${error.message}`);
1412
- process.exit(1);
1413
- }
1414
- }
1415
- process.exit(0);
1416
- }
1417
-
1418
-
1419
- }
1420
-
1421
- if (wantsClearCache) {
1422
- await clearCache(workspaceDir);
1423
- process.exit(0);
1424
- }
1425
-
1426
- if (wantsLogs) {
1427
- process.env.SMART_CODING_LOGS = 'true';
1428
- process.env.SMART_CODING_VERBOSE = 'true';
1429
- await logs({
1430
- workspaceDir,
1431
- tailLines,
1432
- follow: !wantsNoFollow,
1433
- });
1434
- process.exit(0);
1435
- }
1436
-
1437
- if (wantsMem) {
1438
- const ok = await printMemorySnapshot(workspaceDir);
1439
- process.exit(ok ? 0 : 1);
1440
- }
1441
-
1442
- if (unknownFlags.length > 0) {
1443
- console.error(`[Error] Unknown option(s): ${unknownFlags.join(', ')}`);
1444
- printHelp();
1445
- process.exit(1);
1446
- }
1447
-
1448
- if (wantsFix && !wantsStatus) {
1449
- console.error('[Error] --fix can only be used with --status (deprecated, use --cache --clean)');
1450
- printHelp();
1451
- process.exit(1);
1452
- }
1453
-
1454
- if (wantsClean && !wantsCache) {
1455
- console.error('[Error] --clean can only be used with --cache');
1456
- printHelp();
1457
- process.exit(1);
1458
- }
1459
-
1460
- registerSignalHandlers(requestShutdown);
1461
-
1462
-
1463
-
1464
-
1465
-
1466
-
1282
+ if (wantsVersion) {
1283
+ console.info(packageJson.version);
1284
+ process.exit(0);
1285
+ }
1286
+
1287
+ if (wantsHelp) {
1288
+ printHelp();
1289
+ process.exit(0);
1290
+ }
1291
+
1292
+ if (workspaceDir) {
1293
+ console.info(`[Server] Workspace mode: ${workspaceDir}`);
1294
+ }
1295
+
1296
+ if (wantsStop) {
1297
+ await stop();
1298
+ process.exit(0);
1299
+ }
1300
+
1301
+ if (wantsStart) {
1302
+ await start(startFilter);
1303
+ process.exit(0);
1304
+ }
1305
+
1306
+ if (wantsStatus) {
1307
+ await status({ fix: wantsFix, workspaceDir });
1308
+ process.exit(0);
1309
+ }
1310
+
1311
+ if (wantsCache) {
1312
+ await status({ fix: wantsClean, cacheOnly: true, workspaceDir });
1313
+ process.exit(0);
1314
+ }
1315
+
1316
+ const clearIndex = parsed.rawArgs.indexOf('--clear');
1317
+ if (clearIndex !== -1) {
1318
+ const cacheId = parsed.rawArgs[clearIndex + 1];
1319
+ if (cacheId && !cacheId.startsWith('--')) {
1320
+ let cacheHome;
1321
+ if (process.platform === 'win32') {
1322
+ cacheHome = process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local');
1323
+ } else if (process.platform === 'darwin') {
1324
+ cacheHome = path.join(os.homedir(), 'Library', 'Caches');
1325
+ } else {
1326
+ cacheHome = process.env.XDG_CACHE_HOME || path.join(os.homedir(), '.cache');
1327
+ }
1328
+ const globalCacheRoot = path.join(cacheHome, 'heuristic-mcp');
1329
+ const trimmedId = String(cacheId).trim();
1330
+ const hasSeparators = trimmedId.includes('/') || trimmedId.includes('\\');
1331
+ const resolvedCachePath = path.resolve(globalCacheRoot, trimmedId);
1332
+ const relPath = path.relative(globalCacheRoot, resolvedCachePath);
1333
+ const isWithinRoot = relPath && !relPath.startsWith('..') && !path.isAbsolute(relPath);
1334
+
1335
+ if (!trimmedId || hasSeparators || !isWithinRoot) {
1336
+ console.error(`[Cache] ❌ Invalid cache id: ${cacheId}`);
1337
+ console.error('[Cache] Cache id must be a direct child of the cache root.');
1338
+ process.exit(1);
1339
+ }
1340
+
1341
+ const cachePath = resolvedCachePath;
1342
+
1343
+ try {
1344
+ await fs.access(cachePath);
1345
+ console.info(`[Cache] Removing cache: ${cacheId}`);
1346
+ console.info(`[Cache] Path: ${cachePath}`);
1347
+ await fs.rm(cachePath, { recursive: true, force: true });
1348
+ console.info(`[Cache] ✅ Successfully removed cache ${cacheId}`);
1349
+ } catch (error) {
1350
+ if (error.code === 'ENOENT') {
1351
+ console.error(`[Cache] ❌ Cache not found: ${cacheId}`);
1352
+ console.error(`[Cache] Available caches in ${globalCacheRoot}:`);
1353
+ const dirs = await fs.readdir(globalCacheRoot).catch(() => []);
1354
+ dirs.forEach((dir) => console.error(` - ${dir}`));
1355
+ process.exit(1);
1356
+ } else {
1357
+ console.error(`[Cache] Failed to remove cache: ${error.message}`);
1358
+ process.exit(1);
1359
+ }
1360
+ }
1361
+ process.exit(0);
1362
+ }
1363
+ }
1364
+
1365
+ if (wantsClearCache) {
1366
+ await clearCache(workspaceDir);
1367
+ process.exit(0);
1368
+ }
1369
+
1370
+ if (wantsLogs) {
1371
+ process.env.SMART_CODING_LOGS = 'true';
1372
+ process.env.SMART_CODING_VERBOSE = 'true';
1373
+ await logs({
1374
+ workspaceDir,
1375
+ tailLines,
1376
+ follow: !wantsNoFollow,
1377
+ });
1378
+ process.exit(0);
1379
+ }
1380
+
1381
+ if (wantsMem) {
1382
+ const ok = await printMemorySnapshot(workspaceDir);
1383
+ process.exit(ok ? 0 : 1);
1384
+ }
1385
+
1386
+ if (unknownFlags.length > 0) {
1387
+ console.error(`[Error] Unknown option(s): ${unknownFlags.join(', ')}`);
1388
+ printHelp();
1389
+ process.exit(1);
1390
+ }
1391
+
1392
+ if (wantsFix && !wantsStatus) {
1393
+ console.error('[Error] --fix can only be used with --status (deprecated, use --cache --clean)');
1394
+ printHelp();
1395
+ process.exit(1);
1396
+ }
1397
+
1398
+ if (wantsClean && !wantsCache) {
1399
+ console.error('[Error] --clean can only be used with --cache');
1400
+ printHelp();
1401
+ process.exit(1);
1402
+ }
1403
+
1404
+ registerSignalHandlers(requestShutdown);
1405
+
1467
1406
  const detectedRootPromise = new Promise((resolve) => {
1468
1407
  const HANDSHAKE_TIMEOUT_MS = 1000;
1469
1408
  let settled = false;
@@ -1474,7 +1413,9 @@ export async function main(argv = process.argv) {
1474
1413
  };
1475
1414
 
1476
1415
  const timer = setTimeout(() => {
1477
- console.warn(`[Server] MCP handshake timed out after ${HANDSHAKE_TIMEOUT_MS}ms, proceeding without roots.`);
1416
+ console.warn(
1417
+ `[Server] MCP handshake timed out after ${HANDSHAKE_TIMEOUT_MS}ms, proceeding without roots.`
1418
+ );
1478
1419
  resolveOnce(null);
1479
1420
  }, HANDSHAKE_TIMEOUT_MS);
1480
1421
 
@@ -1499,11 +1440,8 @@ export async function main(argv = process.argv) {
1499
1440
  });
1500
1441
  }
1501
1442
 
1502
-
1503
-
1504
1443
  const detectedRoot = await detectedRootPromise;
1505
-
1506
-
1444
+
1507
1445
  const effectiveWorkspace = detectedRoot || workspaceDir;
1508
1446
  if (detectedRoot) {
1509
1447
  console.info(`[Server] Using workspace from MCP roots: ${detectedRoot}`);
@@ -1520,50 +1458,49 @@ export async function main(argv = process.argv) {
1520
1458
  throw err;
1521
1459
  });
1522
1460
  const { startBackgroundTasks } = await initWithResolve;
1523
-
1524
- console.info('[Server] Heuristic MCP server started.');
1525
-
1526
-
1527
-
1528
- void startBackgroundTasks().catch((err) => {
1529
- console.error(`[Server] Background task error: ${err.message}`);
1530
- });
1531
- console.info('[Server] MCP server is now fully ready to accept requests.');
1532
- }
1533
-
1534
-
1535
-
1461
+
1462
+ console.info('[Server] Heuristic MCP server started.');
1463
+
1464
+ void startBackgroundTasks().catch((err) => {
1465
+ console.error(`[Server] Background task error: ${err.message}`);
1466
+ });
1467
+ // Keep-Alive mechanism: ensure the process stays alive even if StdioServerTransport
1468
+ // temporarily loses its active handle status or during complex async chains.
1469
+ if (isServerMode) {
1470
+ setInterval(() => {
1471
+ // Logic to keep event loop active.
1472
+ // We don't need to do anything, just the presence of the timer is enough.
1473
+ }, SERVER_KEEP_ALIVE_INTERVAL_MS);
1474
+ }
1475
+
1476
+ console.info('[Server] MCP server is now fully ready to accept requests.');
1477
+ }
1478
+
1536
1479
  async function gracefulShutdown(signal) {
1537
1480
  console.info(`[Server] Received ${signal}, shutting down gracefully...`);
1538
1481
  const exitCode = isCrashShutdownReason(signal) ? 1 : 0;
1539
-
1540
- const cleanupTasks = [];
1541
-
1542
-
1543
-
1544
- if (indexer && indexer.watcher) {
1545
- cleanupTasks.push(
1546
- indexer.watcher
1547
- .close()
1548
- .then(() => console.info('[Server] File watcher stopped'))
1549
- .catch(() => console.warn('[Server] Error closing watcher'))
1550
- );
1551
- }
1552
-
1553
-
1554
-
1555
- if (indexer && indexer.terminateWorkers) {
1556
- cleanupTasks.push(
1557
- (async () => {
1558
- console.info('[Server] Terminating workers...');
1559
- await indexer.terminateWorkers();
1560
- console.info('[Server] Workers terminated');
1561
- })().catch(() => console.info('[Server] Workers shutdown (with warnings)'))
1562
- );
1563
- }
1564
-
1565
-
1566
-
1482
+
1483
+ const cleanupTasks = [];
1484
+
1485
+ if (indexer && indexer.watcher) {
1486
+ cleanupTasks.push(
1487
+ indexer.watcher
1488
+ .close()
1489
+ .then(() => console.info('[Server] File watcher stopped'))
1490
+ .catch(() => console.warn('[Server] Error closing watcher'))
1491
+ );
1492
+ }
1493
+
1494
+ if (indexer && indexer.terminateWorkers) {
1495
+ cleanupTasks.push(
1496
+ (async () => {
1497
+ console.info('[Server] Terminating workers...');
1498
+ await indexer.terminateWorkers();
1499
+ console.info('[Server] Workers terminated');
1500
+ })().catch(() => console.info('[Server] Workers shutdown (with warnings)'))
1501
+ );
1502
+ }
1503
+
1567
1504
  if (cache) {
1568
1505
  if (!workspaceLockAcquired) {
1569
1506
  console.info('[Server] Secondary/fallback mode: skipping cache save.');
@@ -1576,24 +1513,22 @@ async function gracefulShutdown(signal) {
1576
1513
  );
1577
1514
  }
1578
1515
  }
1579
-
1516
+
1580
1517
  await Promise.allSettled(cleanupTasks);
1581
1518
  console.info('[Server] Goodbye!');
1582
1519
  await flushLogs({ close: true, timeoutMs: 1500 }).catch(() => {});
1583
1520
 
1584
-
1585
-
1586
1521
  setTimeout(() => process.exit(exitCode), 100);
1587
1522
  }
1588
-
1589
- const isMain =
1590
- process.argv[1] &&
1591
- (path.resolve(process.argv[1]).toLowerCase() === fileURLToPath(import.meta.url).toLowerCase() ||
1592
- process.argv[1].endsWith('heuristic-mcp') ||
1593
- process.argv[1].endsWith('heuristic-mcp.js') ||
1594
- path.basename(process.argv[1]) === 'index.js') &&
1595
- !(process.env.VITEST === 'true' || process.env.NODE_ENV === 'test');
1596
-
1523
+
1524
+ const isMain =
1525
+ process.argv[1] &&
1526
+ (path.resolve(process.argv[1]).toLowerCase() === fileURLToPath(import.meta.url).toLowerCase() ||
1527
+ process.argv[1].endsWith('heuristic-mcp') ||
1528
+ process.argv[1].endsWith('heuristic-mcp.js') ||
1529
+ path.basename(process.argv[1]) === 'index.js') &&
1530
+ !(process.env.VITEST === 'true' || process.env.NODE_ENV === 'test');
1531
+
1597
1532
  if (isMain) {
1598
1533
  main().catch(async (err) => {
1599
1534
  console.error(err);