@softerist/heuristic-mcp 3.1.0 → 3.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/config.jsonc CHANGED
@@ -628,7 +628,7 @@
628
628
  "**/.smart-coding-cache/**",
629
629
  ],
630
630
  // Indexing controls.
631
- "indexing": {
631
+ "indexing": {
632
632
  // Enable project-type detection + smart ignore patterns.
633
633
  "smartIndexing": true,
634
634
  // Lines per chunk.
@@ -645,18 +645,10 @@
645
645
  "watchFiles": true,
646
646
  // Save incremental index checkpoints every 2s so interrupted runs can resume with minimal rework.
647
647
  // Increase to 5000-10000 on slower disks if checkpoint writes feel too frequent.
648
- "indexCheckpointIntervalMs": 2000,
649
- },
650
- // Server lifecycle controls.
651
- // When true, a newly started server instance will terminate older heuristic-mcp server processes.
652
- // This helps prevent stale workspace binding after IDE reloads/window switches.
653
- "autoStopOtherServersOnStartup": true,
654
- // Safety guard for IDE sessions:
655
- // when true, semantic/index tools require a current trusted workspace signal
656
- // (MCP roots or explicit workspace env), otherwise the call fails fast.
657
- "requireTrustedWorkspaceSignalForTools": true,
658
- // Logging and diagnostics.
659
- "logging": {
648
+ "indexCheckpointIntervalMs": 2000,
649
+ },
650
+ // Logging and diagnostics.
651
+ "logging": {
660
652
  // Enable verbose logging.
661
653
  "verbose": true,
662
654
  },
@@ -694,10 +686,7 @@
694
686
  "worker": {
695
687
  // Number of embedding workers (0 disables).
696
688
  // Windows + heavy Jina models are more stable with child-process embedding than worker pools.
697
- "workerThreads": 1,
698
- // Safety guard for heavy models on Windows.
699
- // Keep true (recommended). Set false only to opt in to experimental heavy-model workers on Windows.
700
- "workerDisableHeavyModelOnWindows": false,
689
+ "workerThreads": 0,
701
690
  // Worker batch timeout in milliseconds.
702
691
  "workerBatchTimeoutMs": 120000,
703
692
  // Failures before worker circuit opens.
package/index.js CHANGED
@@ -37,11 +37,11 @@ const require = createRequire(import.meta.url);
37
37
  const packageJson = require('./package.json');
38
38
 
39
39
  import { loadConfig, getGlobalCacheDir, isNonProjectDirectory } from './lib/config.js';
40
- import { clearStaleCaches } from './lib/cache-utils.js';
41
- import { enableStderrOnlyLogging, setupFileLogging, getLogFilePath } from './lib/logging.js';
42
- import { parseArgs, printHelp } from './lib/cli.js';
43
- import { clearCache } from './lib/cache-ops.js';
44
- import { logMemory, startMemoryLogger } from './lib/memory-logger.js';
40
+ import { clearStaleCaches } from './lib/cache-utils.js';
41
+ import { enableStderrOnlyLogging, setupFileLogging, getLogFilePath, flushLogs } from './lib/logging.js';
42
+ import { parseArgs, printHelp } from './lib/cli.js';
43
+ import { clearCache } from './lib/cache-ops.js';
44
+ import { logMemory, startMemoryLogger } from './lib/memory-logger.js';
45
45
  import {
46
46
  registerSignalHandlers,
47
47
  setupPidFile,
@@ -181,6 +181,59 @@ function isToolResponseError(result) {
181
181
  entry.text.trim().toLowerCase().startsWith('error:')
182
182
  );
183
183
  }
184
+
185
+ function formatCrashDetail(detail) {
186
+ if (detail instanceof Error) {
187
+ return detail.stack || detail.message || String(detail);
188
+ }
189
+ if (typeof detail === 'string') {
190
+ return detail;
191
+ }
192
+ try {
193
+ return JSON.stringify(detail);
194
+ } catch {
195
+ return String(detail);
196
+ }
197
+ }
198
+
199
+ function isCrashShutdownReason(reason) {
200
+ const normalized = String(reason || '').toLowerCase();
201
+ return normalized.includes('uncaughtexception') || normalized.includes('unhandledrejection');
202
+ }
203
+
204
+ function registerProcessDiagnostics({ isServerMode, requestShutdown, getShutdownReason }) {
205
+ if (!isServerMode) return;
206
+
207
+ process.on('beforeExit', (code) => {
208
+ const reason = getShutdownReason() || 'natural';
209
+ console.info(`[Server] Process beforeExit (code=${code}, reason=${reason}).`);
210
+ });
211
+
212
+ process.on('exit', (code) => {
213
+ const reason = getShutdownReason() || 'natural';
214
+ console.info(`[Server] Process exit (code=${code}, reason=${reason}).`);
215
+ });
216
+
217
+ let fatalHandled = false;
218
+ const handleFatalError = (reason, detail) => {
219
+ if (fatalHandled) return;
220
+ fatalHandled = true;
221
+ console.error(`[Server] Fatal ${reason}: ${formatCrashDetail(detail)}`);
222
+ requestShutdown(reason);
223
+ const forceExitTimer = setTimeout(() => {
224
+ console.error(`[Server] Forced exit after fatal ${reason}.`);
225
+ process.exit(1);
226
+ }, 5000);
227
+ forceExitTimer.unref?.();
228
+ };
229
+
230
+ process.on('uncaughtException', (err) => {
231
+ handleFatalError('uncaughtException', err);
232
+ });
233
+ process.on('unhandledRejection', (reason) => {
234
+ handleFatalError('unhandledRejection', reason);
235
+ });
236
+ }
184
237
 
185
238
  async function resolveWorkspaceFromEnvValue(rawValue) {
186
239
  if (!rawValue || rawValue.includes('${')) return null;
@@ -1158,7 +1211,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1158
1211
 
1159
1212
 
1160
1213
 
1161
- export async function main(argv = process.argv) {
1214
+ export async function main(argv = process.argv) {
1162
1215
  const parsed = parseArgs(argv);
1163
1216
  const {
1164
1217
  isServerMode,
@@ -1180,17 +1233,24 @@ export async function main(argv = process.argv) {
1180
1233
  unknownFlags,
1181
1234
  } = parsed;
1182
1235
 
1183
- let shutdownRequested = false;
1184
- const requestShutdown = (reason) => {
1185
- if (shutdownRequested) return;
1186
- shutdownRequested = true;
1187
- console.info(`[Server] Shutdown requested (${reason}).`);
1188
- void gracefulShutdown(reason);
1189
- };
1190
-
1191
- if (isServerMode && !(process.env.VITEST === 'true' || process.env.NODE_ENV === 'test')) {
1192
- enableStderrOnlyLogging();
1193
- }
1236
+ let shutdownRequested = false;
1237
+ let shutdownReason = 'natural';
1238
+ const requestShutdown = (reason) => {
1239
+ if (shutdownRequested) return;
1240
+ shutdownRequested = true;
1241
+ shutdownReason = String(reason || 'unknown');
1242
+ console.info(`[Server] Shutdown requested (${reason}).`);
1243
+ void gracefulShutdown(reason);
1244
+ };
1245
+ registerProcessDiagnostics({
1246
+ isServerMode,
1247
+ requestShutdown,
1248
+ getShutdownReason: () => shutdownReason,
1249
+ });
1250
+
1251
+ if (isServerMode && !(process.env.VITEST === 'true' || process.env.NODE_ENV === 'test')) {
1252
+ enableStderrOnlyLogging();
1253
+ }
1194
1254
  if (wantsVersion) {
1195
1255
  console.info(packageJson.version);
1196
1256
  process.exit(0);
@@ -1400,8 +1460,9 @@ export async function main(argv = process.argv) {
1400
1460
 
1401
1461
 
1402
1462
 
1403
- async function gracefulShutdown(signal) {
1404
- console.info(`[Server] Received ${signal}, shutting down gracefully...`);
1463
+ async function gracefulShutdown(signal) {
1464
+ console.info(`[Server] Received ${signal}, shutting down gracefully...`);
1465
+ const exitCode = isCrashShutdownReason(signal) ? 1 : 0;
1405
1466
 
1406
1467
  const cleanupTasks = [];
1407
1468
 
@@ -1443,13 +1504,14 @@ async function gracefulShutdown(signal) {
1443
1504
  }
1444
1505
  }
1445
1506
 
1446
- await Promise.allSettled(cleanupTasks);
1447
- console.info('[Server] Goodbye!');
1448
-
1449
-
1450
-
1451
- setTimeout(() => process.exit(0), 100);
1452
- }
1507
+ await Promise.allSettled(cleanupTasks);
1508
+ console.info('[Server] Goodbye!');
1509
+ await flushLogs({ close: true, timeoutMs: 1500 }).catch(() => {});
1510
+
1511
+
1512
+
1513
+ setTimeout(() => process.exit(exitCode), 100);
1514
+ }
1453
1515
 
1454
1516
  const isMain =
1455
1517
  process.argv[1] &&
@@ -1459,6 +1521,10 @@ const isMain =
1459
1521
  path.basename(process.argv[1]) === 'index.js') &&
1460
1522
  !(process.env.VITEST === 'true' || process.env.NODE_ENV === 'test');
1461
1523
 
1462
- if (isMain) {
1463
- main().catch(console.error);
1464
- }
1524
+ if (isMain) {
1525
+ main().catch(async (err) => {
1526
+ console.error(err);
1527
+ await flushLogs({ close: true, timeoutMs: 500 }).catch(() => {});
1528
+ process.exit(1);
1529
+ });
1530
+ }
package/lib/logging.js CHANGED
@@ -24,7 +24,7 @@ export function enableStderrOnlyLogging() {
24
24
  console.info = redirect;
25
25
  }
26
26
 
27
- export async function setupFileLogging(config) {
27
+ export async function setupFileLogging(config) {
28
28
  if (process.env.VITEST === 'true' || process.env.NODE_ENV === 'test') {
29
29
  return null;
30
30
  }
@@ -68,16 +68,51 @@ export async function setupFileLogging(config) {
68
68
  originalConsole.error(`[Logs] Failed to write log file: ${err.message}`);
69
69
  });
70
70
 
71
- process.on('exit', () => {
72
- if (logStream) logStream.end();
73
- });
71
+ process.on('exit', () => {
72
+ if (logStream) logStream.end();
73
+ });
74
74
 
75
75
  return logPath;
76
76
  } catch (err) {
77
77
  originalConsole.error(`[Logs] Failed to initialize log file: ${err.message}`);
78
78
  return null;
79
- }
80
- }
79
+ }
80
+ }
81
+
82
+ export async function flushLogs({ close = true, timeoutMs = 1000 } = {}) {
83
+ if (!logStream) return;
84
+
85
+ const stream = logStream;
86
+ await new Promise((resolve) => {
87
+ let done = false;
88
+ const finish = () => {
89
+ if (done) return;
90
+ done = true;
91
+ if (close && logStream === stream) {
92
+ logStream = null;
93
+ }
94
+ resolve();
95
+ };
96
+
97
+ const timer = setTimeout(finish, timeoutMs);
98
+ timer.unref?.();
99
+
100
+ const onFinished = () => {
101
+ clearTimeout(timer);
102
+ finish();
103
+ };
104
+
105
+ try {
106
+ if (close) {
107
+ stream.end(onFinished);
108
+ } else {
109
+ stream.write('', onFinished);
110
+ }
111
+ } catch {
112
+ onFinished();
113
+ }
114
+ });
115
+ }
81
116
 
82
117
  export function getLogFilePath(config) {
83
118
  return path.join(config.cacheDirectory, 'logs', 'server.log');
@@ -78,7 +78,7 @@ function isProcessRunning(pid) {
78
78
  }
79
79
  }
80
80
 
81
- export async function acquireWorkspaceLock({ cacheDirectory, workspaceDir = null } = {}) {
81
+ export async function acquireWorkspaceLock({ cacheDirectory, workspaceDir = null } = {}) {
82
82
  if (!cacheDirectory || isTestEnv()) {
83
83
  return { acquired: true, lockPath: null };
84
84
  }
@@ -86,23 +86,38 @@ export async function acquireWorkspaceLock({ cacheDirectory, workspaceDir = null
86
86
  await fs.mkdir(cacheDirectory, { recursive: true });
87
87
  const lockPath = path.join(cacheDirectory, 'server.lock.json');
88
88
 
89
- const readLock = async () => {
89
+ const readLock = async () => {
90
90
  try {
91
91
  const raw = await fs.readFile(lockPath, 'utf-8');
92
92
  return JSON.parse(raw);
93
93
  } catch {
94
94
  return null;
95
95
  }
96
- };
97
-
98
- const existing = await readLock();
99
- if (existing && Number.isInteger(existing.pid) && isProcessRunning(existing.pid)) {
100
- return { acquired: false, lockPath, ownerPid: existing.pid, owner: existing };
101
- }
102
-
103
- if (existing) {
104
- await fs.unlink(lockPath).catch(() => {});
105
- }
96
+ };
97
+
98
+ const removeStaleLock = async (lockData) => {
99
+ const stalePid =
100
+ lockData && Number.isInteger(Number(lockData.pid)) ? Number(lockData.pid) : null;
101
+ const staleWorkspace =
102
+ typeof lockData?.workspace === 'string' && lockData.workspace.trim().length > 0
103
+ ? lockData.workspace
104
+ : null;
105
+ const stalePidLabel = stalePid ? `pid ${stalePid}` : 'unknown pid';
106
+ const staleWorkspaceLabel = staleWorkspace ? `, workspace "${staleWorkspace}"` : '';
107
+ console.warn(
108
+ `[Server] Detected stale workspace lock (${stalePidLabel}${staleWorkspaceLabel}) at ${lockPath}; removing stale lock and continuing startup.`
109
+ );
110
+ await fs.unlink(lockPath).catch(() => {});
111
+ };
112
+
113
+ const existing = await readLock();
114
+ if (existing && Number.isInteger(existing.pid) && isProcessRunning(existing.pid)) {
115
+ return { acquired: false, lockPath, ownerPid: existing.pid, owner: existing };
116
+ }
117
+
118
+ if (existing) {
119
+ await removeStaleLock(existing);
120
+ }
106
121
 
107
122
  const payload = {
108
123
  pid: process.pid,
@@ -119,14 +134,18 @@ export async function acquireWorkspaceLock({ cacheDirectory, workspaceDir = null
119
134
  await handle.close();
120
135
  }
121
136
  } catch (err) {
122
- if (err && err.code === 'EEXIST') {
123
- const current = await readLock();
124
- if (current && Number.isInteger(current.pid) && isProcessRunning(current.pid)) {
125
- return { acquired: false, lockPath, ownerPid: current.pid, owner: current };
126
- }
127
- await fs.unlink(lockPath).catch(() => {});
128
- return acquireWorkspaceLock({ cacheDirectory, workspaceDir });
129
- }
137
+ if (err && err.code === 'EEXIST') {
138
+ const current = await readLock();
139
+ if (current && Number.isInteger(current.pid) && isProcessRunning(current.pid)) {
140
+ return { acquired: false, lockPath, ownerPid: current.pid, owner: current };
141
+ }
142
+ if (current) {
143
+ await removeStaleLock(current);
144
+ } else {
145
+ await fs.unlink(lockPath).catch(() => {});
146
+ }
147
+ return acquireWorkspaceLock({ cacheDirectory, workspaceDir });
148
+ }
130
149
  throw err;
131
150
  }
132
151
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@softerist/heuristic-mcp",
3
- "version": "3.1.0",
3
+ "version": "3.2.1",
4
4
  "description": "An enhanced MCP server providing intelligent semantic code search with find-similar-code, recency ranking, and improved chunking. Fork of smart-coding-mcp.",
5
5
  "type": "module",
6
6
  "main": "index.js",