@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 +6 -17
- package/index.js +95 -29
- package/lib/logging.js +41 -6
- package/lib/server-lifecycle.js +39 -20
- package/package.json +1 -1
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
|
-
//
|
|
651
|
-
|
|
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":
|
|
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
|
-
|
|
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.1
|
|
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",
|