@softerist/heuristic-mcp 3.0.15 → 3.0.16
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/README.md +104 -104
- package/config.jsonc +173 -173
- package/features/ann-config.js +131 -0
- package/features/clear-cache.js +84 -0
- package/features/find-similar-code.js +291 -0
- package/features/hybrid-search.js +544 -0
- package/features/index-codebase.js +3268 -0
- package/features/lifecycle.js +1189 -0
- package/features/package-version.js +302 -0
- package/features/register.js +408 -0
- package/features/resources.js +156 -0
- package/features/set-workspace.js +265 -0
- package/index.js +96 -96
- package/lib/cache-ops.js +22 -22
- package/lib/cache-utils.js +565 -565
- package/lib/cache.js +1870 -1870
- package/lib/call-graph.js +396 -396
- package/lib/cli.js +1 -1
- package/lib/config.js +517 -517
- package/lib/constants.js +39 -39
- package/lib/embed-query-process.js +7 -7
- package/lib/embedding-process.js +7 -7
- package/lib/embedding-worker.js +299 -299
- package/lib/ignore-patterns.js +316 -316
- package/lib/json-worker.js +14 -14
- package/lib/json-writer.js +337 -337
- package/lib/logging.js +164 -164
- package/lib/memory-logger.js +13 -13
- package/lib/onnx-backend.js +193 -193
- package/lib/project-detector.js +84 -84
- package/lib/server-lifecycle.js +165 -165
- package/lib/settings-editor.js +754 -754
- package/lib/tokenizer.js +256 -256
- package/lib/utils.js +428 -428
- package/lib/vector-store-binary.js +627 -627
- package/lib/vector-store-sqlite.js +95 -95
- package/lib/workspace-env.js +28 -28
- package/mcp_config.json +9 -9
- package/package.json +86 -75
- package/scripts/clear-cache.js +20 -0
- package/scripts/download-model.js +43 -0
- package/scripts/mcp-launcher.js +49 -0
- package/scripts/postinstall.js +12 -0
- package/search-configs.js +36 -36
- package/.prettierrc +0 -7
- package/debug-pids.js +0 -30
- package/eslint.config.js +0 -36
- package/specs/plan.md +0 -23
- package/vitest.config.js +0 -39
package/lib/server-lifecycle.js
CHANGED
|
@@ -1,165 +1,165 @@
|
|
|
1
|
-
import fs from 'fs/promises';
|
|
2
|
-
import fsSync from 'fs';
|
|
3
|
-
import path from 'path';
|
|
4
|
-
import os from 'os';
|
|
5
|
-
|
|
6
|
-
function isTestEnv() {
|
|
7
|
-
return process.env.VITEST === 'true' || process.env.NODE_ENV === 'test';
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
function getPidFilePath({ pidFileName, cacheDirectory }) {
|
|
11
|
-
if (cacheDirectory) {
|
|
12
|
-
return path.join(cacheDirectory, pidFileName);
|
|
13
|
-
}
|
|
14
|
-
return path.join(os.homedir(), pidFileName);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export async function setupPidFile({
|
|
18
|
-
pidFileName = '.heuristic-mcp.pid',
|
|
19
|
-
cacheDirectory = null,
|
|
20
|
-
} = {}) {
|
|
21
|
-
if (isTestEnv()) {
|
|
22
|
-
return null;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const pidPath = getPidFilePath({ pidFileName, cacheDirectory });
|
|
26
|
-
if (cacheDirectory) {
|
|
27
|
-
try {
|
|
28
|
-
await fs.mkdir(cacheDirectory, { recursive: true });
|
|
29
|
-
} catch {
|
|
30
|
-
// ignore mkdir errors and attempt to write anyway
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
// Clean up stale PID file if present
|
|
34
|
-
try {
|
|
35
|
-
const raw = await fs.readFile(pidPath, 'utf-8');
|
|
36
|
-
const existingPid = parseInt(String(raw).trim(), 10);
|
|
37
|
-
if (Number.isInteger(existingPid) && !isProcessRunning(existingPid)) {
|
|
38
|
-
await fs.unlink(pidPath).catch(() => {});
|
|
39
|
-
}
|
|
40
|
-
} catch {
|
|
41
|
-
// ignore missing/invalid pid file
|
|
42
|
-
}
|
|
43
|
-
try {
|
|
44
|
-
await fs.writeFile(pidPath, `${process.pid}`, 'utf-8');
|
|
45
|
-
} catch (err) {
|
|
46
|
-
console.error(`[Server] Warning: Failed to write PID file: ${err.message}`);
|
|
47
|
-
return null;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const cleanup = () => {
|
|
51
|
-
try {
|
|
52
|
-
fsSync.unlinkSync(pidPath);
|
|
53
|
-
} catch {
|
|
54
|
-
// ignore
|
|
55
|
-
}
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
process.on('exit', cleanup);
|
|
59
|
-
return pidPath;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export function registerSignalHandlers(handler) {
|
|
63
|
-
process.on('SIGINT', () => handler('SIGINT'));
|
|
64
|
-
process.on('SIGTERM', () => handler('SIGTERM'));
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function isProcessRunning(pid) {
|
|
68
|
-
try {
|
|
69
|
-
process.kill(pid, 0);
|
|
70
|
-
return true;
|
|
71
|
-
} catch (err) {
|
|
72
|
-
// On Windows, EPERM can happen even if process exists.
|
|
73
|
-
if (err && err.code === 'EPERM') {
|
|
74
|
-
return true;
|
|
75
|
-
}
|
|
76
|
-
return false;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
export async function acquireWorkspaceLock({ cacheDirectory, workspaceDir = null } = {}) {
|
|
81
|
-
if (!cacheDirectory || isTestEnv()) {
|
|
82
|
-
return { acquired: true, lockPath: null };
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
await fs.mkdir(cacheDirectory, { recursive: true });
|
|
86
|
-
const lockPath = path.join(cacheDirectory, 'server.lock.json');
|
|
87
|
-
|
|
88
|
-
const readLock = async () => {
|
|
89
|
-
try {
|
|
90
|
-
const raw = await fs.readFile(lockPath, 'utf-8');
|
|
91
|
-
return JSON.parse(raw);
|
|
92
|
-
} catch {
|
|
93
|
-
return null;
|
|
94
|
-
}
|
|
95
|
-
};
|
|
96
|
-
|
|
97
|
-
const existing = await readLock();
|
|
98
|
-
if (existing && Number.isInteger(existing.pid) && isProcessRunning(existing.pid)) {
|
|
99
|
-
return { acquired: false, lockPath, ownerPid: existing.pid, owner: existing };
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
if (existing) {
|
|
103
|
-
await fs.unlink(lockPath).catch(() => {});
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const payload = {
|
|
107
|
-
pid: process.pid,
|
|
108
|
-
startedAt: new Date().toISOString(),
|
|
109
|
-
workspace: workspaceDir || null,
|
|
110
|
-
argv: process.argv.join(' '),
|
|
111
|
-
};
|
|
112
|
-
|
|
113
|
-
try {
|
|
114
|
-
const handle = await fs.open(lockPath, 'wx');
|
|
115
|
-
try {
|
|
116
|
-
await handle.writeFile(JSON.stringify(payload), 'utf-8');
|
|
117
|
-
} finally {
|
|
118
|
-
await handle.close();
|
|
119
|
-
}
|
|
120
|
-
} catch (err) {
|
|
121
|
-
if (err && err.code === 'EEXIST') {
|
|
122
|
-
const current = await readLock();
|
|
123
|
-
if (current && Number.isInteger(current.pid) && isProcessRunning(current.pid)) {
|
|
124
|
-
return { acquired: false, lockPath, ownerPid: current.pid, owner: current };
|
|
125
|
-
}
|
|
126
|
-
await fs.unlink(lockPath).catch(() => {});
|
|
127
|
-
return acquireWorkspaceLock({ cacheDirectory, workspaceDir });
|
|
128
|
-
}
|
|
129
|
-
throw err;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
const cleanup = () => {
|
|
133
|
-
try {
|
|
134
|
-
const raw = fsSync.readFileSync(lockPath, 'utf-8');
|
|
135
|
-
const current = JSON.parse(raw);
|
|
136
|
-
if (current && current.pid === process.pid) {
|
|
137
|
-
fsSync.unlinkSync(lockPath);
|
|
138
|
-
}
|
|
139
|
-
} catch {
|
|
140
|
-
// ignore cleanup errors
|
|
141
|
-
}
|
|
142
|
-
};
|
|
143
|
-
|
|
144
|
-
process.on('exit', cleanup);
|
|
145
|
-
process.on('SIGINT', cleanup);
|
|
146
|
-
process.on('SIGTERM', cleanup);
|
|
147
|
-
|
|
148
|
-
return { acquired: true, lockPath };
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
export async function releaseWorkspaceLock({ cacheDirectory } = {}) {
|
|
152
|
-
if (!cacheDirectory || isTestEnv()) {
|
|
153
|
-
return;
|
|
154
|
-
}
|
|
155
|
-
const lockPath = path.join(cacheDirectory, 'server.lock.json');
|
|
156
|
-
try {
|
|
157
|
-
const raw = await fs.readFile(lockPath, 'utf-8');
|
|
158
|
-
const current = JSON.parse(raw);
|
|
159
|
-
if (current && current.pid === process.pid) {
|
|
160
|
-
await fs.unlink(lockPath).catch(() => {});
|
|
161
|
-
}
|
|
162
|
-
} catch {
|
|
163
|
-
// ignore release errors
|
|
164
|
-
}
|
|
165
|
-
}
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import fsSync from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
|
|
6
|
+
function isTestEnv() {
|
|
7
|
+
return process.env.VITEST === 'true' || process.env.NODE_ENV === 'test';
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function getPidFilePath({ pidFileName, cacheDirectory }) {
|
|
11
|
+
if (cacheDirectory) {
|
|
12
|
+
return path.join(cacheDirectory, pidFileName);
|
|
13
|
+
}
|
|
14
|
+
return path.join(os.homedir(), pidFileName);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function setupPidFile({
|
|
18
|
+
pidFileName = '.heuristic-mcp.pid',
|
|
19
|
+
cacheDirectory = null,
|
|
20
|
+
} = {}) {
|
|
21
|
+
if (isTestEnv()) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const pidPath = getPidFilePath({ pidFileName, cacheDirectory });
|
|
26
|
+
if (cacheDirectory) {
|
|
27
|
+
try {
|
|
28
|
+
await fs.mkdir(cacheDirectory, { recursive: true });
|
|
29
|
+
} catch {
|
|
30
|
+
// ignore mkdir errors and attempt to write anyway
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
// Clean up stale PID file if present
|
|
34
|
+
try {
|
|
35
|
+
const raw = await fs.readFile(pidPath, 'utf-8');
|
|
36
|
+
const existingPid = parseInt(String(raw).trim(), 10);
|
|
37
|
+
if (Number.isInteger(existingPid) && !isProcessRunning(existingPid)) {
|
|
38
|
+
await fs.unlink(pidPath).catch(() => {});
|
|
39
|
+
}
|
|
40
|
+
} catch {
|
|
41
|
+
// ignore missing/invalid pid file
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
await fs.writeFile(pidPath, `${process.pid}`, 'utf-8');
|
|
45
|
+
} catch (err) {
|
|
46
|
+
console.error(`[Server] Warning: Failed to write PID file: ${err.message}`);
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const cleanup = () => {
|
|
51
|
+
try {
|
|
52
|
+
fsSync.unlinkSync(pidPath);
|
|
53
|
+
} catch {
|
|
54
|
+
// ignore
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
process.on('exit', cleanup);
|
|
59
|
+
return pidPath;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function registerSignalHandlers(handler) {
|
|
63
|
+
process.on('SIGINT', () => handler('SIGINT'));
|
|
64
|
+
process.on('SIGTERM', () => handler('SIGTERM'));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function isProcessRunning(pid) {
|
|
68
|
+
try {
|
|
69
|
+
process.kill(pid, 0);
|
|
70
|
+
return true;
|
|
71
|
+
} catch (err) {
|
|
72
|
+
// On Windows, EPERM can happen even if process exists.
|
|
73
|
+
if (err && err.code === 'EPERM') {
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function acquireWorkspaceLock({ cacheDirectory, workspaceDir = null } = {}) {
|
|
81
|
+
if (!cacheDirectory || isTestEnv()) {
|
|
82
|
+
return { acquired: true, lockPath: null };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
await fs.mkdir(cacheDirectory, { recursive: true });
|
|
86
|
+
const lockPath = path.join(cacheDirectory, 'server.lock.json');
|
|
87
|
+
|
|
88
|
+
const readLock = async () => {
|
|
89
|
+
try {
|
|
90
|
+
const raw = await fs.readFile(lockPath, 'utf-8');
|
|
91
|
+
return JSON.parse(raw);
|
|
92
|
+
} catch {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const existing = await readLock();
|
|
98
|
+
if (existing && Number.isInteger(existing.pid) && isProcessRunning(existing.pid)) {
|
|
99
|
+
return { acquired: false, lockPath, ownerPid: existing.pid, owner: existing };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (existing) {
|
|
103
|
+
await fs.unlink(lockPath).catch(() => {});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const payload = {
|
|
107
|
+
pid: process.pid,
|
|
108
|
+
startedAt: new Date().toISOString(),
|
|
109
|
+
workspace: workspaceDir || null,
|
|
110
|
+
argv: process.argv.join(' '),
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
const handle = await fs.open(lockPath, 'wx');
|
|
115
|
+
try {
|
|
116
|
+
await handle.writeFile(JSON.stringify(payload), 'utf-8');
|
|
117
|
+
} finally {
|
|
118
|
+
await handle.close();
|
|
119
|
+
}
|
|
120
|
+
} catch (err) {
|
|
121
|
+
if (err && err.code === 'EEXIST') {
|
|
122
|
+
const current = await readLock();
|
|
123
|
+
if (current && Number.isInteger(current.pid) && isProcessRunning(current.pid)) {
|
|
124
|
+
return { acquired: false, lockPath, ownerPid: current.pid, owner: current };
|
|
125
|
+
}
|
|
126
|
+
await fs.unlink(lockPath).catch(() => {});
|
|
127
|
+
return acquireWorkspaceLock({ cacheDirectory, workspaceDir });
|
|
128
|
+
}
|
|
129
|
+
throw err;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const cleanup = () => {
|
|
133
|
+
try {
|
|
134
|
+
const raw = fsSync.readFileSync(lockPath, 'utf-8');
|
|
135
|
+
const current = JSON.parse(raw);
|
|
136
|
+
if (current && current.pid === process.pid) {
|
|
137
|
+
fsSync.unlinkSync(lockPath);
|
|
138
|
+
}
|
|
139
|
+
} catch {
|
|
140
|
+
// ignore cleanup errors
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
process.on('exit', cleanup);
|
|
145
|
+
process.on('SIGINT', cleanup);
|
|
146
|
+
process.on('SIGTERM', cleanup);
|
|
147
|
+
|
|
148
|
+
return { acquired: true, lockPath };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export async function releaseWorkspaceLock({ cacheDirectory } = {}) {
|
|
152
|
+
if (!cacheDirectory || isTestEnv()) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
const lockPath = path.join(cacheDirectory, 'server.lock.json');
|
|
156
|
+
try {
|
|
157
|
+
const raw = await fs.readFile(lockPath, 'utf-8');
|
|
158
|
+
const current = JSON.parse(raw);
|
|
159
|
+
if (current && current.pid === process.pid) {
|
|
160
|
+
await fs.unlink(lockPath).catch(() => {});
|
|
161
|
+
}
|
|
162
|
+
} catch {
|
|
163
|
+
// ignore release errors
|
|
164
|
+
}
|
|
165
|
+
}
|