@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 +95 -29
- package/lib/logging.js +41 -6
- package/lib/server-lifecycle.js +39 -20
- package/package.json +1 -1
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
|
-
|
|
1185
|
-
|
|
1186
|
-
shutdownRequested
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
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
|
-
|
|
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(
|
|
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');
|
package/lib/server-lifecycle.js
CHANGED
|
@@ -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
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
128
|
-
|
|
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.
|
|
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",
|