@softerist/heuristic-mcp 3.0.17 → 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/config.jsonc +23 -6
- package/features/ann-config.js +7 -14
- package/features/clear-cache.js +3 -3
- package/features/find-similar-code.js +17 -22
- package/features/hybrid-search.js +59 -67
- package/features/index-codebase.js +305 -268
- package/features/lifecycle.js +370 -176
- package/features/package-version.js +15 -26
- package/features/register.js +75 -57
- package/features/resources.js +21 -47
- package/features/set-workspace.js +31 -43
- package/index.js +912 -200
- package/lib/cache-utils.js +95 -99
- package/lib/cache.js +121 -166
- package/lib/cli.js +246 -238
- package/lib/config.js +232 -62
- package/lib/constants.js +22 -2
- package/lib/embed-query-process.js +13 -29
- package/lib/embedding-process.js +29 -19
- package/lib/embedding-worker.js +166 -149
- package/lib/ignore-patterns.js +39 -39
- package/lib/json-writer.js +7 -34
- package/lib/logging.js +52 -48
- package/lib/onnx-backend.js +4 -4
- package/lib/path-utils.js +4 -21
- package/lib/project-detector.js +3 -3
- package/lib/server-lifecycle.js +148 -35
- package/lib/settings-editor.js +25 -18
- package/lib/slice-normalize.js +6 -16
- package/lib/tokenizer.js +56 -109
- package/lib/utils.js +62 -81
- package/lib/vector-store-binary.js +7 -7
- package/lib/vector-store-sqlite.js +35 -67
- package/lib/workspace-cache-key.js +36 -0
- package/lib/workspace-env.js +55 -14
- package/package.json +86 -86
package/lib/cache-utils.js
CHANGED
|
@@ -1,17 +1,15 @@
|
|
|
1
|
-
import fs from 'fs/promises';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import os from 'os';
|
|
4
|
-
import { getGlobalCacheDir } from './config.js';
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
* Check if a process is running
|
|
8
|
-
*/
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import { getGlobalCacheDir, isNonProjectDirectory } from './config.js';
|
|
5
|
+
|
|
6
|
+
|
|
9
7
|
function isProcessRunning(pid) {
|
|
10
8
|
try {
|
|
11
9
|
process.kill(pid, 0);
|
|
12
10
|
return true;
|
|
13
11
|
} catch (err) {
|
|
14
|
-
|
|
12
|
+
|
|
15
13
|
if (err && err.code === 'EPERM') {
|
|
16
14
|
return true;
|
|
17
15
|
}
|
|
@@ -19,42 +17,35 @@ function isProcessRunning(pid) {
|
|
|
19
17
|
}
|
|
20
18
|
}
|
|
21
19
|
|
|
22
|
-
|
|
23
|
-
* Normalize workspace path for comparison
|
|
24
|
-
* - Resolves symlinks
|
|
25
|
-
* - Lowercase on Windows
|
|
26
|
-
* - Normalizes path separators
|
|
27
|
-
*/
|
|
20
|
+
|
|
28
21
|
async function normalizeWorkspacePath(workspacePath) {
|
|
29
22
|
if (!workspacePath) return null;
|
|
30
23
|
|
|
31
24
|
try {
|
|
32
25
|
let normalized = workspacePath;
|
|
33
26
|
|
|
34
|
-
|
|
27
|
+
|
|
35
28
|
try {
|
|
36
29
|
normalized = await fs.realpath(normalized);
|
|
37
30
|
} catch {
|
|
38
|
-
|
|
31
|
+
|
|
39
32
|
}
|
|
40
33
|
|
|
41
|
-
|
|
34
|
+
|
|
42
35
|
if (process.platform === 'win32') {
|
|
43
36
|
normalized = normalized.toLowerCase();
|
|
44
37
|
}
|
|
45
38
|
|
|
46
|
-
|
|
39
|
+
|
|
47
40
|
normalized = path.normalize(normalized);
|
|
48
41
|
|
|
49
42
|
return normalized;
|
|
50
43
|
} catch {
|
|
51
|
-
return workspacePath;
|
|
44
|
+
return workspacePath;
|
|
52
45
|
}
|
|
53
46
|
}
|
|
54
47
|
|
|
55
|
-
|
|
56
|
-
* Check if workspace path indicates a temporary/test workspace
|
|
57
|
-
*/
|
|
48
|
+
|
|
58
49
|
function isTemporaryWorkspace(workspacePath) {
|
|
59
50
|
if (!workspacePath) return false;
|
|
60
51
|
|
|
@@ -65,7 +56,7 @@ function isTemporaryWorkspace(workspacePath) {
|
|
|
65
56
|
return true;
|
|
66
57
|
}
|
|
67
58
|
|
|
68
|
-
|
|
59
|
+
|
|
69
60
|
try {
|
|
70
61
|
const tempDir = os.tmpdir().toLowerCase();
|
|
71
62
|
return normalized.startsWith(tempDir);
|
|
@@ -74,9 +65,7 @@ function isTemporaryWorkspace(workspacePath) {
|
|
|
74
65
|
}
|
|
75
66
|
}
|
|
76
67
|
|
|
77
|
-
|
|
78
|
-
* Get timestamp from progress data
|
|
79
|
-
*/
|
|
68
|
+
|
|
80
69
|
function getProgressTimestamp(cacheInfo) {
|
|
81
70
|
if (cacheInfo.progress?.updatedAt) {
|
|
82
71
|
const timestamp = Date.parse(cacheInfo.progress.updatedAt);
|
|
@@ -85,7 +74,7 @@ function getProgressTimestamp(cacheInfo) {
|
|
|
85
74
|
}
|
|
86
75
|
}
|
|
87
76
|
|
|
88
|
-
|
|
77
|
+
|
|
89
78
|
if (cacheInfo.stats.progressFile) {
|
|
90
79
|
return cacheInfo.stats.progressFile.mtimeMs;
|
|
91
80
|
}
|
|
@@ -93,9 +82,7 @@ function getProgressTimestamp(cacheInfo) {
|
|
|
93
82
|
return 0;
|
|
94
83
|
}
|
|
95
84
|
|
|
96
|
-
|
|
97
|
-
* Check if progress was updated recently
|
|
98
|
-
*/
|
|
85
|
+
|
|
99
86
|
function hasRecentProgress(cacheInfo, thresholdMs = 5 * 60 * 1000) {
|
|
100
87
|
if (!cacheInfo.progress) return false;
|
|
101
88
|
|
|
@@ -103,9 +90,7 @@ function hasRecentProgress(cacheInfo, thresholdMs = 5 * 60 * 1000) {
|
|
|
103
90
|
return Date.now() - progressTime < thresholdMs;
|
|
104
91
|
}
|
|
105
92
|
|
|
106
|
-
|
|
107
|
-
* Safe file stat - returns null on error
|
|
108
|
-
*/
|
|
93
|
+
|
|
109
94
|
async function safeStat(filePath) {
|
|
110
95
|
try {
|
|
111
96
|
return await fs.stat(filePath);
|
|
@@ -114,9 +99,7 @@ async function safeStat(filePath) {
|
|
|
114
99
|
}
|
|
115
100
|
}
|
|
116
101
|
|
|
117
|
-
|
|
118
|
-
* Safe JSON parse - returns null on error
|
|
119
|
-
*/
|
|
102
|
+
|
|
120
103
|
async function safeReadJson(filePath) {
|
|
121
104
|
try {
|
|
122
105
|
const content = await fs.readFile(filePath, 'utf-8');
|
|
@@ -126,14 +109,12 @@ async function safeReadJson(filePath) {
|
|
|
126
109
|
}
|
|
127
110
|
}
|
|
128
111
|
|
|
129
|
-
|
|
130
|
-
* Collect comprehensive cache information
|
|
131
|
-
*/
|
|
112
|
+
|
|
132
113
|
async function collectCacheInfo(cacheDir) {
|
|
133
114
|
const cacheId = path.basename(cacheDir);
|
|
134
115
|
const errors = [];
|
|
135
116
|
|
|
136
|
-
|
|
117
|
+
|
|
137
118
|
const metaPath = path.join(cacheDir, 'meta.json');
|
|
138
119
|
const progressPath = path.join(cacheDir, 'progress.json');
|
|
139
120
|
const lockPath = path.join(cacheDir, 'server.lock.json');
|
|
@@ -142,14 +123,14 @@ async function collectCacheInfo(cacheDir) {
|
|
|
142
123
|
const vectorsSqlitePath = path.join(cacheDir, 'vectors.sqlite');
|
|
143
124
|
const annIndexPath = path.join(cacheDir, 'ann-index.bin');
|
|
144
125
|
|
|
145
|
-
|
|
126
|
+
|
|
146
127
|
const [meta, progress, lock] = await Promise.all([
|
|
147
128
|
safeReadJson(metaPath),
|
|
148
129
|
safeReadJson(progressPath),
|
|
149
130
|
safeReadJson(lockPath),
|
|
150
131
|
]);
|
|
151
132
|
|
|
152
|
-
|
|
133
|
+
|
|
153
134
|
const [
|
|
154
135
|
cacheDirStat,
|
|
155
136
|
metaFileStat,
|
|
@@ -170,13 +151,13 @@ async function collectCacheInfo(cacheDir) {
|
|
|
170
151
|
safeStat(annIndexPath),
|
|
171
152
|
]);
|
|
172
153
|
|
|
173
|
-
|
|
154
|
+
|
|
174
155
|
const embeddingsFileStat = vectorsBinStat || vectorsSqliteStat || embeddingsJsonStat;
|
|
175
156
|
|
|
176
|
-
|
|
157
|
+
|
|
177
158
|
const workspacePath = meta?.workspace || null;
|
|
178
159
|
|
|
179
|
-
|
|
160
|
+
|
|
180
161
|
let workspaceExists = false;
|
|
181
162
|
if (workspacePath) {
|
|
182
163
|
try {
|
|
@@ -187,10 +168,10 @@ async function collectCacheInfo(cacheDir) {
|
|
|
187
168
|
}
|
|
188
169
|
}
|
|
189
170
|
|
|
190
|
-
|
|
171
|
+
|
|
191
172
|
const workspacePathNormalized = await normalizeWorkspacePath(workspacePath);
|
|
192
173
|
|
|
193
|
-
|
|
174
|
+
|
|
194
175
|
const timestamps = [
|
|
195
176
|
meta?.lastSaveTime ? Date.parse(meta.lastSaveTime) : null,
|
|
196
177
|
getProgressTimestamp({ progress, stats: { progressFile: progressFileStat } }),
|
|
@@ -204,10 +185,10 @@ async function collectCacheInfo(cacheDir) {
|
|
|
204
185
|
? Math.max(...timestamps)
|
|
205
186
|
: cacheDirStat?.mtimeMs || 0;
|
|
206
187
|
|
|
207
|
-
|
|
188
|
+
|
|
208
189
|
let isActive = false;
|
|
209
190
|
|
|
210
|
-
|
|
191
|
+
|
|
211
192
|
if (lock && Number.isInteger(lock.pid)) {
|
|
212
193
|
isActive = isProcessRunning(lock.pid);
|
|
213
194
|
}
|
|
@@ -235,28 +216,49 @@ async function collectCacheInfo(cacheDir) {
|
|
|
235
216
|
};
|
|
236
217
|
}
|
|
237
218
|
|
|
238
|
-
/**
|
|
239
|
-
* Evaluate a single cache using the safe decision pipeline
|
|
240
|
-
*/
|
|
241
|
-
function evaluateCache(cacheInfo, thresholds) {
|
|
242
|
-
const now = Date.now();
|
|
243
|
-
const age = now - cacheInfo.lastActivityMs;
|
|
244
|
-
|
|
245
|
-
// Step 1: Keep if active or within safety window
|
|
246
|
-
if (cacheInfo.isActive || age < thresholds.safetyWindowMs) {
|
|
247
|
-
return {
|
|
248
|
-
action: 'KEEP',
|
|
249
|
-
reason: cacheInfo.isActive ? 'active_lock' : 'recent_activity',
|
|
250
|
-
details: {
|
|
251
|
-
isActive: cacheInfo.isActive,
|
|
252
|
-
lockPid: cacheInfo.lock?.pid,
|
|
253
|
-
ageMs: age,
|
|
254
|
-
},
|
|
255
|
-
};
|
|
256
|
-
}
|
|
257
219
|
|
|
258
|
-
|
|
259
|
-
|
|
220
|
+
function evaluateCache(cacheInfo, thresholds) {
|
|
221
|
+
const now = Date.now();
|
|
222
|
+
const age = now - cacheInfo.lastActivityMs;
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
if (cacheInfo.isActive) {
|
|
226
|
+
return {
|
|
227
|
+
action: 'KEEP',
|
|
228
|
+
reason: 'active_lock',
|
|
229
|
+
details: {
|
|
230
|
+
isActive: true,
|
|
231
|
+
lockPid: cacheInfo.lock?.pid,
|
|
232
|
+
ageMs: age,
|
|
233
|
+
},
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
if (cacheInfo.workspacePath && isNonProjectDirectory(cacheInfo.workspacePath)) {
|
|
239
|
+
return {
|
|
240
|
+
action: 'REMOVE',
|
|
241
|
+
reason: 'system_workspace_cache',
|
|
242
|
+
details: {
|
|
243
|
+
workspace: cacheInfo.workspacePath,
|
|
244
|
+
ageMs: age,
|
|
245
|
+
},
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
if (age < thresholds.safetyWindowMs) {
|
|
251
|
+
return {
|
|
252
|
+
action: 'KEEP',
|
|
253
|
+
reason: 'recent_activity',
|
|
254
|
+
details: {
|
|
255
|
+
ageMs: age,
|
|
256
|
+
},
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
if (!cacheInfo.meta) {
|
|
260
262
|
const dirAge = now - (cacheInfo.stats.cacheDir?.mtimeMs || 0);
|
|
261
263
|
if (dirAge > thresholds.staleNoMetaMs && !hasRecentProgress(cacheInfo)) {
|
|
262
264
|
return {
|
|
@@ -272,8 +274,8 @@ function evaluateCache(cacheInfo, thresholds) {
|
|
|
272
274
|
};
|
|
273
275
|
}
|
|
274
276
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
+
|
|
278
|
+
|
|
277
279
|
if (isTemporaryWorkspace(cacheInfo.workspacePath)) {
|
|
278
280
|
if (age > thresholds.tempThresholdMs) {
|
|
279
281
|
return {
|
|
@@ -285,7 +287,7 @@ function evaluateCache(cacheInfo, thresholds) {
|
|
|
285
287
|
},
|
|
286
288
|
};
|
|
287
289
|
}
|
|
288
|
-
|
|
290
|
+
|
|
289
291
|
return {
|
|
290
292
|
action: 'KEEP',
|
|
291
293
|
reason: 'recent_temp_workspace',
|
|
@@ -296,7 +298,7 @@ function evaluateCache(cacheInfo, thresholds) {
|
|
|
296
298
|
};
|
|
297
299
|
}
|
|
298
300
|
|
|
299
|
-
|
|
301
|
+
|
|
300
302
|
const filesIndexed = cacheInfo.meta.filesIndexed ?? 0;
|
|
301
303
|
const chunksStored = cacheInfo.meta.chunksStored ?? 0;
|
|
302
304
|
|
|
@@ -323,7 +325,7 @@ function evaluateCache(cacheInfo, thresholds) {
|
|
|
323
325
|
};
|
|
324
326
|
}
|
|
325
327
|
|
|
326
|
-
|
|
328
|
+
|
|
327
329
|
if (cacheInfo.workspacePath && !cacheInfo.workspaceExists) {
|
|
328
330
|
if (age > thresholds.workspaceGraceMs) {
|
|
329
331
|
return {
|
|
@@ -345,7 +347,7 @@ function evaluateCache(cacheInfo, thresholds) {
|
|
|
345
347
|
};
|
|
346
348
|
}
|
|
347
349
|
|
|
348
|
-
|
|
350
|
+
|
|
349
351
|
if (cacheInfo.progress && !hasRecentProgress(cacheInfo, thresholds.safetyWindowMs)) {
|
|
350
352
|
const progressAge = now - getProgressTimestamp(cacheInfo);
|
|
351
353
|
if (progressAge > thresholds.staleProgressMs) {
|
|
@@ -360,7 +362,7 @@ function evaluateCache(cacheInfo, thresholds) {
|
|
|
360
362
|
}
|
|
361
363
|
}
|
|
362
364
|
|
|
363
|
-
|
|
365
|
+
|
|
364
366
|
if (age > thresholds.maxUnusedMs) {
|
|
365
367
|
return {
|
|
366
368
|
action: 'REMOVE',
|
|
@@ -369,7 +371,7 @@ function evaluateCache(cacheInfo, thresholds) {
|
|
|
369
371
|
};
|
|
370
372
|
}
|
|
371
373
|
|
|
372
|
-
|
|
374
|
+
|
|
373
375
|
return {
|
|
374
376
|
action: 'KEEP',
|
|
375
377
|
reason: 'valid_cache',
|
|
@@ -381,16 +383,14 @@ function evaluateCache(cacheInfo, thresholds) {
|
|
|
381
383
|
};
|
|
382
384
|
}
|
|
383
385
|
|
|
384
|
-
|
|
385
|
-
* Find duplicate workspace caches
|
|
386
|
-
*/
|
|
386
|
+
|
|
387
387
|
function findDuplicateWorkspaces(cacheInfos) {
|
|
388
|
-
const workspaceMap = new Map();
|
|
388
|
+
const workspaceMap = new Map();
|
|
389
389
|
|
|
390
390
|
for (const info of cacheInfos) {
|
|
391
391
|
if (!info.workspacePathNormalized) continue;
|
|
392
392
|
|
|
393
|
-
|
|
393
|
+
|
|
394
394
|
const dimLabel = info.meta?.embeddingDimension ?? 'default';
|
|
395
395
|
const key = `${info.workspacePathNormalized}::${info.meta?.embeddingModel || 'default'}::${dimLabel}`;
|
|
396
396
|
|
|
@@ -403,10 +403,10 @@ function findDuplicateWorkspaces(cacheInfos) {
|
|
|
403
403
|
const duplicates = [];
|
|
404
404
|
for (const [key, infos] of workspaceMap) {
|
|
405
405
|
if (infos.length > 1) {
|
|
406
|
-
|
|
406
|
+
|
|
407
407
|
infos.sort((a, b) => b.lastActivityMs - a.lastActivityMs);
|
|
408
408
|
|
|
409
|
-
|
|
409
|
+
|
|
410
410
|
for (let i = 1; i < infos.length; i++) {
|
|
411
411
|
if (!infos[i].isActive) {
|
|
412
412
|
duplicates.push({
|
|
@@ -427,9 +427,7 @@ function findDuplicateWorkspaces(cacheInfos) {
|
|
|
427
427
|
return duplicates;
|
|
428
428
|
}
|
|
429
429
|
|
|
430
|
-
|
|
431
|
-
* Main cache cleanup function with intelligent sanitization
|
|
432
|
-
*/
|
|
430
|
+
|
|
433
431
|
export async function clearStaleCaches(options = {}) {
|
|
434
432
|
const config = {
|
|
435
433
|
staleNoMetaHours: 6,
|
|
@@ -445,7 +443,7 @@ export async function clearStaleCaches(options = {}) {
|
|
|
445
443
|
...options,
|
|
446
444
|
};
|
|
447
445
|
|
|
448
|
-
|
|
446
|
+
|
|
449
447
|
const thresholds = {
|
|
450
448
|
staleNoMetaMs: config.staleNoMetaHours * 60 * 60 * 1000,
|
|
451
449
|
emptyThresholdMs: config.emptyThresholdHours * 60 * 60 * 1000,
|
|
@@ -463,12 +461,12 @@ export async function clearStaleCaches(options = {}) {
|
|
|
463
461
|
return { removed: 0, kept: 0, dryRun: config.dryRun, decisions: [] };
|
|
464
462
|
}
|
|
465
463
|
|
|
466
|
-
|
|
464
|
+
|
|
467
465
|
const cacheInfos = await Promise.all(
|
|
468
466
|
cacheDirs.map((dir) => collectCacheInfo(path.join(globalCacheRoot, dir)))
|
|
469
467
|
);
|
|
470
468
|
|
|
471
|
-
|
|
469
|
+
|
|
472
470
|
const decisions = cacheInfos.map((info) => {
|
|
473
471
|
const evaluation = evaluateCache(info, thresholds);
|
|
474
472
|
return {
|
|
@@ -479,11 +477,11 @@ export async function clearStaleCaches(options = {}) {
|
|
|
479
477
|
};
|
|
480
478
|
});
|
|
481
479
|
|
|
482
|
-
|
|
480
|
+
|
|
483
481
|
if (config.removeDuplicates) {
|
|
484
482
|
const duplicates = findDuplicateWorkspaces(cacheInfos);
|
|
485
483
|
for (const dup of duplicates) {
|
|
486
|
-
|
|
484
|
+
|
|
487
485
|
const existing = decisions.find((d) => d.cacheId === dup.info.cacheId);
|
|
488
486
|
if (existing && existing.action === 'KEEP') {
|
|
489
487
|
existing.action = dup.action;
|
|
@@ -493,7 +491,7 @@ export async function clearStaleCaches(options = {}) {
|
|
|
493
491
|
}
|
|
494
492
|
}
|
|
495
493
|
|
|
496
|
-
|
|
494
|
+
|
|
497
495
|
let removed = 0;
|
|
498
496
|
let kept = 0;
|
|
499
497
|
|
|
@@ -514,7 +512,7 @@ export async function clearStaleCaches(options = {}) {
|
|
|
514
512
|
`[Cache] Failed to remove ${decision.cacheId}: ${err.message}`
|
|
515
513
|
);
|
|
516
514
|
}
|
|
517
|
-
|
|
515
|
+
|
|
518
516
|
kept++;
|
|
519
517
|
decision.action = 'KEEP';
|
|
520
518
|
decision.reason = 'removal_failed';
|
|
@@ -547,9 +545,7 @@ export async function clearStaleCaches(options = {}) {
|
|
|
547
545
|
};
|
|
548
546
|
}
|
|
549
547
|
|
|
550
|
-
|
|
551
|
-
* Format age in human-readable form
|
|
552
|
-
*/
|
|
548
|
+
|
|
553
549
|
function formatAge(ms) {
|
|
554
550
|
if (!Number.isFinite(ms)) return 'unknown';
|
|
555
551
|
|