@softerist/heuristic-mcp 2.1.46 → 3.0.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/.agent/workflows/code-review.md +60 -0
- package/.prettierrc +7 -0
- package/ARCHITECTURE.md +105 -170
- package/CONTRIBUTING.md +32 -113
- package/GEMINI.md +73 -0
- package/LICENSE +21 -21
- package/README.md +161 -54
- package/config.json +876 -76
- package/debug-pids.js +27 -0
- package/eslint.config.js +36 -0
- package/features/ann-config.js +37 -26
- package/features/clear-cache.js +28 -19
- package/features/find-similar-code.js +142 -66
- package/features/hybrid-search.js +253 -93
- package/features/index-codebase.js +1455 -394
- package/features/lifecycle.js +813 -180
- package/features/register.js +58 -52
- package/index.js +450 -306
- package/lib/cache-ops.js +22 -0
- package/lib/cache-utils.js +68 -0
- package/lib/cache.js +1392 -587
- package/lib/call-graph.js +165 -50
- package/lib/cli.js +154 -0
- package/lib/config.js +462 -121
- package/lib/embedding-process.js +77 -0
- package/lib/embedding-worker.js +545 -30
- package/lib/ignore-patterns.js +61 -59
- package/lib/json-worker.js +14 -0
- package/lib/json-writer.js +344 -0
- package/lib/logging.js +88 -0
- package/lib/memory-logger.js +13 -0
- package/lib/project-detector.js +13 -17
- package/lib/server-lifecycle.js +38 -0
- package/lib/settings-editor.js +645 -0
- package/lib/tokenizer.js +207 -104
- package/lib/utils.js +273 -198
- package/lib/vector-store-binary.js +592 -0
- package/mcp_config.example.json +13 -0
- package/package.json +13 -2
- package/scripts/clear-cache.js +6 -17
- package/scripts/download-model.js +14 -9
- package/scripts/postinstall.js +5 -5
- package/search-configs.js +36 -0
- package/test/ann-config.test.js +179 -0
- package/test/ann-fallback.test.js +6 -6
- package/test/binary-store.test.js +69 -0
- package/test/cache-branches.test.js +120 -0
- package/test/cache-errors.test.js +264 -0
- package/test/cache-extra.test.js +300 -0
- package/test/cache-helpers.test.js +205 -0
- package/test/cache-hnsw-failure.test.js +40 -0
- package/test/cache-json-worker.test.js +190 -0
- package/test/cache-worker.test.js +102 -0
- package/test/cache.test.js +443 -0
- package/test/call-graph.test.js +103 -4
- package/test/clear-cache.test.js +69 -68
- package/test/code-review-workflow.test.js +50 -0
- package/test/config.test.js +418 -0
- package/test/coverage-gap.test.js +497 -0
- package/test/coverage-maximizer.test.js +236 -0
- package/test/debug-analysis.js +107 -0
- package/test/embedding-model.test.js +173 -103
- package/test/embedding-worker-extra.test.js +272 -0
- package/test/embedding-worker.test.js +158 -0
- package/test/features.test.js +139 -0
- package/test/final-boost.test.js +271 -0
- package/test/final-polish.test.js +183 -0
- package/test/final.test.js +95 -0
- package/test/find-similar-code.test.js +191 -0
- package/test/helpers.js +92 -11
- package/test/helpers.test.js +46 -0
- package/test/hybrid-search-basic.test.js +62 -0
- package/test/hybrid-search-branch.test.js +202 -0
- package/test/hybrid-search-callgraph.test.js +229 -0
- package/test/hybrid-search-extra.test.js +81 -0
- package/test/hybrid-search.test.js +484 -71
- package/test/index-cli.test.js +520 -0
- package/test/index-codebase-batch.test.js +119 -0
- package/test/index-codebase-branches.test.js +585 -0
- package/test/index-codebase-core.test.js +1032 -0
- package/test/index-codebase-edge-cases.test.js +254 -0
- package/test/index-codebase-errors.test.js +132 -0
- package/test/index-codebase-gap.test.js +239 -0
- package/test/index-codebase-lines.test.js +151 -0
- package/test/index-codebase-watcher.test.js +259 -0
- package/test/index-codebase-zone.test.js +259 -0
- package/test/index-codebase.test.js +371 -69
- package/test/index-memory.test.js +220 -0
- package/test/indexer-detailed.test.js +176 -0
- package/test/integration.test.js +148 -92
- package/test/json-worker.test.js +50 -0
- package/test/lifecycle.test.js +541 -0
- package/test/master.test.js +198 -0
- package/test/perfection.test.js +349 -0
- package/test/project-detector.test.js +65 -0
- package/test/register.test.js +262 -0
- package/test/tokenizer.test.js +55 -93
- package/test/ultra-maximizer.test.js +116 -0
- package/test/utils-branches.test.js +161 -0
- package/test/utils-extra.test.js +116 -0
- package/test/utils.test.js +131 -0
- package/test/verify_fixes.js +76 -0
- package/test/worker-errors.test.js +96 -0
- package/test/worker-init.test.js +102 -0
- package/test/worker_throttling.test.js +93 -0
- package/tools/scripts/benchmark-search.js +95 -0
- package/tools/scripts/cache-stats.js +71 -0
- package/tools/scripts/manual-search.js +34 -0
- package/vitest.config.js +19 -9
package/features/lifecycle.js
CHANGED
|
@@ -1,37 +1,137 @@
|
|
|
1
|
-
|
|
2
1
|
import { exec } from 'child_process';
|
|
3
2
|
import util from 'util';
|
|
4
3
|
import path from 'path';
|
|
5
4
|
import os from 'os';
|
|
6
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 { findMcpServerEntry, parseJsonc, upsertMcpServerEntryInText } from '../lib/settings-editor.js';
|
|
7
11
|
|
|
8
12
|
const execPromise = util.promisify(exec);
|
|
9
13
|
|
|
10
14
|
export async function stop() {
|
|
11
|
-
console.
|
|
15
|
+
console.info('[Lifecycle] Stopping Heuristic MCP servers...');
|
|
12
16
|
try {
|
|
13
17
|
const platform = process.platform;
|
|
14
18
|
const currentPid = process.pid;
|
|
15
19
|
let pids = [];
|
|
20
|
+
const cmdByPid = new Map();
|
|
21
|
+
const manualPid = process.env.HEURISTIC_MCP_PID;
|
|
16
22
|
|
|
17
23
|
if (platform === 'win32') {
|
|
18
|
-
|
|
19
|
-
|
|
24
|
+
// 1. Try PID file first for reliability
|
|
25
|
+
const home = os.homedir();
|
|
26
|
+
const pidFile = path.join(home, '.heuristic-mcp.pid');
|
|
27
|
+
try {
|
|
28
|
+
const content = await fs.readFile(pidFile, 'utf-8');
|
|
29
|
+
const p = content.trim();
|
|
30
|
+
if (p && !isNaN(p)) {
|
|
31
|
+
const pid = parseInt(p, 10);
|
|
32
|
+
if (pid !== currentPid) {
|
|
33
|
+
try {
|
|
34
|
+
process.kill(pid, 0);
|
|
35
|
+
pids.push(p);
|
|
36
|
+
} catch (e) {
|
|
37
|
+
// If we lack permission, still attempt to stop by PID.
|
|
38
|
+
if (e.code === 'EPERM') {
|
|
39
|
+
pids.push(p);
|
|
40
|
+
} else {
|
|
41
|
+
// Stale PID file
|
|
42
|
+
await fs.unlink(pidFile).catch(() => {});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
} catch (_e) {
|
|
48
|
+
// Fallback to WMIC when CIM access is denied
|
|
49
|
+
try {
|
|
50
|
+
const { stdout } = await execPromise(
|
|
51
|
+
`wmic process where "CommandLine like '%heuristic-mcp%'" get ProcessId /FORMAT:LIST`
|
|
52
|
+
);
|
|
53
|
+
const matches = stdout.match(/ProcessId=(\d+)/g) || [];
|
|
54
|
+
for (const match of matches) {
|
|
55
|
+
const pid = match.replace('ProcessId=', '');
|
|
56
|
+
if (pid && !isNaN(pid) && parseInt(pid, 10) !== currentPid) {
|
|
57
|
+
if (!pids.includes(pid)) pids.push(pid);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
} catch (_wmicErr) {
|
|
61
|
+
// ignore secondary failures
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// 2. Fallback to process list with fuzzier matching (kill all heuristic-mcp instances)
|
|
66
|
+
try {
|
|
67
|
+
const { stdout } = await execPromise(
|
|
68
|
+
`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"`
|
|
69
|
+
);
|
|
70
|
+
const listPids = stdout
|
|
71
|
+
.trim()
|
|
72
|
+
.split(/\s+/)
|
|
73
|
+
.filter((p) => p && !isNaN(p) && parseInt(p) !== currentPid);
|
|
74
|
+
|
|
75
|
+
// Retrieve command lines to filter out workers
|
|
76
|
+
if (listPids.length > 0) {
|
|
77
|
+
const { stdout: cmdOut } = await execPromise(
|
|
78
|
+
`powershell -NoProfile -Command "Get-CimInstance Win32_Process | Where-Object { $_.ProcessId -in @(${listPids.join(',')}) } | Select-Object ProcessId, CommandLine"`
|
|
79
|
+
);
|
|
80
|
+
const lines = cmdOut.trim().split(/\r?\n/);
|
|
81
|
+
for (const line of lines) {
|
|
82
|
+
const trimmed = line.trim();
|
|
83
|
+
if (!trimmed || trimmed.startsWith('ProcessId')) continue;
|
|
84
|
+
const match = trimmed.match(/^(\d+)\s+(.*)$/);
|
|
85
|
+
if (match) {
|
|
86
|
+
const pid = parseInt(match[1], 10);
|
|
87
|
+
const cmd = match[2];
|
|
88
|
+
if (cmd.includes('embedding-worker') || cmd.includes('embedding-process') || cmd.includes('json-worker')) {
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (pid && !pids.includes(String(pid))) {
|
|
92
|
+
pids.push(String(pid));
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
} catch (_e) { /* ignore */ }
|
|
20
98
|
} else {
|
|
21
99
|
// Unix: Use pgrep to get all matching PIDs
|
|
22
100
|
try {
|
|
23
|
-
const { stdout } = await execPromise(`pgrep -
|
|
24
|
-
const
|
|
101
|
+
const { stdout } = await execPromise(`pgrep -fl "heuristic-mcp"`);
|
|
102
|
+
const lines = stdout.trim().split(/\r?\n/);
|
|
25
103
|
|
|
26
|
-
// Filter out current PID
|
|
104
|
+
// Filter out current PID, dead processes, and workers
|
|
27
105
|
pids = [];
|
|
28
|
-
for (const
|
|
29
|
-
|
|
30
|
-
|
|
106
|
+
for (const line of lines) {
|
|
107
|
+
const tokens = line.trim().split(/\s+/).filter(Boolean);
|
|
108
|
+
if (tokens.length === 0) continue;
|
|
109
|
+
|
|
110
|
+
const allNumeric = tokens.every((token) => /^\d+$/.test(token));
|
|
111
|
+
const candidatePids = allNumeric ? tokens : [tokens[0]];
|
|
112
|
+
|
|
113
|
+
for (const candidate of candidatePids) {
|
|
114
|
+
const pid = parseInt(candidate, 10);
|
|
115
|
+
if (!Number.isFinite(pid) || pid === currentPid) continue;
|
|
116
|
+
|
|
117
|
+
// Exclude workers when command line is present
|
|
118
|
+
if (
|
|
119
|
+
!allNumeric &&
|
|
120
|
+
(line.includes('embedding-worker') ||
|
|
121
|
+
line.includes('embedding-process') ||
|
|
122
|
+
line.includes('json-worker'))
|
|
123
|
+
) {
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
|
|
31
127
|
try {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
128
|
+
process.kill(pid, 0);
|
|
129
|
+
const pidValue = String(pid);
|
|
130
|
+
if (!pids.includes(pidValue)) {
|
|
131
|
+
pids.push(pidValue);
|
|
132
|
+
}
|
|
133
|
+
} catch (_e) { /* ignore */ }
|
|
134
|
+
}
|
|
35
135
|
}
|
|
36
136
|
} catch (e) {
|
|
37
137
|
// pgrep returns code 1 if no processes found, which is fine
|
|
@@ -40,221 +140,754 @@ export async function stop() {
|
|
|
40
140
|
}
|
|
41
141
|
}
|
|
42
142
|
|
|
143
|
+
// Manual PID override (best-effort)
|
|
144
|
+
if (manualPid) {
|
|
145
|
+
const parts = String(manualPid)
|
|
146
|
+
.split(/[,\s]+/)
|
|
147
|
+
.map((part) => part.trim())
|
|
148
|
+
.filter(Boolean);
|
|
149
|
+
for (const part of parts) {
|
|
150
|
+
if (!isNaN(part)) {
|
|
151
|
+
const pidValue = String(parseInt(part, 10));
|
|
152
|
+
if (pidValue && !pids.includes(pidValue)) {
|
|
153
|
+
pids.push(pidValue);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
43
159
|
if (pids.length === 0) {
|
|
44
|
-
console.
|
|
160
|
+
console.info('[Lifecycle] No running instances found (already stopped).');
|
|
161
|
+
await setMcpServerEnabled(false);
|
|
45
162
|
return;
|
|
46
163
|
}
|
|
47
164
|
|
|
48
|
-
//
|
|
165
|
+
// Capture command lines before killing (best-effort)
|
|
166
|
+
try {
|
|
167
|
+
if (platform === 'win32') {
|
|
168
|
+
const { stdout } = await execPromise(
|
|
169
|
+
`powershell -NoProfile -Command "Get-CimInstance Win32_Process | Where-Object { $_.ProcessId -in @(${pids.join(',')}) } | Select-Object ProcessId, CommandLine"`
|
|
170
|
+
);
|
|
171
|
+
const lines = stdout.trim().split(/\r?\n/);
|
|
172
|
+
for (const line of lines) {
|
|
173
|
+
const trimmed = line.trim();
|
|
174
|
+
if (!trimmed || trimmed.startsWith('ProcessId')) continue;
|
|
175
|
+
const match = trimmed.match(/^(\d+)\s+(.*)$/);
|
|
176
|
+
if (match) {
|
|
177
|
+
cmdByPid.set(parseInt(match[1], 10), match[2]);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
} else {
|
|
181
|
+
const { stdout } = await execPromise(`ps -o pid=,command= -p ${pids.join(',')}`);
|
|
182
|
+
const lines = stdout.trim().split(/\r?\n/);
|
|
183
|
+
for (const line of lines) {
|
|
184
|
+
const match = line.trim().match(/^(\d+)\s+(.*)$/);
|
|
185
|
+
if (match) {
|
|
186
|
+
cmdByPid.set(parseInt(match[1], 10), match[2]);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
} catch (_e) {
|
|
191
|
+
// ignore command line lookup failures
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Kill each process (Windows uses taskkill for compatibility)
|
|
49
195
|
let killedCount = 0;
|
|
196
|
+
const killedPids = [];
|
|
197
|
+
const failedPids = [];
|
|
50
198
|
for (const pid of pids) {
|
|
51
199
|
try {
|
|
52
|
-
|
|
200
|
+
if (platform === 'win32') {
|
|
201
|
+
try {
|
|
202
|
+
await execPromise(`taskkill /PID ${pid} /T`);
|
|
203
|
+
} catch (e) {
|
|
204
|
+
const message = String(e?.message || '');
|
|
205
|
+
if (message.includes('not found') || message.includes('not be found')) {
|
|
206
|
+
// Process already exited; treat as success.
|
|
207
|
+
killedCount++;
|
|
208
|
+
killedPids.push(pid);
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
try {
|
|
212
|
+
await execPromise(`taskkill /PID ${pid} /T /F`);
|
|
213
|
+
} catch (e2) {
|
|
214
|
+
const message2 = String(e2?.message || '');
|
|
215
|
+
if (message2.includes('not found') || message2.includes('not be found')) {
|
|
216
|
+
killedCount++;
|
|
217
|
+
killedPids.push(pid);
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
throw e2;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
} else {
|
|
224
|
+
process.kill(parseInt(pid), 'SIGTERM');
|
|
225
|
+
}
|
|
53
226
|
killedCount++;
|
|
227
|
+
killedPids.push(pid);
|
|
54
228
|
} catch (e) {
|
|
55
229
|
// Ignore if process already gone
|
|
56
|
-
if (e.code !== 'ESRCH')
|
|
230
|
+
if (e.code !== 'ESRCH') {
|
|
231
|
+
failedPids.push(pid);
|
|
232
|
+
console.warn(`[Lifecycle] Failed to kill PID ${pid}: ${e.message}`);
|
|
233
|
+
}
|
|
57
234
|
}
|
|
58
235
|
}
|
|
59
236
|
|
|
60
|
-
console.
|
|
237
|
+
console.info(`[Lifecycle] ✅ Stopped ${killedCount} running instance(s).`);
|
|
238
|
+
if (killedPids.length > 0) {
|
|
239
|
+
console.info('[Lifecycle] Killed processes:');
|
|
240
|
+
for (const pid of killedPids) {
|
|
241
|
+
const cmd = cmdByPid.get(parseInt(pid, 10));
|
|
242
|
+
if (cmd) {
|
|
243
|
+
console.info(` ${pid}: ${cmd}`);
|
|
244
|
+
} else {
|
|
245
|
+
console.info(` ${pid}`);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
if (failedPids.length > 0) {
|
|
250
|
+
console.info('[Lifecycle] Failed to kill:');
|
|
251
|
+
for (const pid of failedPids) {
|
|
252
|
+
const cmd = cmdByPid.get(parseInt(pid, 10));
|
|
253
|
+
if (cmd) {
|
|
254
|
+
console.info(` ${pid}: ${cmd}`);
|
|
255
|
+
} else {
|
|
256
|
+
console.info(` ${pid}`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
await setMcpServerEnabled(false);
|
|
61
262
|
} catch (error) {
|
|
62
263
|
console.warn(`[Lifecycle] Warning: Stop command encountered an error: ${error.message}`);
|
|
63
264
|
}
|
|
64
265
|
}
|
|
65
266
|
|
|
66
267
|
export async function start() {
|
|
67
|
-
|
|
68
|
-
|
|
268
|
+
console.info('[Lifecycle] Ensuring server is configured...');
|
|
269
|
+
// Re-use the registration logic to ensure the config is present and correct
|
|
270
|
+
try {
|
|
271
|
+
const { register } = await import('./register.js');
|
|
272
|
+
await register();
|
|
273
|
+
await setMcpServerEnabled(true);
|
|
274
|
+
console.info('[Lifecycle] ✅ Configuration checked.');
|
|
275
|
+
console.info(
|
|
276
|
+
'[Lifecycle] To start the server, please reload your IDE window or restart the IDE.'
|
|
277
|
+
);
|
|
278
|
+
} catch (err) {
|
|
279
|
+
console.error(`[Lifecycle] Failed to configure server: ${err.message}`);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
async function setMcpServerEnabled(enabled) {
|
|
284
|
+
const paths = getMcpConfigPaths();
|
|
285
|
+
const target = 'heuristic-mcp';
|
|
286
|
+
let changed = 0;
|
|
287
|
+
|
|
288
|
+
for (const { name, path: configPath } of paths) {
|
|
69
289
|
try {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
290
|
+
await fs.access(configPath);
|
|
291
|
+
} catch {
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
try {
|
|
296
|
+
const raw = await fs.readFile(configPath, 'utf-8');
|
|
297
|
+
if (!raw || !raw.trim()) {
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
const parsed = parseJsonc(raw);
|
|
301
|
+
if (!parsed) {
|
|
302
|
+
console.warn(
|
|
303
|
+
`[Lifecycle] Skipping ${name} config: not valid JSON/JSONC (won't overwrite).`
|
|
304
|
+
);
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const found = findMcpServerEntry(parsed, target);
|
|
309
|
+
if (!found || !found.entry || typeof found.entry !== 'object') {
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const updatedEntry = { ...found.entry, disabled: !enabled };
|
|
314
|
+
const updatedText = upsertMcpServerEntryInText(raw, target, updatedEntry);
|
|
315
|
+
if (!updatedText) {
|
|
316
|
+
console.warn(`[Lifecycle] Failed to update ${name} config (unparseable layout).`);
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
await fs.writeFile(configPath, updatedText);
|
|
321
|
+
changed++;
|
|
74
322
|
} catch (err) {
|
|
75
|
-
|
|
323
|
+
console.warn(`[Lifecycle] Failed to update ${name} config: ${err.message}`);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (changed > 0) {
|
|
328
|
+
console.info(
|
|
329
|
+
`[Lifecycle] MCP server ${enabled ? 'enabled' : 'disabled'} in ${changed} config file(s).`
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function getMcpConfigPaths() {
|
|
335
|
+
const home = os.homedir();
|
|
336
|
+
const configLocations = [
|
|
337
|
+
{
|
|
338
|
+
name: 'Antigravity',
|
|
339
|
+
path: path.join(home, '.gemini', 'antigravity', 'mcp_config.json'),
|
|
340
|
+
},
|
|
341
|
+
{
|
|
342
|
+
name: 'Claude Desktop',
|
|
343
|
+
path: path.join(home, '.config', 'Claude', 'claude_desktop_config.json'),
|
|
344
|
+
},
|
|
345
|
+
{
|
|
346
|
+
name: 'VS Code',
|
|
347
|
+
path: path.join(home, '.config', 'Code', 'User', 'settings.json'),
|
|
348
|
+
settingsMode: true,
|
|
349
|
+
},
|
|
350
|
+
{
|
|
351
|
+
name: 'Cursor',
|
|
352
|
+
path: path.join(home, '.config', 'Cursor', 'User', 'settings.json'),
|
|
353
|
+
},
|
|
354
|
+
];
|
|
355
|
+
|
|
356
|
+
if (process.platform === 'darwin') {
|
|
357
|
+
configLocations[1].path = path.join(
|
|
358
|
+
home,
|
|
359
|
+
'Library',
|
|
360
|
+
'Application Support',
|
|
361
|
+
'Claude',
|
|
362
|
+
'claude_desktop_config.json'
|
|
363
|
+
);
|
|
364
|
+
configLocations[2].path = path.join(
|
|
365
|
+
home,
|
|
366
|
+
'Library',
|
|
367
|
+
'Application Support',
|
|
368
|
+
'Code',
|
|
369
|
+
'User',
|
|
370
|
+
'settings.json'
|
|
371
|
+
);
|
|
372
|
+
configLocations[3].path = path.join(
|
|
373
|
+
home,
|
|
374
|
+
'Library',
|
|
375
|
+
'Application Support',
|
|
376
|
+
'Cursor',
|
|
377
|
+
'User',
|
|
378
|
+
'settings.json'
|
|
379
|
+
);
|
|
380
|
+
} else if (process.platform === 'win32') {
|
|
381
|
+
configLocations[1].path = path.join(
|
|
382
|
+
process.env.APPDATA || '',
|
|
383
|
+
'Claude',
|
|
384
|
+
'claude_desktop_config.json'
|
|
385
|
+
);
|
|
386
|
+
configLocations[2].path = path.join(
|
|
387
|
+
process.env.APPDATA || '',
|
|
388
|
+
'Code',
|
|
389
|
+
'User',
|
|
390
|
+
'settings.json'
|
|
391
|
+
);
|
|
392
|
+
configLocations[3].path = path.join(
|
|
393
|
+
process.env.APPDATA || '',
|
|
394
|
+
'Cursor',
|
|
395
|
+
'User',
|
|
396
|
+
'settings.json'
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
return configLocations;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
async function readTail(filePath, maxLines) {
|
|
404
|
+
const data = await fs.readFile(filePath, 'utf-8');
|
|
405
|
+
if (!data) return '';
|
|
406
|
+
const lines = data.split(/\r?\n/);
|
|
407
|
+
const tail = lines.slice(-maxLines).join('\n');
|
|
408
|
+
return tail.trimEnd();
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
async function followFile(filePath, startPosition) {
|
|
412
|
+
let position = startPosition;
|
|
413
|
+
const watcher = fsSync.watch(filePath, { persistent: true }, async (event) => {
|
|
414
|
+
if (event !== 'change') return;
|
|
415
|
+
try {
|
|
416
|
+
const stats = await fs.stat(filePath);
|
|
417
|
+
if (stats.size < position) {
|
|
418
|
+
position = 0;
|
|
419
|
+
}
|
|
420
|
+
if (stats.size === position) return;
|
|
421
|
+
const stream = fsSync.createReadStream(filePath, { start: position, end: stats.size - 1 });
|
|
422
|
+
stream.pipe(process.stdout, { end: false });
|
|
423
|
+
position = stats.size;
|
|
424
|
+
} catch {
|
|
425
|
+
// ignore read errors while watching
|
|
76
426
|
}
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
const stop = () => {
|
|
430
|
+
watcher.close();
|
|
431
|
+
process.exit(0);
|
|
432
|
+
};
|
|
433
|
+
|
|
434
|
+
process.on('SIGINT', stop);
|
|
435
|
+
process.on('SIGTERM', stop);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function formatDurationMs(ms) {
|
|
439
|
+
if (!Number.isFinite(ms) || ms < 0) return null;
|
|
440
|
+
const totalSeconds = Math.round(ms / 1000);
|
|
441
|
+
const hours = Math.floor(totalSeconds / 3600);
|
|
442
|
+
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
|
443
|
+
const seconds = totalSeconds % 60;
|
|
444
|
+
|
|
445
|
+
if (hours > 0) return `${hours}h ${minutes}m ${seconds}s`;
|
|
446
|
+
if (minutes > 0) return `${minutes}m ${seconds}s`;
|
|
447
|
+
return `${seconds}s`;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function formatDateTime(value) {
|
|
451
|
+
const date = value instanceof Date ? value : new Date(value);
|
|
452
|
+
if (Number.isNaN(date.getTime())) return null;
|
|
453
|
+
return `${date.toLocaleString()} (${date.toISOString()})`;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
export async function logs({ workspaceDir = null, tailLines = 200, follow = true } = {}) {
|
|
457
|
+
const config = await loadConfig(workspaceDir);
|
|
458
|
+
const logPath = getLogFilePath(config);
|
|
459
|
+
|
|
460
|
+
try {
|
|
461
|
+
const stats = await fs.stat(logPath);
|
|
462
|
+
const tail = await readTail(logPath, tailLines);
|
|
463
|
+
if (tail) {
|
|
464
|
+
process.stdout.write(tail + '\n');
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
if (!follow) {
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
console.info(`[Logs] Following ${logPath} (Ctrl+C to stop)...`);
|
|
472
|
+
await followFile(logPath, stats.size);
|
|
473
|
+
} catch (err) {
|
|
474
|
+
if (err.code === 'ENOENT') {
|
|
475
|
+
console.error(`[Logs] No log file found for workspace.`);
|
|
476
|
+
console.error(`[Logs] Expected location: ${logPath}`);
|
|
477
|
+
console.error(`[Logs] Start the server from your IDE, then run: heuristic-mcp --logs`);
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
console.error(`[Logs] Failed to read log file: ${err.message}`);
|
|
481
|
+
}
|
|
77
482
|
}
|
|
78
483
|
|
|
79
484
|
// Helper to get global cache dir
|
|
80
485
|
function getGlobalCacheDir() {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
486
|
+
if (process.platform === 'win32') {
|
|
487
|
+
return process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local');
|
|
488
|
+
} else if (process.platform === 'darwin') {
|
|
489
|
+
return path.join(os.homedir(), 'Library', 'Caches');
|
|
490
|
+
}
|
|
491
|
+
return process.env.XDG_CACHE_HOME || path.join(os.homedir(), '.cache');
|
|
87
492
|
}
|
|
88
493
|
|
|
89
|
-
export async function status() {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
494
|
+
export async function status({ fix = false } = {}) {
|
|
495
|
+
try {
|
|
496
|
+
const home = os.homedir();
|
|
497
|
+
const pids = [];
|
|
498
|
+
const now = new Date();
|
|
93
499
|
|
|
94
|
-
|
|
95
|
-
|
|
500
|
+
// 1. Check PID file first
|
|
501
|
+
const pidFile = path.join(home, '.heuristic-mcp.pid');
|
|
96
502
|
|
|
503
|
+
try {
|
|
504
|
+
const content = await fs.readFile(pidFile, 'utf-8');
|
|
505
|
+
const pid = parseInt(content.trim(), 10);
|
|
506
|
+
if (pid && !isNaN(pid)) {
|
|
507
|
+
// Check if running
|
|
97
508
|
try {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
process.kill(pid, 0);
|
|
104
|
-
pids.push(pid);
|
|
105
|
-
} catch (e) {
|
|
106
|
-
// Stale PID file
|
|
107
|
-
await fs.unlink(pidFile).catch(() => {});
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
} catch (e) {
|
|
111
|
-
// No pid file, ignore
|
|
509
|
+
process.kill(pid, 0);
|
|
510
|
+
pids.push(pid);
|
|
511
|
+
} catch (_e) {
|
|
512
|
+
// Stale PID file
|
|
513
|
+
await fs.unlink(pidFile).catch(() => {});
|
|
112
514
|
}
|
|
515
|
+
}
|
|
516
|
+
} catch (_e) {
|
|
517
|
+
// No pid file, ignore
|
|
518
|
+
}
|
|
113
519
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
520
|
+
// 2. Fallback to process list if no PID file found or process dead
|
|
521
|
+
if (pids.length === 0) {
|
|
522
|
+
try {
|
|
523
|
+
const myPid = process.pid;
|
|
524
|
+
if (process.platform === 'win32') {
|
|
525
|
+
const { stdout } = await execPromise(
|
|
526
|
+
`powershell -NoProfile -Command "Get-CimInstance Win32_Process -Filter \\"CommandLine LIKE '%heuristic-mcp%index.js%'\\" | Select-Object -ExpandProperty ProcessId"`
|
|
527
|
+
);
|
|
528
|
+
const winPids = stdout
|
|
529
|
+
.trim()
|
|
530
|
+
.split(/\s+/)
|
|
531
|
+
.filter((p) => p && !isNaN(p));
|
|
532
|
+
|
|
533
|
+
// Retrieve command lines to filter out workers
|
|
534
|
+
if (winPids.length > 0) {
|
|
535
|
+
const { stdout: cmdOut } = await execPromise(
|
|
536
|
+
`powershell -NoProfile -Command "Get-CimInstance Win32_Process | Where-Object { $_.ProcessId -in @(${winPids.join(',')}) } | Select-Object ProcessId, CommandLine"`
|
|
537
|
+
);
|
|
538
|
+
const lines = cmdOut.trim().split(/\r?\n/);
|
|
539
|
+
for (const line of lines) {
|
|
540
|
+
const trimmed = line.trim();
|
|
541
|
+
if (!trimmed || trimmed.startsWith('ProcessId')) continue;
|
|
542
|
+
const match = trimmed.match(/^(\d+)\s+(.*)$/);
|
|
543
|
+
if (match) {
|
|
544
|
+
const pid = parseInt(match[1], 10);
|
|
545
|
+
const cmd = match[2];
|
|
546
|
+
if (cmd.includes('embedding-worker') || cmd.includes('embedding-process') || cmd.includes('json-worker')) {
|
|
547
|
+
continue;
|
|
548
|
+
}
|
|
549
|
+
if (pid && pid !== myPid) {
|
|
550
|
+
if (!pids.includes(pid)) pids.push(pid);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
} else {
|
|
556
|
+
const { stdout } = await execPromise('ps aux');
|
|
557
|
+
const lines = stdout.split('\n');
|
|
558
|
+
const validPids = [];
|
|
559
|
+
|
|
560
|
+
for (const line of lines) {
|
|
561
|
+
if (line.includes('heuristic-mcp/index.js') || line.includes('heuristic-mcp')) {
|
|
562
|
+
// Exclude workers
|
|
563
|
+
if (line.includes('embedding-worker') || line.includes('embedding-process') || line.includes('json-worker')) {
|
|
564
|
+
continue;
|
|
565
|
+
}
|
|
566
|
+
const parts = line.trim().split(/\s+/);
|
|
567
|
+
const pid = parseInt(parts[1], 10);
|
|
568
|
+
if (pid && !isNaN(pid) && pid !== myPid && !line.includes(' grep ')) {
|
|
569
|
+
validPids.push(pid);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
// Merge validPids into pids if not already present
|
|
574
|
+
for (const p of validPids) {
|
|
575
|
+
if (!pids.includes(p)) pids.push(p);
|
|
576
|
+
}
|
|
143
577
|
}
|
|
578
|
+
} catch (_e) { /* ignore */ }
|
|
579
|
+
}
|
|
144
580
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
581
|
+
// STATUS OUTPUT
|
|
582
|
+
console.info(''); // spacer
|
|
583
|
+
if (pids.length > 0) {
|
|
584
|
+
console.info(`[Lifecycle] 🟢 Server is RUNNING. PID(s): ${pids.join(', ')}`);
|
|
585
|
+
} else {
|
|
586
|
+
console.info('[Lifecycle] ⚪ Server is STOPPED.');
|
|
587
|
+
}
|
|
588
|
+
if (pids.length > 1) {
|
|
589
|
+
console.info('[Lifecycle] ⚠️ Multiple servers detected; progress may be inconsistent.');
|
|
590
|
+
}
|
|
591
|
+
if (pids.length > 0) {
|
|
592
|
+
const cmdByPid = new Map();
|
|
593
|
+
try {
|
|
594
|
+
if (process.platform === 'win32') {
|
|
595
|
+
const { stdout } = await execPromise(
|
|
596
|
+
`powershell -NoProfile -Command "Get-CimInstance Win32_Process | Where-Object { $_.ProcessId -in @(${pids.join(',')}) } | Select-Object ProcessId, CommandLine"`
|
|
597
|
+
);
|
|
598
|
+
const lines = stdout.trim().split(/\r?\n/);
|
|
599
|
+
for (const line of lines) {
|
|
600
|
+
const trimmed = line.trim();
|
|
601
|
+
if (!trimmed || trimmed.startsWith('ProcessId')) continue;
|
|
602
|
+
const match = trimmed.match(/^(\d+)\s+(.*)$/);
|
|
603
|
+
if (match) {
|
|
604
|
+
cmdByPid.set(parseInt(match[1], 10), match[2]);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
149
607
|
} else {
|
|
150
|
-
|
|
608
|
+
const { stdout } = await execPromise(`ps -o pid=,command= -p ${pids.join(',')}`);
|
|
609
|
+
const lines = stdout.trim().split(/\r?\n/);
|
|
610
|
+
for (const line of lines) {
|
|
611
|
+
const match = line.trim().match(/^(\d+)\s+(.*)$/);
|
|
612
|
+
if (match) {
|
|
613
|
+
cmdByPid.set(parseInt(match[1], 10), match[2]);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
} catch (_e) {
|
|
618
|
+
// ignore command line lookup failures
|
|
619
|
+
}
|
|
620
|
+
if (cmdByPid.size > 0) {
|
|
621
|
+
console.info('[Lifecycle] Active command lines:');
|
|
622
|
+
for (const pid of pids) {
|
|
623
|
+
const cmd = cmdByPid.get(pid);
|
|
624
|
+
if (cmd) {
|
|
625
|
+
console.info(` ${pid}: ${cmd}`);
|
|
626
|
+
}
|
|
151
627
|
}
|
|
152
|
-
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
console.info(''); // spacer
|
|
153
631
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
632
|
+
// APPEND LOGS INFO (Cache Status)
|
|
633
|
+
const globalCacheRoot = path.join(getGlobalCacheDir(), 'heuristic-mcp');
|
|
634
|
+
console.info('[Status] Inspecting cache status...\n');
|
|
157
635
|
|
|
158
|
-
|
|
636
|
+
if (fix) {
|
|
637
|
+
console.info('[Status] Fixing stale caches...\n');
|
|
638
|
+
await clearStaleCaches();
|
|
639
|
+
}
|
|
159
640
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
641
|
+
const cacheDirs = await fs.readdir(globalCacheRoot).catch(() => []);
|
|
642
|
+
|
|
643
|
+
if (cacheDirs.length === 0) {
|
|
644
|
+
console.info('[Status] No cache directories found.');
|
|
645
|
+
console.info(`[Status] Expected location: ${globalCacheRoot}`);
|
|
646
|
+
} else {
|
|
647
|
+
console.info(
|
|
648
|
+
`[Status] Found ${cacheDirs.length} cache director${cacheDirs.length === 1 ? 'y' : 'ies'} in ${globalCacheRoot}`
|
|
649
|
+
);
|
|
650
|
+
|
|
651
|
+
for (const dir of cacheDirs) {
|
|
652
|
+
const cacheDir = path.join(globalCacheRoot, dir);
|
|
653
|
+
const metaFile = path.join(cacheDir, 'meta.json');
|
|
654
|
+
const progressFile = path.join(cacheDir, 'progress.json');
|
|
655
|
+
|
|
656
|
+
console.info(`${'─'.repeat(60)}`);
|
|
657
|
+
console.info(`📁 Cache: ${dir}`);
|
|
658
|
+
console.info(` Path: ${cacheDir}`);
|
|
659
|
+
|
|
660
|
+
let metaData = null;
|
|
661
|
+
try {
|
|
662
|
+
metaData = JSON.parse(await fs.readFile(metaFile, 'utf-8'));
|
|
663
|
+
|
|
664
|
+
console.info(` Status: ✅ Valid cache`);
|
|
665
|
+
console.info(` Workspace: ${metaData.workspace || 'Unknown'}`);
|
|
666
|
+
console.info(` Files indexed: ${metaData.filesIndexed ?? 'N/A'}`);
|
|
667
|
+
console.info(` Chunks stored: ${metaData.chunksStored ?? 'N/A'}`);
|
|
668
|
+
|
|
669
|
+
if (Number.isFinite(metaData.lastDiscoveredFiles)) {
|
|
670
|
+
console.info(` Files discovered (last run): ${metaData.lastDiscoveredFiles}`);
|
|
671
|
+
}
|
|
672
|
+
if (Number.isFinite(metaData.lastFilesProcessed)) {
|
|
673
|
+
console.info(` Files processed (last run): ${metaData.lastFilesProcessed}`);
|
|
674
|
+
}
|
|
675
|
+
if (
|
|
676
|
+
Number.isFinite(metaData.lastDiscoveredFiles) &&
|
|
677
|
+
Number.isFinite(metaData.lastFilesProcessed)
|
|
678
|
+
) {
|
|
679
|
+
const delta = metaData.lastDiscoveredFiles - metaData.lastFilesProcessed;
|
|
680
|
+
console.info(` Discovery delta (last run): ${delta >= 0 ? delta : 0}`);
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
if (metaData.lastSaveTime) {
|
|
684
|
+
const saveDate = new Date(metaData.lastSaveTime);
|
|
685
|
+
const ageMs = now - saveDate;
|
|
686
|
+
const ageHours = Math.floor(ageMs / (1000 * 60 * 60));
|
|
687
|
+
const ageMins = Math.floor((ageMs % (1000 * 60 * 60)) / (1000 * 60));
|
|
688
|
+
console.info(
|
|
689
|
+
` Cached snapshot saved: ${formatDateTime(saveDate)} (${ageHours}h ${ageMins}m ago)`
|
|
690
|
+
);
|
|
691
|
+
const ageLabel = formatDurationMs(ageMs);
|
|
692
|
+
if (ageLabel) {
|
|
693
|
+
console.info(` Cached snapshot age: ${ageLabel}`);
|
|
694
|
+
}
|
|
695
|
+
console.info(` Initial index complete at: ${formatDateTime(saveDate)}`);
|
|
696
|
+
}
|
|
697
|
+
if (metaData.lastIndexStartedAt) {
|
|
698
|
+
console.info(
|
|
699
|
+
` Last index started: ${formatDateTime(metaData.lastIndexStartedAt)}`
|
|
700
|
+
);
|
|
701
|
+
}
|
|
702
|
+
if (metaData.lastIndexEndedAt) {
|
|
703
|
+
console.info(
|
|
704
|
+
` Last index ended: ${formatDateTime(metaData.lastIndexEndedAt)}`
|
|
705
|
+
);
|
|
706
|
+
}
|
|
707
|
+
if (Number.isFinite(metaData.indexDurationMs)) {
|
|
708
|
+
const duration = formatDurationMs(metaData.indexDurationMs);
|
|
709
|
+
if (duration) {
|
|
710
|
+
console.info(` Last full index duration: ${duration}`);
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
if (metaData.lastIndexMode) {
|
|
714
|
+
console.info(` Last index mode: ${String(metaData.lastIndexMode)}`);
|
|
715
|
+
}
|
|
716
|
+
if (Number.isFinite(metaData.lastBatchSize)) {
|
|
717
|
+
console.info(` Last batch size: ${metaData.lastBatchSize}`);
|
|
718
|
+
}
|
|
719
|
+
if (Number.isFinite(metaData.lastWorkerThreads)) {
|
|
720
|
+
console.info(` Last worker threads: ${metaData.lastWorkerThreads}`);
|
|
721
|
+
}
|
|
722
|
+
try {
|
|
723
|
+
const dirStats = await fs.stat(cacheDir);
|
|
724
|
+
console.info(
|
|
725
|
+
` Cache dir last write: ${formatDateTime(dirStats.mtime)}`
|
|
726
|
+
);
|
|
727
|
+
} catch {
|
|
728
|
+
// ignore cache dir stat errors
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
// Verify indexing completion
|
|
732
|
+
if (metaData.filesIndexed && metaData.filesIndexed > 0) {
|
|
733
|
+
console.info(` Cached index: ✅ COMPLETE (${metaData.filesIndexed} files)`);
|
|
734
|
+
} else if (metaData.filesIndexed === 0) {
|
|
735
|
+
console.info(` Cached index: ⚠️ NO FILES (check excludePatterns)`);
|
|
736
|
+
} else {
|
|
737
|
+
console.info(` Cached index: ⚠️ INCOMPLETE`);
|
|
738
|
+
}
|
|
739
|
+
} catch (err) {
|
|
740
|
+
if (err.code === 'ENOENT') {
|
|
741
|
+
try {
|
|
742
|
+
const stats = await fs.stat(cacheDir);
|
|
743
|
+
const ageMs = new Date() - stats.mtime;
|
|
744
|
+
if (ageMs < 10 * 60 * 1000) {
|
|
745
|
+
console.info(` Status: ⏳ Initializing / Indexing in progress...`);
|
|
746
|
+
console.info(` (Metadata file has not been written yet using ID ${dir})`);
|
|
747
|
+
console.info(' Initial index: ⏳ IN PROGRESS');
|
|
748
|
+
} else {
|
|
749
|
+
console.info(` Status: ⚠️ Incomplete cache (stale)`);
|
|
750
|
+
}
|
|
751
|
+
console.info(
|
|
752
|
+
` Cache dir last write: ${stats.mtime.toLocaleString()}`
|
|
753
|
+
);
|
|
754
|
+
} catch {
|
|
755
|
+
console.info(` Status: ❌ Invalid cache directory`);
|
|
218
756
|
}
|
|
219
|
-
|
|
757
|
+
} else {
|
|
758
|
+
console.info(` Status: ❌ Invalid or corrupted (${err.message})`);
|
|
759
|
+
}
|
|
220
760
|
}
|
|
221
761
|
|
|
222
|
-
//
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
// Global npm bin
|
|
226
|
-
let npmBin = 'unknown';
|
|
762
|
+
// Show latest indexing progress if available
|
|
763
|
+
let progressData = null;
|
|
227
764
|
try {
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
console.log(` 📦 Global npm bin: ${npmBin}`);
|
|
232
|
-
|
|
233
|
-
// Configs
|
|
234
|
-
const configLocations = [
|
|
235
|
-
{ name: 'Antigravity', path: path.join(os.homedir(), '.gemini', 'antigravity', 'mcp_config.json') },
|
|
236
|
-
{ name: 'Cursor', path: path.join(os.homedir(), '.config', 'Cursor', 'User', 'settings.json') }
|
|
237
|
-
];
|
|
238
|
-
|
|
239
|
-
// Platform specific logic for Cursor
|
|
240
|
-
if (process.platform === 'darwin') {
|
|
241
|
-
configLocations[1].path = path.join(os.homedir(), 'Library', 'Application Support', 'Cursor', 'User', 'settings.json');
|
|
242
|
-
} else if (process.platform === 'win32') {
|
|
243
|
-
configLocations[1].path = path.join(process.env.APPDATA || '', 'Cursor', 'User', 'settings.json');
|
|
765
|
+
progressData = JSON.parse(await fs.readFile(progressFile, 'utf-8'));
|
|
766
|
+
} catch {
|
|
767
|
+
// no progress file
|
|
244
768
|
}
|
|
245
769
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
770
|
+
if (progressData && typeof progressData.progress === 'number') {
|
|
771
|
+
const updatedAt = progressData.updatedAt
|
|
772
|
+
? formatDateTime(progressData.updatedAt)
|
|
773
|
+
: 'Unknown';
|
|
774
|
+
const progressLabel = metaData
|
|
775
|
+
? 'Incremental update (post-snapshot)'
|
|
776
|
+
: 'Initial index progress';
|
|
777
|
+
console.info(
|
|
778
|
+
` ${progressLabel}: ${progressData.progress}/${progressData.total} (${progressData.message || 'n/a'})`
|
|
779
|
+
);
|
|
780
|
+
console.info(` Progress updated: ${updatedAt}`);
|
|
781
|
+
|
|
782
|
+
if (progressData.updatedAt) {
|
|
783
|
+
const updatedDate = new Date(progressData.updatedAt);
|
|
784
|
+
const ageMs = now - updatedDate;
|
|
785
|
+
const staleMs = 5 * 60 * 1000;
|
|
786
|
+
const ageLabel = formatDurationMs(ageMs);
|
|
787
|
+
if (ageLabel) {
|
|
788
|
+
console.info(` Progress age: ${ageLabel}`);
|
|
789
|
+
}
|
|
790
|
+
if (Number.isFinite(ageMs) && ageMs > staleMs) {
|
|
791
|
+
const staleLabel = formatDurationMs(ageMs);
|
|
792
|
+
console.info(` Progress stale: last update ${staleLabel} ago`);
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
if (progressData.updatedAt && metaData?.lastSaveTime) {
|
|
797
|
+
const updatedDate = new Date(progressData.updatedAt);
|
|
798
|
+
const saveDate = new Date(metaData.lastSaveTime);
|
|
799
|
+
if (updatedDate > saveDate) {
|
|
800
|
+
console.info(' Note: Incremental update in progress; cached snapshot may lag.');
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
if (progressData.indexMode) {
|
|
804
|
+
console.info(` Current index mode: ${String(progressData.indexMode)}`);
|
|
805
|
+
}
|
|
806
|
+
if (progressData.workerCircuitOpen && Number.isFinite(progressData.workersDisabledUntil)) {
|
|
807
|
+
const remainingMs = progressData.workersDisabledUntil - Date.now();
|
|
808
|
+
const remainingLabel = formatDurationMs(Math.max(0, remainingMs));
|
|
809
|
+
console.info(` Workers paused: ${remainingLabel || '0s'} remaining`);
|
|
810
|
+
console.info(
|
|
811
|
+
` Workers disabled until: ${formatDateTime(progressData.workersDisabledUntil)}`
|
|
812
|
+
);
|
|
813
|
+
}
|
|
814
|
+
} else {
|
|
815
|
+
if (metaData) {
|
|
816
|
+
console.info(' Summary: Cached snapshot available; no update running.');
|
|
817
|
+
} else {
|
|
818
|
+
console.info(' Summary: No cached snapshot yet; indexing has not started.');
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
if (metaData && progressData && typeof progressData.progress === 'number') {
|
|
823
|
+
console.info(' Indexing state: Cached snapshot available; incremental update running.');
|
|
824
|
+
} else if (metaData) {
|
|
825
|
+
console.info(' Indexing state: Cached snapshot available; idle.');
|
|
826
|
+
} else if (progressData && typeof progressData.progress === 'number') {
|
|
827
|
+
console.info(' Indexing state: Initial index in progress; no cached snapshot yet.');
|
|
828
|
+
} else {
|
|
829
|
+
console.info(' Indexing state: No cached snapshot; idle.');
|
|
251
830
|
}
|
|
831
|
+
}
|
|
832
|
+
console.info(`${'─'.repeat(60)}`);
|
|
833
|
+
}
|
|
252
834
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
console.log('');
|
|
835
|
+
// SHOW PATHS
|
|
836
|
+
console.info('\n[Paths] Important locations:');
|
|
256
837
|
|
|
257
|
-
|
|
258
|
-
|
|
838
|
+
// Global npm bin
|
|
839
|
+
let npmBin = 'unknown';
|
|
840
|
+
try {
|
|
841
|
+
const { stdout } = await execPromise('npm config get prefix');
|
|
842
|
+
npmBin = path.join(stdout.trim(), 'bin');
|
|
843
|
+
} catch { /* ignore */ }
|
|
844
|
+
console.info(` 📦 Global npm bin: ${npmBin}`);
|
|
845
|
+
|
|
846
|
+
// Configs
|
|
847
|
+
const configLocations = [
|
|
848
|
+
{
|
|
849
|
+
name: 'Antigravity',
|
|
850
|
+
path: path.join(os.homedir(), '.gemini', 'antigravity', 'mcp_config.json'),
|
|
851
|
+
},
|
|
852
|
+
{
|
|
853
|
+
name: 'Cursor',
|
|
854
|
+
path: path.join(os.homedir(), '.config', 'Cursor', 'User', 'settings.json'),
|
|
855
|
+
},
|
|
856
|
+
];
|
|
857
|
+
|
|
858
|
+
// Platform specific logic for Cursor
|
|
859
|
+
if (process.platform === 'darwin') {
|
|
860
|
+
configLocations[1].path = path.join(
|
|
861
|
+
os.homedir(),
|
|
862
|
+
'Library',
|
|
863
|
+
'Application Support',
|
|
864
|
+
'Cursor',
|
|
865
|
+
'User',
|
|
866
|
+
'settings.json'
|
|
867
|
+
);
|
|
868
|
+
} else if (process.platform === 'win32') {
|
|
869
|
+
configLocations[1].path = path.join(
|
|
870
|
+
process.env.APPDATA || '',
|
|
871
|
+
'Cursor',
|
|
872
|
+
'User',
|
|
873
|
+
'settings.json'
|
|
874
|
+
);
|
|
259
875
|
}
|
|
876
|
+
|
|
877
|
+
console.info(' ⚙️ MCP configs:');
|
|
878
|
+
for (const loc of configLocations) {
|
|
879
|
+
let status = '(not found)';
|
|
880
|
+
try {
|
|
881
|
+
await fs.access(loc.path);
|
|
882
|
+
status = '(exists)';
|
|
883
|
+
} catch { /* ignore */ }
|
|
884
|
+
console.info(` - ${loc.name}: ${loc.path} ${status}`);
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
console.info(` 💾 Cache root: ${globalCacheRoot}`);
|
|
888
|
+
console.info(` 📁 Current dir: ${process.cwd()}`);
|
|
889
|
+
console.info('');
|
|
890
|
+
} catch (error) {
|
|
891
|
+
console.error(`[Lifecycle] Failed to check status: ${error.message}`);
|
|
892
|
+
}
|
|
260
893
|
}
|