@softerist/heuristic-mcp 3.1.0 → 3.2.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.
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.0",
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",