@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
|
@@ -0,0 +1,1189 @@
|
|
|
1
|
+
import { exec } from 'child_process';
|
|
2
|
+
import util from 'util';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
import fs from 'fs/promises';
|
|
6
|
+
import fsSync from 'fs';
|
|
7
|
+
import { loadConfig } from '../lib/config.js';
|
|
8
|
+
import { getLogFilePath } from '../lib/logging.js';
|
|
9
|
+
import { clearStaleCaches } from '../lib/cache-utils.js';
|
|
10
|
+
import {
|
|
11
|
+
findMcpServerEntry,
|
|
12
|
+
parseJsonc,
|
|
13
|
+
setMcpServerDisabledInToml,
|
|
14
|
+
upsertMcpServerEntryInText,
|
|
15
|
+
} from '../lib/settings-editor.js';
|
|
16
|
+
|
|
17
|
+
const execPromise = util.promisify(exec);
|
|
18
|
+
const PID_FILE_NAME = '.heuristic-mcp.pid';
|
|
19
|
+
|
|
20
|
+
function getUserHomeDir() {
|
|
21
|
+
if (process.platform === 'win32' && process.env.USERPROFILE) {
|
|
22
|
+
return process.env.USERPROFILE;
|
|
23
|
+
}
|
|
24
|
+
return os.homedir();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function listPidFilePaths() {
|
|
28
|
+
const pidFiles = new Set();
|
|
29
|
+
pidFiles.add(path.join(getUserHomeDir(), PID_FILE_NAME));
|
|
30
|
+
const globalCacheRoot = path.join(getGlobalCacheDir(), 'heuristic-mcp');
|
|
31
|
+
let cacheDirs = [];
|
|
32
|
+
try {
|
|
33
|
+
cacheDirs = await fs.readdir(globalCacheRoot);
|
|
34
|
+
} catch {
|
|
35
|
+
cacheDirs = [];
|
|
36
|
+
}
|
|
37
|
+
if (!Array.isArray(cacheDirs)) {
|
|
38
|
+
cacheDirs = [];
|
|
39
|
+
}
|
|
40
|
+
for (const dir of cacheDirs) {
|
|
41
|
+
pidFiles.add(path.join(globalCacheRoot, dir, PID_FILE_NAME));
|
|
42
|
+
}
|
|
43
|
+
return Array.from(pidFiles);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function readPidFromFile(filePath) {
|
|
47
|
+
try {
|
|
48
|
+
const raw = await fs.readFile(filePath, 'utf-8');
|
|
49
|
+
const trimmed = String(raw || '').trim();
|
|
50
|
+
if (!trimmed) return null;
|
|
51
|
+
if (trimmed.startsWith('{')) {
|
|
52
|
+
try {
|
|
53
|
+
const parsed = JSON.parse(trimmed);
|
|
54
|
+
const pid = Number(parsed?.pid);
|
|
55
|
+
if (Number.isInteger(pid)) return pid;
|
|
56
|
+
} catch {
|
|
57
|
+
// fall through
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const pid = parseInt(trimmed, 10);
|
|
61
|
+
if (!Number.isNaN(pid)) return pid;
|
|
62
|
+
} catch {
|
|
63
|
+
// ignore missing/invalid pid file
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export async function stop() {
|
|
69
|
+
console.info('[Lifecycle] Stopping Heuristic MCP servers...');
|
|
70
|
+
try {
|
|
71
|
+
const platform = process.platform;
|
|
72
|
+
const currentPid = process.pid;
|
|
73
|
+
let pids = [];
|
|
74
|
+
const cmdByPid = new Map();
|
|
75
|
+
const manualPid = process.env.HEURISTIC_MCP_PID;
|
|
76
|
+
|
|
77
|
+
if (platform === 'win32') {
|
|
78
|
+
// 1. Try PID files first for reliability (per-workspace)
|
|
79
|
+
const pidFiles = await listPidFilePaths();
|
|
80
|
+
for (const pidFile of pidFiles) {
|
|
81
|
+
const pid = await readPidFromFile(pidFile);
|
|
82
|
+
if (!Number.isInteger(pid) || pid === currentPid) continue;
|
|
83
|
+
try {
|
|
84
|
+
process.kill(pid, 0);
|
|
85
|
+
const pidValue = String(pid);
|
|
86
|
+
if (!pids.includes(pidValue)) pids.push(pidValue);
|
|
87
|
+
} catch (e) {
|
|
88
|
+
// If we lack permission, still attempt to stop by PID.
|
|
89
|
+
if (e.code === 'EPERM') {
|
|
90
|
+
const pidValue = String(pid);
|
|
91
|
+
if (!pids.includes(pidValue)) pids.push(pidValue);
|
|
92
|
+
} else {
|
|
93
|
+
await fs.unlink(pidFile).catch(() => {});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// 2. Fallback to WMIC when CIM access is denied
|
|
99
|
+
if (pids.length === 0) {
|
|
100
|
+
try {
|
|
101
|
+
const { stdout } = await execPromise(
|
|
102
|
+
`wmic process where "CommandLine like '%heuristic-mcp%'" get ProcessId /FORMAT:LIST`
|
|
103
|
+
);
|
|
104
|
+
const matches = stdout.match(/ProcessId=(\d+)/g) || [];
|
|
105
|
+
for (const match of matches) {
|
|
106
|
+
const pid = match.replace('ProcessId=', '');
|
|
107
|
+
if (pid && !isNaN(pid) && parseInt(pid, 10) !== currentPid) {
|
|
108
|
+
if (!pids.includes(pid)) pids.push(pid);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
} catch (_wmicErr) {
|
|
112
|
+
// ignore secondary failures
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// 3. Fallback to process list with fuzzier matching (kill all heuristic-mcp instances)
|
|
117
|
+
try {
|
|
118
|
+
const { stdout } = await execPromise(
|
|
119
|
+
`powershell -NoProfile -Command "Get-CimInstance Win32_Process | Where-Object { $_.CommandLine -and ($_.CommandLine -like '*heuristic-mcp*' -or $_.CommandLine -like '*heuristic-mcp\\\\index.js*' -or $_.CommandLine -like '*heuristic-mcp/index.js*') } | Select-Object -ExpandProperty ProcessId"`
|
|
120
|
+
);
|
|
121
|
+
const listPids = stdout
|
|
122
|
+
.trim()
|
|
123
|
+
.split(/\s+/)
|
|
124
|
+
.filter((p) => p && !isNaN(p) && parseInt(p) !== currentPid);
|
|
125
|
+
|
|
126
|
+
// Retrieve command lines to filter out workers
|
|
127
|
+
if (listPids.length > 0) {
|
|
128
|
+
const { stdout: cmdOut } = await execPromise(
|
|
129
|
+
`powershell -NoProfile -Command "Get-CimInstance Win32_Process | Where-Object { $_.ProcessId -in @(${listPids.join(',')}) } | Select-Object ProcessId, CommandLine"`
|
|
130
|
+
);
|
|
131
|
+
const lines = cmdOut.trim().split(/\r?\n/);
|
|
132
|
+
for (const line of lines) {
|
|
133
|
+
const trimmed = line.trim();
|
|
134
|
+
if (!trimmed || trimmed.startsWith('ProcessId')) continue;
|
|
135
|
+
const match = trimmed.match(/^(\d+)\s+(.*)$/);
|
|
136
|
+
if (match) {
|
|
137
|
+
const pid = parseInt(match[1], 10);
|
|
138
|
+
const cmd = match[2];
|
|
139
|
+
if (
|
|
140
|
+
cmd.includes('embedding-worker') ||
|
|
141
|
+
cmd.includes('embedding-process') ||
|
|
142
|
+
cmd.includes('json-worker')
|
|
143
|
+
) {
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
if (pid && !pids.includes(String(pid))) {
|
|
147
|
+
pids.push(String(pid));
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
} catch (_e) {
|
|
153
|
+
/* ignore */
|
|
154
|
+
}
|
|
155
|
+
} else {
|
|
156
|
+
// Unix: Use pgrep to get all matching PIDs
|
|
157
|
+
try {
|
|
158
|
+
const { stdout } = await execPromise(`pgrep -fl "heuristic-mcp"`);
|
|
159
|
+
const lines = stdout.trim().split(/\r?\n/);
|
|
160
|
+
|
|
161
|
+
// Filter out current PID, dead processes, and workers
|
|
162
|
+
pids = [];
|
|
163
|
+
for (const line of lines) {
|
|
164
|
+
const tokens = line.trim().split(/\s+/).filter(Boolean);
|
|
165
|
+
if (tokens.length === 0) continue;
|
|
166
|
+
|
|
167
|
+
const allNumeric = tokens.every((token) => /^\d+$/.test(token));
|
|
168
|
+
const candidatePids = allNumeric ? tokens : [tokens[0]];
|
|
169
|
+
|
|
170
|
+
for (const candidate of candidatePids) {
|
|
171
|
+
const pid = parseInt(candidate, 10);
|
|
172
|
+
if (!Number.isFinite(pid) || pid === currentPid) continue;
|
|
173
|
+
|
|
174
|
+
// Exclude workers when command line is present
|
|
175
|
+
if (
|
|
176
|
+
!allNumeric &&
|
|
177
|
+
(line.includes('embedding-worker') ||
|
|
178
|
+
line.includes('embedding-process') ||
|
|
179
|
+
line.includes('json-worker'))
|
|
180
|
+
) {
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
try {
|
|
185
|
+
process.kill(pid, 0);
|
|
186
|
+
const pidValue = String(pid);
|
|
187
|
+
if (!pids.includes(pidValue)) {
|
|
188
|
+
pids.push(pidValue);
|
|
189
|
+
}
|
|
190
|
+
} catch (_e) {
|
|
191
|
+
/* ignore */
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
} catch (e) {
|
|
196
|
+
// pgrep returns code 1 if no processes found, which is fine
|
|
197
|
+
if (e.code === 1) pids = [];
|
|
198
|
+
else throw e;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Manual PID override (best-effort)
|
|
203
|
+
if (manualPid) {
|
|
204
|
+
const parts = String(manualPid)
|
|
205
|
+
.split(/[,\s]+/)
|
|
206
|
+
.map((part) => part.trim())
|
|
207
|
+
.filter(Boolean);
|
|
208
|
+
for (const part of parts) {
|
|
209
|
+
if (!isNaN(part)) {
|
|
210
|
+
const pidValue = String(parseInt(part, 10));
|
|
211
|
+
if (pidValue && !pids.includes(pidValue)) {
|
|
212
|
+
pids.push(pidValue);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (pids.length === 0) {
|
|
219
|
+
console.info('[Lifecycle] No running instances found (already stopped).');
|
|
220
|
+
await setMcpServerEnabled(false);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Capture command lines before killing (best-effort)
|
|
225
|
+
try {
|
|
226
|
+
if (platform === 'win32') {
|
|
227
|
+
const { stdout } = await execPromise(
|
|
228
|
+
`powershell -NoProfile -Command "Get-CimInstance Win32_Process | Where-Object { $_.ProcessId -in @(${pids.join(',')}) } | Select-Object ProcessId, CommandLine"`
|
|
229
|
+
);
|
|
230
|
+
const lines = stdout.trim().split(/\r?\n/);
|
|
231
|
+
for (const line of lines) {
|
|
232
|
+
const trimmed = line.trim();
|
|
233
|
+
if (!trimmed || trimmed.startsWith('ProcessId')) continue;
|
|
234
|
+
const match = trimmed.match(/^(\d+)\s+(.*)$/);
|
|
235
|
+
if (match) {
|
|
236
|
+
cmdByPid.set(parseInt(match[1], 10), match[2]);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
} else {
|
|
240
|
+
const { stdout } = await execPromise(`ps -o pid=,command= -p ${pids.join(',')}`);
|
|
241
|
+
const lines = stdout.trim().split(/\r?\n/);
|
|
242
|
+
for (const line of lines) {
|
|
243
|
+
const match = line.trim().match(/^(\d+)\s+(.*)$/);
|
|
244
|
+
if (match) {
|
|
245
|
+
cmdByPid.set(parseInt(match[1], 10), match[2]);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
} catch (_e) {
|
|
250
|
+
// ignore command line lookup failures
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Kill each process (Windows uses taskkill for compatibility)
|
|
254
|
+
let killedCount = 0;
|
|
255
|
+
const killedPids = [];
|
|
256
|
+
const failedPids = [];
|
|
257
|
+
for (const pid of pids) {
|
|
258
|
+
try {
|
|
259
|
+
if (platform === 'win32') {
|
|
260
|
+
try {
|
|
261
|
+
await execPromise(`taskkill /PID ${pid} /T`);
|
|
262
|
+
} catch (e) {
|
|
263
|
+
const message = String(e?.message || '');
|
|
264
|
+
if (message.includes('not found') || message.includes('not be found')) {
|
|
265
|
+
// Process already exited; treat as success.
|
|
266
|
+
killedCount++;
|
|
267
|
+
killedPids.push(pid);
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
try {
|
|
271
|
+
await execPromise(`taskkill /PID ${pid} /T /F`);
|
|
272
|
+
} catch (e2) {
|
|
273
|
+
const message2 = String(e2?.message || '');
|
|
274
|
+
if (message2.includes('not found') || message2.includes('not be found')) {
|
|
275
|
+
killedCount++;
|
|
276
|
+
killedPids.push(pid);
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
throw e2;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
} else {
|
|
283
|
+
process.kill(parseInt(pid), 'SIGTERM');
|
|
284
|
+
}
|
|
285
|
+
killedCount++;
|
|
286
|
+
killedPids.push(pid);
|
|
287
|
+
} catch (e) {
|
|
288
|
+
// Ignore if process already gone
|
|
289
|
+
if (e.code !== 'ESRCH') {
|
|
290
|
+
failedPids.push(pid);
|
|
291
|
+
console.warn(`[Lifecycle] Failed to kill PID ${pid}: ${e.message}`);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
console.info(`[Lifecycle] ✅ Stopped ${killedCount} running instance(s).`);
|
|
297
|
+
if (killedPids.length > 0) {
|
|
298
|
+
console.info('[Lifecycle] Killed processes:');
|
|
299
|
+
for (const pid of killedPids) {
|
|
300
|
+
const cmd = cmdByPid.get(parseInt(pid, 10));
|
|
301
|
+
if (cmd) {
|
|
302
|
+
console.info(` ${pid}: ${cmd}`);
|
|
303
|
+
} else {
|
|
304
|
+
console.info(` ${pid}`);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
if (failedPids.length > 0) {
|
|
309
|
+
console.info('[Lifecycle] Failed to kill:');
|
|
310
|
+
for (const pid of failedPids) {
|
|
311
|
+
const cmd = cmdByPid.get(parseInt(pid, 10));
|
|
312
|
+
if (cmd) {
|
|
313
|
+
console.info(` ${pid}: ${cmd}`);
|
|
314
|
+
} else {
|
|
315
|
+
console.info(` ${pid}`);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
await setMcpServerEnabled(false);
|
|
321
|
+
} catch (error) {
|
|
322
|
+
console.warn(`[Lifecycle] Warning: Stop command encountered an error: ${error.message}`);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
export async function start(filter = null) {
|
|
327
|
+
console.info('[Lifecycle] Ensuring server is configured...');
|
|
328
|
+
// Re-use the registration logic to ensure the config is present and correct
|
|
329
|
+
try {
|
|
330
|
+
const { register } = await import('./register.js');
|
|
331
|
+
await register(filter);
|
|
332
|
+
await setMcpServerEnabled(true);
|
|
333
|
+
console.info('[Lifecycle] ✅ Configuration checked.');
|
|
334
|
+
console.info(
|
|
335
|
+
'[Lifecycle] To start the server, please reload your IDE window or restart the IDE.'
|
|
336
|
+
);
|
|
337
|
+
} catch (err) {
|
|
338
|
+
console.error(`[Lifecycle] Failed to configure server: ${err.message}`);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
async function setMcpServerEnabled(enabled) {
|
|
343
|
+
const paths = getMcpConfigPaths();
|
|
344
|
+
const target = 'heuristic-mcp';
|
|
345
|
+
let changed = 0;
|
|
346
|
+
|
|
347
|
+
for (const { name, path: configPath, format } of paths) {
|
|
348
|
+
try {
|
|
349
|
+
await fs.access(configPath);
|
|
350
|
+
} catch {
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
try {
|
|
355
|
+
const raw = await fs.readFile(configPath, 'utf-8');
|
|
356
|
+
if (!raw || !raw.trim()) {
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
if (format === 'toml') {
|
|
360
|
+
const updatedToml = setMcpServerDisabledInToml(raw, target, !enabled);
|
|
361
|
+
if (updatedToml === raw) {
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
await fs.writeFile(configPath, updatedToml);
|
|
365
|
+
changed++;
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const parsed = parseJsonc(raw);
|
|
370
|
+
if (!parsed) {
|
|
371
|
+
console.warn(
|
|
372
|
+
`[Lifecycle] Skipping ${name} config: not valid JSON/JSONC (won't overwrite).`
|
|
373
|
+
);
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const found = findMcpServerEntry(parsed, target);
|
|
378
|
+
if (!found || !found.entry || typeof found.entry !== 'object') {
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const updatedEntry = { ...found.entry, disabled: !enabled };
|
|
383
|
+
const updatedText = upsertMcpServerEntryInText(raw, target, updatedEntry);
|
|
384
|
+
if (!updatedText) {
|
|
385
|
+
console.warn(`[Lifecycle] Failed to update ${name} config (unparseable layout).`);
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
await fs.writeFile(configPath, updatedText);
|
|
390
|
+
changed++;
|
|
391
|
+
} catch (err) {
|
|
392
|
+
console.warn(`[Lifecycle] Failed to update ${name} config: ${err.message}`);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (changed > 0) {
|
|
397
|
+
console.info(
|
|
398
|
+
`[Lifecycle] MCP server ${enabled ? 'enabled' : 'disabled'} in ${changed} config file(s).`
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function getMcpConfigPaths() {
|
|
404
|
+
const home = getUserHomeDir();
|
|
405
|
+
const configLocations = [
|
|
406
|
+
{
|
|
407
|
+
name: 'Antigravity',
|
|
408
|
+
path: path.join(home, '.gemini', 'antigravity', 'mcp_config.json'),
|
|
409
|
+
format: 'json',
|
|
410
|
+
},
|
|
411
|
+
{
|
|
412
|
+
name: 'Codex',
|
|
413
|
+
path: path.join(home, '.codex', 'config.toml'),
|
|
414
|
+
format: 'toml',
|
|
415
|
+
},
|
|
416
|
+
{
|
|
417
|
+
name: 'Claude Desktop',
|
|
418
|
+
path: path.join(home, '.config', 'Claude', 'claude_desktop_config.json'),
|
|
419
|
+
format: 'json',
|
|
420
|
+
},
|
|
421
|
+
{
|
|
422
|
+
name: 'VS Code',
|
|
423
|
+
path: path.join(home, '.config', 'Code', 'User', 'mcp.json'),
|
|
424
|
+
format: 'json',
|
|
425
|
+
},
|
|
426
|
+
{
|
|
427
|
+
name: 'VS Code Insiders',
|
|
428
|
+
path: path.join(home, '.config', 'Code - Insiders', 'User', 'mcp.json'),
|
|
429
|
+
format: 'json',
|
|
430
|
+
},
|
|
431
|
+
{
|
|
432
|
+
name: 'Cursor',
|
|
433
|
+
path: path.join(home, '.config', 'Cursor', 'User', 'settings.json'),
|
|
434
|
+
format: 'json',
|
|
435
|
+
},
|
|
436
|
+
{
|
|
437
|
+
name: 'Cursor Global',
|
|
438
|
+
path: path.join(home, '.cursor', 'mcp.json'),
|
|
439
|
+
format: 'json',
|
|
440
|
+
},
|
|
441
|
+
{
|
|
442
|
+
name: 'Windsurf',
|
|
443
|
+
path: path.join(home, '.codeium', 'windsurf', 'mcp_config.json'),
|
|
444
|
+
format: 'json',
|
|
445
|
+
},
|
|
446
|
+
{
|
|
447
|
+
name: 'Warp',
|
|
448
|
+
path: path.join(home, '.warp', 'mcp_settings.json'),
|
|
449
|
+
format: 'json',
|
|
450
|
+
},
|
|
451
|
+
];
|
|
452
|
+
|
|
453
|
+
if (process.platform === 'darwin') {
|
|
454
|
+
configLocations[2].path = path.join(
|
|
455
|
+
home,
|
|
456
|
+
'Library',
|
|
457
|
+
'Application Support',
|
|
458
|
+
'Claude',
|
|
459
|
+
'claude_desktop_config.json'
|
|
460
|
+
);
|
|
461
|
+
configLocations[3].path = path.join(
|
|
462
|
+
home,
|
|
463
|
+
'Library',
|
|
464
|
+
'Application Support',
|
|
465
|
+
'Code',
|
|
466
|
+
'User',
|
|
467
|
+
'mcp.json'
|
|
468
|
+
);
|
|
469
|
+
configLocations[4].path = path.join(
|
|
470
|
+
home,
|
|
471
|
+
'Library',
|
|
472
|
+
'Application Support',
|
|
473
|
+
'Code - Insiders',
|
|
474
|
+
'User',
|
|
475
|
+
'mcp.json'
|
|
476
|
+
);
|
|
477
|
+
configLocations[5].path = path.join(
|
|
478
|
+
home,
|
|
479
|
+
'Library',
|
|
480
|
+
'Application Support',
|
|
481
|
+
'Cursor',
|
|
482
|
+
'User',
|
|
483
|
+
'settings.json'
|
|
484
|
+
);
|
|
485
|
+
} else if (process.platform === 'win32') {
|
|
486
|
+
configLocations[2].path = path.join(
|
|
487
|
+
process.env.APPDATA || '',
|
|
488
|
+
'Claude',
|
|
489
|
+
'claude_desktop_config.json'
|
|
490
|
+
);
|
|
491
|
+
configLocations[3].path = path.join(process.env.APPDATA || '', 'Code', 'User', 'mcp.json');
|
|
492
|
+
configLocations[4].path = path.join(
|
|
493
|
+
process.env.APPDATA || '',
|
|
494
|
+
'Code - Insiders',
|
|
495
|
+
'User',
|
|
496
|
+
'mcp.json'
|
|
497
|
+
);
|
|
498
|
+
configLocations[5].path = path.join(
|
|
499
|
+
process.env.APPDATA || '',
|
|
500
|
+
'Cursor',
|
|
501
|
+
'User',
|
|
502
|
+
'settings.json'
|
|
503
|
+
);
|
|
504
|
+
configLocations.push({
|
|
505
|
+
name: 'Warp AppData',
|
|
506
|
+
path: path.join(process.env.APPDATA || '', 'Warp', 'mcp_settings.json'),
|
|
507
|
+
format: 'json',
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
return configLocations;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
async function readTail(filePath, maxLines) {
|
|
515
|
+
const data = await fs.readFile(filePath, 'utf-8');
|
|
516
|
+
if (!data) return '';
|
|
517
|
+
const lines = data.split(/\r?\n/);
|
|
518
|
+
const tail = lines.slice(-maxLines).join('\n');
|
|
519
|
+
return tail.trimEnd();
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
async function followFile(filePath, startPosition) {
|
|
523
|
+
let position = startPosition;
|
|
524
|
+
const watcher = fsSync.watch(filePath, { persistent: true }, async (event) => {
|
|
525
|
+
if (event !== 'change') return;
|
|
526
|
+
try {
|
|
527
|
+
const stats = await fs.stat(filePath);
|
|
528
|
+
if (stats.size < position) {
|
|
529
|
+
position = 0;
|
|
530
|
+
}
|
|
531
|
+
if (stats.size === position) return;
|
|
532
|
+
const stream = fsSync.createReadStream(filePath, { start: position, end: stats.size - 1 });
|
|
533
|
+
stream.pipe(process.stdout, { end: false });
|
|
534
|
+
position = stats.size;
|
|
535
|
+
} catch {
|
|
536
|
+
// ignore read errors while watching
|
|
537
|
+
}
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
const stop = () => {
|
|
541
|
+
watcher.close();
|
|
542
|
+
process.exit(0);
|
|
543
|
+
};
|
|
544
|
+
|
|
545
|
+
process.on('SIGINT', stop);
|
|
546
|
+
process.on('SIGTERM', stop);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function formatDurationMs(ms) {
|
|
550
|
+
if (!Number.isFinite(ms) || ms < 0) return null;
|
|
551
|
+
const totalSeconds = Math.round(ms / 1000);
|
|
552
|
+
const hours = Math.floor(totalSeconds / 3600);
|
|
553
|
+
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
|
554
|
+
const seconds = totalSeconds % 60;
|
|
555
|
+
|
|
556
|
+
if (hours > 0) return `${hours}h ${minutes}m ${seconds}s`;
|
|
557
|
+
if (minutes > 0) return `${minutes}m ${seconds}s`;
|
|
558
|
+
return `${seconds}s`;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function formatDateTime(value) {
|
|
562
|
+
const date = value instanceof Date ? value : new Date(value);
|
|
563
|
+
if (Number.isNaN(date.getTime())) return null;
|
|
564
|
+
return `${date.toLocaleString()} (${date.toISOString()})`;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
async function captureConsoleOutput(fn) {
|
|
568
|
+
const original = {
|
|
569
|
+
info: console.info,
|
|
570
|
+
warn: console.warn,
|
|
571
|
+
error: console.error,
|
|
572
|
+
};
|
|
573
|
+
const lines = [];
|
|
574
|
+
const collect = (...args) => {
|
|
575
|
+
const message = util.format(...args);
|
|
576
|
+
if (message && message.trim()) {
|
|
577
|
+
lines.push(message);
|
|
578
|
+
}
|
|
579
|
+
};
|
|
580
|
+
console.info = collect;
|
|
581
|
+
console.warn = collect;
|
|
582
|
+
console.error = collect;
|
|
583
|
+
try {
|
|
584
|
+
const result = await fn();
|
|
585
|
+
return { result, lines };
|
|
586
|
+
} finally {
|
|
587
|
+
console.info = original.info;
|
|
588
|
+
console.warn = original.warn;
|
|
589
|
+
console.error = original.error;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
export async function logs({ workspaceDir = null, tailLines = 200, follow = true } = {}) {
|
|
594
|
+
const config = await loadConfig(workspaceDir);
|
|
595
|
+
const logPath = getLogFilePath(config);
|
|
596
|
+
|
|
597
|
+
try {
|
|
598
|
+
const stats = await fs.stat(logPath);
|
|
599
|
+
const tail = await readTail(logPath, tailLines);
|
|
600
|
+
if (tail) {
|
|
601
|
+
process.stdout.write(tail + '\n');
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
if (!follow) {
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
console.info(`[Logs] Following ${logPath} (Ctrl+C to stop)...`);
|
|
609
|
+
await followFile(logPath, stats.size);
|
|
610
|
+
} catch (err) {
|
|
611
|
+
if (err.code === 'ENOENT') {
|
|
612
|
+
console.error(`[Logs] No log file found for workspace.`);
|
|
613
|
+
console.error(`[Logs] Expected location: ${logPath}`);
|
|
614
|
+
console.error(`[Logs] Start the server from your IDE, then run: heuristic-mcp --logs`);
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
console.error(`[Logs] Failed to read log file: ${err.message}`);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// Helper to get global cache dir
|
|
622
|
+
function getGlobalCacheDir() {
|
|
623
|
+
const home = getUserHomeDir();
|
|
624
|
+
if (process.platform === 'win32') {
|
|
625
|
+
return process.env.LOCALAPPDATA || path.join(home, 'AppData', 'Local');
|
|
626
|
+
} else if (process.platform === 'darwin') {
|
|
627
|
+
return path.join(home, 'Library', 'Caches');
|
|
628
|
+
}
|
|
629
|
+
return process.env.XDG_CACHE_HOME || path.join(home, '.cache');
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
export async function status({ fix = false, cacheOnly = false, workspaceDir = null } = {}) {
|
|
633
|
+
try {
|
|
634
|
+
const pids = [];
|
|
635
|
+
const now = new Date();
|
|
636
|
+
const globalCacheRoot = path.join(getGlobalCacheDir(), 'heuristic-mcp');
|
|
637
|
+
let logPath = 'unknown';
|
|
638
|
+
let logStatus = '';
|
|
639
|
+
let cacheSummary = null;
|
|
640
|
+
let config = null;
|
|
641
|
+
let configLogs = [];
|
|
642
|
+
|
|
643
|
+
// 1. Check PID files first (per-workspace)
|
|
644
|
+
const pidFiles = await listPidFilePaths();
|
|
645
|
+
for (const pidFile of pidFiles) {
|
|
646
|
+
const pid = await readPidFromFile(pidFile);
|
|
647
|
+
if (!Number.isInteger(pid)) continue;
|
|
648
|
+
// Check if running
|
|
649
|
+
try {
|
|
650
|
+
process.kill(pid, 0);
|
|
651
|
+
pids.push(pid);
|
|
652
|
+
} catch (_e) {
|
|
653
|
+
// Stale PID file
|
|
654
|
+
await fs.unlink(pidFile).catch(() => {});
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// 2. Fallback to process list if no PID file found or process dead
|
|
659
|
+
if (pids.length === 0) {
|
|
660
|
+
try {
|
|
661
|
+
const myPid = process.pid;
|
|
662
|
+
if (process.platform === 'win32') {
|
|
663
|
+
const { stdout } = await execPromise(
|
|
664
|
+
`powershell -NoProfile -Command "Get-CimInstance Win32_Process -Filter \\"CommandLine LIKE '%heuristic-mcp%index.js%'\\" | Select-Object -ExpandProperty ProcessId"`
|
|
665
|
+
);
|
|
666
|
+
const winPids = stdout
|
|
667
|
+
.trim()
|
|
668
|
+
.split(/\s+/)
|
|
669
|
+
.filter((p) => p && !isNaN(p));
|
|
670
|
+
|
|
671
|
+
// Retrieve command lines to filter out workers
|
|
672
|
+
if (winPids.length > 0) {
|
|
673
|
+
const { stdout: cmdOut } = await execPromise(
|
|
674
|
+
`powershell -NoProfile -Command "Get-CimInstance Win32_Process | Where-Object { $_.ProcessId -in @(${winPids.join(',')}) } | Select-Object ProcessId, CommandLine"`
|
|
675
|
+
);
|
|
676
|
+
const lines = cmdOut.trim().split(/\r?\n/);
|
|
677
|
+
for (const line of lines) {
|
|
678
|
+
const trimmed = line.trim();
|
|
679
|
+
if (!trimmed || trimmed.startsWith('ProcessId')) continue;
|
|
680
|
+
const match = trimmed.match(/^(\d+)\s+(.*)$/);
|
|
681
|
+
if (match) {
|
|
682
|
+
const pid = parseInt(match[1], 10);
|
|
683
|
+
const cmd = match[2];
|
|
684
|
+
if (
|
|
685
|
+
cmd.includes('embedding-worker') ||
|
|
686
|
+
cmd.includes('embedding-process') ||
|
|
687
|
+
cmd.includes('json-worker')
|
|
688
|
+
) {
|
|
689
|
+
continue;
|
|
690
|
+
}
|
|
691
|
+
if (pid && pid !== myPid) {
|
|
692
|
+
if (!pids.includes(pid)) pids.push(pid);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
} else {
|
|
698
|
+
const { stdout } = await execPromise('ps aux');
|
|
699
|
+
const lines = stdout.split('\n');
|
|
700
|
+
const validPids = [];
|
|
701
|
+
|
|
702
|
+
for (const line of lines) {
|
|
703
|
+
if (line.includes('heuristic-mcp/index.js') || line.includes('heuristic-mcp')) {
|
|
704
|
+
// Exclude workers
|
|
705
|
+
if (
|
|
706
|
+
line.includes('embedding-worker') ||
|
|
707
|
+
line.includes('embedding-process') ||
|
|
708
|
+
line.includes('json-worker')
|
|
709
|
+
) {
|
|
710
|
+
continue;
|
|
711
|
+
}
|
|
712
|
+
const parts = line.trim().split(/\s+/);
|
|
713
|
+
const pid = parseInt(parts[1], 10);
|
|
714
|
+
if (pid && !isNaN(pid) && pid !== myPid && !line.includes(' grep ')) {
|
|
715
|
+
validPids.push(pid);
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
// Merge validPids into pids if not already present
|
|
720
|
+
for (const p of validPids) {
|
|
721
|
+
if (!pids.includes(p)) pids.push(p);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
} catch (_e) {
|
|
725
|
+
/* ignore */
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
if (!cacheOnly) {
|
|
730
|
+
// STATUS OUTPUT
|
|
731
|
+
console.info(''); // spacer
|
|
732
|
+
if (pids.length > 0) {
|
|
733
|
+
console.info(`[Lifecycle] 🟢 Server is RUNNING. PID(s): ${pids.join(', ')}`);
|
|
734
|
+
} else {
|
|
735
|
+
console.info('[Lifecycle] ⚪ Server is STOPPED.');
|
|
736
|
+
}
|
|
737
|
+
if (pids.length > 1) {
|
|
738
|
+
console.info('[Lifecycle] ⚠️ Multiple servers detected; progress may be inconsistent.');
|
|
739
|
+
}
|
|
740
|
+
if (pids.length > 0) {
|
|
741
|
+
const cmdByPid = new Map();
|
|
742
|
+
try {
|
|
743
|
+
if (process.platform === 'win32') {
|
|
744
|
+
const { stdout } = await execPromise(
|
|
745
|
+
`powershell -NoProfile -Command "Get-CimInstance Win32_Process | Where-Object { $_.ProcessId -in @(${pids.join(',')}) } | Select-Object ProcessId, CommandLine"`
|
|
746
|
+
);
|
|
747
|
+
const lines = stdout.trim().split(/\r?\n/);
|
|
748
|
+
for (const line of lines) {
|
|
749
|
+
const trimmed = line.trim();
|
|
750
|
+
if (!trimmed || trimmed.startsWith('ProcessId')) continue;
|
|
751
|
+
const match = trimmed.match(/^(\d+)\s+(.*)$/);
|
|
752
|
+
if (match) {
|
|
753
|
+
cmdByPid.set(parseInt(match[1], 10), match[2]);
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
} else {
|
|
757
|
+
const { stdout } = await execPromise(`ps -o pid=,command= -p ${pids.join(',')}`);
|
|
758
|
+
const lines = stdout.trim().split(/\r?\n/);
|
|
759
|
+
for (const line of lines) {
|
|
760
|
+
const match = line.trim().match(/^(\d+)\s+(.*)$/);
|
|
761
|
+
if (match) {
|
|
762
|
+
cmdByPid.set(parseInt(match[1], 10), match[2]);
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
} catch (_e) {
|
|
767
|
+
// ignore command line lookup failures
|
|
768
|
+
}
|
|
769
|
+
if (cmdByPid.size > 0) {
|
|
770
|
+
console.info('[Lifecycle] Active command lines:');
|
|
771
|
+
for (const pid of pids) {
|
|
772
|
+
const cmd = cmdByPid.get(pid);
|
|
773
|
+
if (cmd) {
|
|
774
|
+
console.info(` ${pid}: ${cmd}`);
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
console.info(''); // spacer
|
|
780
|
+
} // End if (!cacheOnly) - server status
|
|
781
|
+
|
|
782
|
+
if (!cacheOnly) {
|
|
783
|
+
try {
|
|
784
|
+
const captured = await captureConsoleOutput(() => loadConfig(workspaceDir));
|
|
785
|
+
config = captured.result;
|
|
786
|
+
configLogs = captured.lines;
|
|
787
|
+
logPath = getLogFilePath(config);
|
|
788
|
+
try {
|
|
789
|
+
await fs.access(logPath);
|
|
790
|
+
logStatus = '(exists)';
|
|
791
|
+
} catch {
|
|
792
|
+
logStatus = '(not found)';
|
|
793
|
+
}
|
|
794
|
+
if (config?.cacheDirectory) {
|
|
795
|
+
const metaFile = path.join(config.cacheDirectory, 'meta.json');
|
|
796
|
+
const progressFile = path.join(config.cacheDirectory, 'progress.json');
|
|
797
|
+
let metaData = null;
|
|
798
|
+
let progressData = null;
|
|
799
|
+
try {
|
|
800
|
+
metaData = JSON.parse(await fs.readFile(metaFile, 'utf-8'));
|
|
801
|
+
} catch {
|
|
802
|
+
metaData = null;
|
|
803
|
+
}
|
|
804
|
+
try {
|
|
805
|
+
progressData = JSON.parse(await fs.readFile(progressFile, 'utf-8'));
|
|
806
|
+
} catch {
|
|
807
|
+
progressData = null;
|
|
808
|
+
}
|
|
809
|
+
cacheSummary = {
|
|
810
|
+
cacheDir: config.cacheDirectory,
|
|
811
|
+
hasSnapshot: !!metaData,
|
|
812
|
+
snapshotTime: metaData?.lastSaveTime || null,
|
|
813
|
+
progress: progressData && typeof progressData.progress === 'number' ? progressData : null,
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
} catch {
|
|
817
|
+
logPath = 'unknown';
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
if (config?.searchDirectory) {
|
|
821
|
+
console.info(`[Lifecycle] Workspace: ${config.searchDirectory}`);
|
|
822
|
+
}
|
|
823
|
+
console.info(` Log file: ${logPath} ${logStatus}`.trimEnd());
|
|
824
|
+
if (cacheSummary?.cacheDir) {
|
|
825
|
+
const snapshotLabel = cacheSummary.hasSnapshot ? 'available' : 'none';
|
|
826
|
+
console.info(`[Cache] Snapshot: ${snapshotLabel}`);
|
|
827
|
+
if (cacheSummary.snapshotTime) {
|
|
828
|
+
console.info(
|
|
829
|
+
`[Cache] Snapshot saved: ${formatDateTime(cacheSummary.snapshotTime) || cacheSummary.snapshotTime}`
|
|
830
|
+
);
|
|
831
|
+
}
|
|
832
|
+
if (cacheSummary.progress) {
|
|
833
|
+
const progress = cacheSummary.progress;
|
|
834
|
+
console.info(
|
|
835
|
+
`[Cache] Progress: ${progress.progress}/${progress.total} (${progress.message || 'n/a'})`
|
|
836
|
+
);
|
|
837
|
+
} else {
|
|
838
|
+
console.info('[Cache] Progress: idle');
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
console.info(''); // spacer
|
|
842
|
+
|
|
843
|
+
if (configLogs.length > 0) {
|
|
844
|
+
for (const line of configLogs) {
|
|
845
|
+
console.info(line);
|
|
846
|
+
}
|
|
847
|
+
console.info(''); // spacer
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
if (cacheOnly) {
|
|
852
|
+
// APPEND LOGS INFO (Cache Status)
|
|
853
|
+
console.info('[Status] Inspecting cache status...\n');
|
|
854
|
+
|
|
855
|
+
if (fix) {
|
|
856
|
+
console.info('[Status] Fixing stale caches...\n');
|
|
857
|
+
await clearStaleCaches();
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
const cacheDirs = await fs.readdir(globalCacheRoot).catch(() => []);
|
|
861
|
+
|
|
862
|
+
if (cacheDirs.length === 0) {
|
|
863
|
+
console.info('[Status] No cache directories found.');
|
|
864
|
+
console.info(`[Status] Expected location: ${globalCacheRoot}`);
|
|
865
|
+
} else {
|
|
866
|
+
console.info(
|
|
867
|
+
`[Status] Found ${cacheDirs.length} cache director${cacheDirs.length === 1 ? 'y' : 'ies'} in ${globalCacheRoot}`
|
|
868
|
+
);
|
|
869
|
+
|
|
870
|
+
for (const dir of cacheDirs) {
|
|
871
|
+
const cacheDir = path.join(globalCacheRoot, dir);
|
|
872
|
+
const metaFile = path.join(cacheDir, 'meta.json');
|
|
873
|
+
const progressFile = path.join(cacheDir, 'progress.json');
|
|
874
|
+
|
|
875
|
+
console.info(`${'─'.repeat(60)}`);
|
|
876
|
+
console.info(`📁 Cache: ${dir}`);
|
|
877
|
+
console.info(` Path: ${cacheDir}`);
|
|
878
|
+
|
|
879
|
+
let metaData = null;
|
|
880
|
+
try {
|
|
881
|
+
metaData = JSON.parse(await fs.readFile(metaFile, 'utf-8'));
|
|
882
|
+
|
|
883
|
+
console.info(` Status: ✅ Valid cache`);
|
|
884
|
+
console.info(` Workspace: ${metaData.workspace || 'Unknown'}`);
|
|
885
|
+
console.info(` Files indexed: ${metaData.filesIndexed ?? 'N/A'}`);
|
|
886
|
+
console.info(` Chunks stored: ${metaData.chunksStored ?? 'N/A'}`);
|
|
887
|
+
|
|
888
|
+
if (Number.isFinite(metaData.lastDiscoveredFiles)) {
|
|
889
|
+
console.info(` Files discovered (last run): ${metaData.lastDiscoveredFiles}`);
|
|
890
|
+
}
|
|
891
|
+
if (Number.isFinite(metaData.lastFilesProcessed)) {
|
|
892
|
+
console.info(` Files processed (last run): ${metaData.lastFilesProcessed}`);
|
|
893
|
+
}
|
|
894
|
+
if (
|
|
895
|
+
Number.isFinite(metaData.lastDiscoveredFiles) &&
|
|
896
|
+
Number.isFinite(metaData.lastFilesProcessed)
|
|
897
|
+
) {
|
|
898
|
+
const delta = metaData.lastDiscoveredFiles - metaData.lastFilesProcessed;
|
|
899
|
+
console.info(` Discovery delta (last run): ${delta >= 0 ? delta : 0}`);
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
if (metaData.lastSaveTime) {
|
|
903
|
+
const saveDate = new Date(metaData.lastSaveTime);
|
|
904
|
+
const ageMs = now - saveDate;
|
|
905
|
+
const ageHours = Math.floor(ageMs / (1000 * 60 * 60));
|
|
906
|
+
const ageMins = Math.floor((ageMs % (1000 * 60 * 60)) / (1000 * 60));
|
|
907
|
+
console.info(
|
|
908
|
+
` Cached snapshot saved: ${formatDateTime(saveDate)} (${ageHours}h ${ageMins}m ago)`
|
|
909
|
+
);
|
|
910
|
+
const ageLabel = formatDurationMs(ageMs);
|
|
911
|
+
if (ageLabel) {
|
|
912
|
+
console.info(` Cached snapshot age: ${ageLabel}`);
|
|
913
|
+
}
|
|
914
|
+
console.info(` Initial index complete at: ${formatDateTime(saveDate)}`);
|
|
915
|
+
}
|
|
916
|
+
if (metaData.lastIndexStartedAt) {
|
|
917
|
+
console.info(` Last index started: ${formatDateTime(metaData.lastIndexStartedAt)}`);
|
|
918
|
+
}
|
|
919
|
+
if (metaData.lastIndexEndedAt) {
|
|
920
|
+
console.info(` Last index ended: ${formatDateTime(metaData.lastIndexEndedAt)}`);
|
|
921
|
+
}
|
|
922
|
+
if (Number.isFinite(metaData.indexDurationMs)) {
|
|
923
|
+
const duration = formatDurationMs(metaData.indexDurationMs);
|
|
924
|
+
if (duration) {
|
|
925
|
+
console.info(` Last full index duration: ${duration}`);
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
if (metaData.lastIndexMode) {
|
|
929
|
+
console.info(` Last index mode: ${String(metaData.lastIndexMode)}`);
|
|
930
|
+
}
|
|
931
|
+
if (Number.isFinite(metaData.lastBatchSize)) {
|
|
932
|
+
console.info(` Last batch size: ${metaData.lastBatchSize}`);
|
|
933
|
+
}
|
|
934
|
+
if (Number.isFinite(metaData.lastWorkerThreads)) {
|
|
935
|
+
console.info(` Last worker threads: ${metaData.lastWorkerThreads}`);
|
|
936
|
+
}
|
|
937
|
+
try {
|
|
938
|
+
const dirStats = await fs.stat(cacheDir);
|
|
939
|
+
console.info(` Cache dir last write: ${formatDateTime(dirStats.mtime)}`);
|
|
940
|
+
} catch {
|
|
941
|
+
// ignore cache dir stat errors
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
// Verify indexing completion
|
|
945
|
+
if (metaData.filesIndexed && metaData.filesIndexed > 0) {
|
|
946
|
+
console.info(` Cached index: ✅ COMPLETE (${metaData.filesIndexed} files)`);
|
|
947
|
+
} else if (metaData.filesIndexed === 0) {
|
|
948
|
+
console.info(` Cached index: ⚠️ NO FILES (check excludePatterns)`);
|
|
949
|
+
} else {
|
|
950
|
+
console.info(` Cached index: ⚠️ INCOMPLETE`);
|
|
951
|
+
}
|
|
952
|
+
} catch (err) {
|
|
953
|
+
if (err.code === 'ENOENT') {
|
|
954
|
+
try {
|
|
955
|
+
const stats = await fs.stat(cacheDir);
|
|
956
|
+
const ageMs = new Date() - stats.mtime;
|
|
957
|
+
if (ageMs < 10 * 60 * 1000) {
|
|
958
|
+
console.info(` Status: ⏳ Initializing / Indexing in progress...`);
|
|
959
|
+
console.info(` (Metadata file has not been written yet using ID ${dir})`);
|
|
960
|
+
console.info(' Initial index: ⏳ IN PROGRESS');
|
|
961
|
+
} else {
|
|
962
|
+
console.info(` Status: ⚠️ Incomplete cache (stale)`);
|
|
963
|
+
}
|
|
964
|
+
console.info(` Cache dir last write: ${stats.mtime.toLocaleString()}`);
|
|
965
|
+
} catch {
|
|
966
|
+
console.info(` Status: ❌ Invalid cache directory`);
|
|
967
|
+
}
|
|
968
|
+
} else {
|
|
969
|
+
console.info(` Status: ❌ Invalid or corrupted (${err.message})`);
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
// Show latest indexing progress if available
|
|
974
|
+
let progressData = null;
|
|
975
|
+
try {
|
|
976
|
+
progressData = JSON.parse(await fs.readFile(progressFile, 'utf-8'));
|
|
977
|
+
} catch {
|
|
978
|
+
// no progress file
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
if (progressData && typeof progressData.progress === 'number') {
|
|
982
|
+
const updatedAt = progressData.updatedAt
|
|
983
|
+
? formatDateTime(progressData.updatedAt)
|
|
984
|
+
: 'Unknown';
|
|
985
|
+
const progressLabel = metaData
|
|
986
|
+
? 'Incremental update (post-snapshot)'
|
|
987
|
+
: 'Initial index progress';
|
|
988
|
+
console.info(
|
|
989
|
+
` ${progressLabel}: ${progressData.progress}/${progressData.total} (${progressData.message || 'n/a'})`
|
|
990
|
+
);
|
|
991
|
+
console.info(` Progress updated: ${updatedAt}`);
|
|
992
|
+
|
|
993
|
+
if (progressData.updatedAt) {
|
|
994
|
+
const updatedDate = new Date(progressData.updatedAt);
|
|
995
|
+
const ageMs = now - updatedDate;
|
|
996
|
+
const staleMs = 5 * 60 * 1000;
|
|
997
|
+
const ageLabel = formatDurationMs(ageMs);
|
|
998
|
+
if (ageLabel) {
|
|
999
|
+
console.info(` Progress age: ${ageLabel}`);
|
|
1000
|
+
}
|
|
1001
|
+
if (Number.isFinite(ageMs) && ageMs > staleMs) {
|
|
1002
|
+
const staleLabel = formatDurationMs(ageMs);
|
|
1003
|
+
console.info(` Progress stale: last update ${staleLabel} ago`);
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
if (progressData.updatedAt && metaData?.lastSaveTime) {
|
|
1008
|
+
const updatedDate = new Date(progressData.updatedAt);
|
|
1009
|
+
const saveDate = new Date(metaData.lastSaveTime);
|
|
1010
|
+
if (updatedDate > saveDate) {
|
|
1011
|
+
console.info(' Note: Incremental update in progress; cached snapshot may lag.');
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
if (progressData.indexMode) {
|
|
1015
|
+
console.info(` Current index mode: ${String(progressData.indexMode)}`);
|
|
1016
|
+
}
|
|
1017
|
+
if (
|
|
1018
|
+
progressData.workerCircuitOpen &&
|
|
1019
|
+
Number.isFinite(progressData.workersDisabledUntil)
|
|
1020
|
+
) {
|
|
1021
|
+
const remainingMs = progressData.workersDisabledUntil - Date.now();
|
|
1022
|
+
const remainingLabel = formatDurationMs(Math.max(0, remainingMs));
|
|
1023
|
+
console.info(` Workers paused: ${remainingLabel || '0s'} remaining`);
|
|
1024
|
+
console.info(
|
|
1025
|
+
` Workers disabled until: ${formatDateTime(progressData.workersDisabledUntil)}`
|
|
1026
|
+
);
|
|
1027
|
+
}
|
|
1028
|
+
} else {
|
|
1029
|
+
if (metaData) {
|
|
1030
|
+
console.info(' Summary: Cached snapshot available; no update running.');
|
|
1031
|
+
} else {
|
|
1032
|
+
console.info(' Summary: No cached snapshot yet; indexing has not started.');
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
if (metaData && progressData && typeof progressData.progress === 'number') {
|
|
1037
|
+
console.info(' Indexing state: Cached snapshot available; incremental update running.');
|
|
1038
|
+
} else if (metaData) {
|
|
1039
|
+
console.info(' Indexing state: Cached snapshot available; idle.');
|
|
1040
|
+
} else if (progressData && typeof progressData.progress === 'number') {
|
|
1041
|
+
console.info(' Indexing state: Initial index in progress; no cached snapshot yet.');
|
|
1042
|
+
} else {
|
|
1043
|
+
console.info(' Indexing state: No cached snapshot; idle.');
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
console.info(`${'─'.repeat(60)}`);
|
|
1047
|
+
}
|
|
1048
|
+
} else {
|
|
1049
|
+
if (fix) {
|
|
1050
|
+
const results = await clearStaleCaches();
|
|
1051
|
+
if (results.removed > 0) {
|
|
1052
|
+
console.info(
|
|
1053
|
+
`[Status] Cache cleanup removed ${results.removed} stale cache${results.removed === 1 ? '' : 's'}`
|
|
1054
|
+
);
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
const cacheDirs = await fs.readdir(globalCacheRoot).catch(() => null);
|
|
1058
|
+
if (Array.isArray(cacheDirs)) {
|
|
1059
|
+
console.info(
|
|
1060
|
+
`[Status] Cache: ${cacheDirs.length} director${cacheDirs.length === 1 ? 'y' : 'ies'} in ${globalCacheRoot}`
|
|
1061
|
+
);
|
|
1062
|
+
} else {
|
|
1063
|
+
console.info(`[Status] Cache: ${globalCacheRoot} (not found)`);
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
// Show paths only for --status command
|
|
1068
|
+
if (!cacheOnly) {
|
|
1069
|
+
// SHOW PATHS
|
|
1070
|
+
console.info('\n[Paths] Important locations:');
|
|
1071
|
+
|
|
1072
|
+
// Global npm bin
|
|
1073
|
+
let npmBin = 'unknown';
|
|
1074
|
+
try {
|
|
1075
|
+
const { stdout } = await execPromise('npm config get prefix');
|
|
1076
|
+
npmBin = path.join(stdout.trim(), 'bin');
|
|
1077
|
+
} catch {
|
|
1078
|
+
/* ignore */
|
|
1079
|
+
}
|
|
1080
|
+
console.info(` 📦 Global npm bin: ${npmBin}`);
|
|
1081
|
+
|
|
1082
|
+
// Configs
|
|
1083
|
+
const configLocations = [
|
|
1084
|
+
{
|
|
1085
|
+
name: 'Antigravity',
|
|
1086
|
+
path: path.join(getUserHomeDir(), '.gemini', 'antigravity', 'mcp_config.json'),
|
|
1087
|
+
},
|
|
1088
|
+
{
|
|
1089
|
+
name: 'Codex',
|
|
1090
|
+
path: path.join(getUserHomeDir(), '.codex', 'config.toml'),
|
|
1091
|
+
},
|
|
1092
|
+
{
|
|
1093
|
+
name: 'Claude Desktop',
|
|
1094
|
+
path: path.join(getUserHomeDir(), '.config', 'Claude', 'claude_desktop_config.json'),
|
|
1095
|
+
},
|
|
1096
|
+
{
|
|
1097
|
+
name: 'VS Code',
|
|
1098
|
+
path: path.join(getUserHomeDir(), '.config', 'Code', 'User', 'mcp.json'),
|
|
1099
|
+
},
|
|
1100
|
+
{
|
|
1101
|
+
name: 'Cursor',
|
|
1102
|
+
path: path.join(getUserHomeDir(), '.config', 'Cursor', 'User', 'settings.json'),
|
|
1103
|
+
},
|
|
1104
|
+
{
|
|
1105
|
+
name: 'Cursor Global',
|
|
1106
|
+
path: path.join(getUserHomeDir(), '.cursor', 'mcp.json'),
|
|
1107
|
+
},
|
|
1108
|
+
{
|
|
1109
|
+
name: 'Windsurf',
|
|
1110
|
+
path: path.join(getUserHomeDir(), '.codeium', 'windsurf', 'mcp_config.json'),
|
|
1111
|
+
},
|
|
1112
|
+
{
|
|
1113
|
+
name: 'Warp',
|
|
1114
|
+
path: path.join(getUserHomeDir(), '.warp', 'mcp_settings.json'),
|
|
1115
|
+
},
|
|
1116
|
+
];
|
|
1117
|
+
|
|
1118
|
+
// Platform specific logic for config paths
|
|
1119
|
+
if (process.platform === 'darwin') {
|
|
1120
|
+
configLocations[2].path = path.join(
|
|
1121
|
+
os.homedir(),
|
|
1122
|
+
// Keep platform-native macOS path behavior.
|
|
1123
|
+
// Home directory above is used only for Windows/Linux defaults.
|
|
1124
|
+
'Library',
|
|
1125
|
+
'Application Support',
|
|
1126
|
+
'Claude',
|
|
1127
|
+
'claude_desktop_config.json'
|
|
1128
|
+
);
|
|
1129
|
+
configLocations[3].path = path.join(
|
|
1130
|
+
os.homedir(),
|
|
1131
|
+
'Library',
|
|
1132
|
+
'Application Support',
|
|
1133
|
+
'Code',
|
|
1134
|
+
'User',
|
|
1135
|
+
'mcp.json'
|
|
1136
|
+
);
|
|
1137
|
+
configLocations[4].path = path.join(
|
|
1138
|
+
os.homedir(),
|
|
1139
|
+
'Library',
|
|
1140
|
+
'Application Support',
|
|
1141
|
+
'Cursor',
|
|
1142
|
+
'User',
|
|
1143
|
+
'settings.json'
|
|
1144
|
+
);
|
|
1145
|
+
} else if (process.platform === 'win32') {
|
|
1146
|
+
configLocations[2].path = path.join(
|
|
1147
|
+
process.env.APPDATA || '',
|
|
1148
|
+
'Claude',
|
|
1149
|
+
'claude_desktop_config.json'
|
|
1150
|
+
);
|
|
1151
|
+
configLocations[3].path = path.join(
|
|
1152
|
+
process.env.APPDATA || '',
|
|
1153
|
+
'Code',
|
|
1154
|
+
'User',
|
|
1155
|
+
'mcp.json'
|
|
1156
|
+
);
|
|
1157
|
+
configLocations[4].path = path.join(
|
|
1158
|
+
process.env.APPDATA || '',
|
|
1159
|
+
'Cursor',
|
|
1160
|
+
'User',
|
|
1161
|
+
'settings.json'
|
|
1162
|
+
);
|
|
1163
|
+
configLocations.push({
|
|
1164
|
+
name: 'Warp AppData',
|
|
1165
|
+
path: path.join(process.env.APPDATA || '', 'Warp', 'mcp_settings.json'),
|
|
1166
|
+
});
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
console.info(' ⚙️ MCP configs:');
|
|
1170
|
+
for (const loc of configLocations) {
|
|
1171
|
+
let status = '(not found)';
|
|
1172
|
+
try {
|
|
1173
|
+
await fs.access(loc.path);
|
|
1174
|
+
status = '(exists)';
|
|
1175
|
+
} catch {
|
|
1176
|
+
/* ignore */
|
|
1177
|
+
}
|
|
1178
|
+
console.info(` - ${loc.name}: ${loc.path} ${status}`);
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
console.info(` 📝 Log file: ${logPath} ${logStatus}`.trimEnd());
|
|
1182
|
+
console.info(` 💾 Cache root: ${globalCacheRoot}`);
|
|
1183
|
+
console.info(` 📁 Current dir: ${process.cwd()}`);
|
|
1184
|
+
console.info('');
|
|
1185
|
+
} // End if (!cacheOnly) - paths
|
|
1186
|
+
} catch (error) {
|
|
1187
|
+
console.error(`[Lifecycle] Failed to check status: ${error.message}`);
|
|
1188
|
+
}
|
|
1189
|
+
}
|