@softerist/heuristic-mcp 3.2.3 → 3.2.4

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.
Files changed (46) hide show
  1. package/README.md +387 -376
  2. package/config.jsonc +800 -800
  3. package/features/ann-config.js +102 -110
  4. package/features/clear-cache.js +81 -84
  5. package/features/find-similar-code.js +265 -286
  6. package/features/hybrid-search.js +487 -536
  7. package/features/index-codebase.js +3139 -3270
  8. package/features/lifecycle.js +1011 -1063
  9. package/features/package-version.js +277 -291
  10. package/features/register.js +351 -370
  11. package/features/resources.js +115 -130
  12. package/features/set-workspace.js +214 -240
  13. package/index.js +693 -758
  14. package/lib/cache-ops.js +22 -22
  15. package/lib/cache-utils.js +465 -519
  16. package/lib/cache.js +1749 -1849
  17. package/lib/call-graph.js +396 -396
  18. package/lib/cli.js +232 -226
  19. package/lib/config.js +1483 -1495
  20. package/lib/constants.js +511 -493
  21. package/lib/embed-query-process.js +206 -212
  22. package/lib/embedding-process.js +434 -451
  23. package/lib/embedding-worker.js +862 -934
  24. package/lib/ignore-patterns.js +276 -316
  25. package/lib/json-worker.js +14 -14
  26. package/lib/json-writer.js +302 -310
  27. package/lib/logging.js +116 -127
  28. package/lib/memory-logger.js +13 -13
  29. package/lib/onnx-backend.js +188 -193
  30. package/lib/path-utils.js +18 -23
  31. package/lib/project-detector.js +82 -84
  32. package/lib/server-lifecycle.js +133 -145
  33. package/lib/settings-editor.js +738 -739
  34. package/lib/slice-normalize.js +25 -31
  35. package/lib/tokenizer.js +168 -203
  36. package/lib/utils.js +364 -409
  37. package/lib/vector-store-binary.js +973 -991
  38. package/lib/vector-store-sqlite.js +377 -414
  39. package/lib/workspace-env.js +32 -34
  40. package/mcp_config.json +9 -9
  41. package/package.json +86 -86
  42. package/scripts/clear-cache.js +20 -20
  43. package/scripts/download-model.js +43 -43
  44. package/scripts/mcp-launcher.js +49 -49
  45. package/scripts/postinstall.js +12 -12
  46. package/search-configs.js +36 -36
@@ -1,19 +1,19 @@
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
-
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
17
  const execPromise = util.promisify(exec);
18
18
  const PID_FILE_NAME = '.heuristic-mcp.pid';
19
19
  const BINARY_TELEMETRY_FILE = 'binary-store-telemetry.json';
@@ -31,350 +31,328 @@ function hasNonZeroBinaryTelemetry(totals) {
31
31
  if (!totals || typeof totals !== 'object') return false;
32
32
  return Object.values(totals).some((value) => Number.isFinite(value) && value > 0);
33
33
  }
34
-
35
- function getUserHomeDir() {
36
- if (process.platform === 'win32' && process.env.USERPROFILE) {
37
- return process.env.USERPROFILE;
38
- }
39
- return os.homedir();
40
- }
41
-
42
- async function listPidFilePaths() {
43
- const pidFiles = new Set();
44
- pidFiles.add(path.join(getUserHomeDir(), PID_FILE_NAME));
45
- const globalCacheRoot = path.join(getGlobalCacheDir(), 'heuristic-mcp');
46
- let cacheDirs = [];
47
- try {
48
- cacheDirs = await fs.readdir(globalCacheRoot);
49
- } catch {
50
- cacheDirs = [];
51
- }
52
- if (!Array.isArray(cacheDirs)) {
53
- cacheDirs = [];
54
- }
55
- for (const dir of cacheDirs) {
56
- pidFiles.add(path.join(globalCacheRoot, dir, PID_FILE_NAME));
57
- }
58
- return Array.from(pidFiles);
59
- }
60
-
61
- async function readPidFromFile(filePath) {
62
- try {
63
- const raw = await fs.readFile(filePath, 'utf-8');
64
- const trimmed = String(raw || '').trim();
65
- if (!trimmed) return null;
66
- if (trimmed.startsWith('{')) {
67
- try {
68
- const parsed = JSON.parse(trimmed);
69
- const pid = Number(parsed?.pid);
70
- if (Number.isInteger(pid)) return pid;
71
- } catch {
72
-
73
- }
74
- }
75
- const pid = parseInt(trimmed, 10);
76
- if (!Number.isNaN(pid)) return pid;
77
- } catch {
78
-
79
- }
80
- return null;
81
- }
82
-
83
- export async function stop() {
84
- console.info('[Lifecycle] Stopping Heuristic MCP servers...');
85
- try {
86
- const platform = process.platform;
87
- const currentPid = process.pid;
88
- let pids = [];
89
- const cmdByPid = new Map();
90
- const manualPid = process.env.HEURISTIC_MCP_PID;
91
-
92
- if (platform === 'win32') {
93
-
94
- const pidFiles = await listPidFilePaths();
95
- for (const pidFile of pidFiles) {
96
- const pid = await readPidFromFile(pidFile);
97
- if (!Number.isInteger(pid) || pid === currentPid) continue;
98
- try {
99
- process.kill(pid, 0);
100
- const pidValue = String(pid);
101
- if (!pids.includes(pidValue)) pids.push(pidValue);
102
- } catch (e) {
103
-
104
- if (e.code === 'EPERM') {
105
- const pidValue = String(pid);
106
- if (!pids.includes(pidValue)) pids.push(pidValue);
107
- } else {
108
- await fs.unlink(pidFile).catch(() => {});
109
- }
110
- }
111
- }
112
-
113
- try {
114
- const { stdout } = await execPromise(
115
- `powershell -NoProfile -Command "Get-CimInstance Win32_Process | Where-Object { $_.Name -match '^node(\\\\.exe)?$' -and $_.CommandLine -and ($_.CommandLine -like '*heuristic-mcp\\\\index.js*' -or $_.CommandLine -like '*heuristic-mcp/index.js*') } | Select-Object -ExpandProperty ProcessId"`
116
- );
117
- const listPids = stdout
118
- .trim()
119
- .split(/\s+/)
120
- .filter((p) => p && !isNaN(p) && parseInt(p) !== currentPid);
121
-
122
-
123
- if (listPids.length > 0) {
124
- const { stdout: cmdOut } = await execPromise(
125
- `powershell -NoProfile -Command "Get-CimInstance Win32_Process | Where-Object { $_.ProcessId -in @(${listPids.join(',')}) } | Select-Object ProcessId, CommandLine"`
126
- );
127
- const lines = cmdOut.trim().split(/\r?\n/);
128
- for (const line of lines) {
129
- const trimmed = line.trim();
130
- if (!trimmed || trimmed.startsWith('ProcessId')) continue;
131
- const match = trimmed.match(/^(\d+)\s+(.*)$/);
132
- if (match) {
133
- const pid = parseInt(match[1], 10);
134
- const cmd = match[2];
135
- if (
136
- cmd.includes('embedding-worker') ||
137
- cmd.includes('embedding-process') ||
138
- cmd.includes('json-worker')
139
- ) {
140
- continue;
141
- }
142
- if (pid && !pids.includes(String(pid))) {
143
- pids.push(String(pid));
144
- }
145
- }
146
- }
147
- }
148
- } catch (_e) {
149
-
150
- }
151
- } else {
152
-
153
- try {
154
- const { stdout } = await execPromise(`pgrep -fl "heuristic-mcp"`);
155
- const lines = stdout.trim().split(/\r?\n/);
156
-
157
-
158
- pids = [];
159
- for (const line of lines) {
160
- const tokens = line.trim().split(/\s+/).filter(Boolean);
161
- if (tokens.length === 0) continue;
162
-
163
- const allNumeric = tokens.every((token) => /^\d+$/.test(token));
164
- const candidatePids = allNumeric ? tokens : [tokens[0]];
165
-
166
- for (const candidate of candidatePids) {
167
- const pid = parseInt(candidate, 10);
168
- if (!Number.isFinite(pid) || pid === currentPid) continue;
169
-
170
-
171
- if (
172
- !allNumeric &&
173
- (line.includes('embedding-worker') ||
174
- line.includes('embedding-process') ||
175
- line.includes('json-worker'))
176
- ) {
177
- continue;
178
- }
179
-
180
- try {
181
- process.kill(pid, 0);
182
- const pidValue = String(pid);
183
- if (!pids.includes(pidValue)) {
184
- pids.push(pidValue);
185
- }
186
- } catch (_e) {
187
-
188
- }
189
- }
190
- }
191
- } catch (e) {
192
-
193
- if (e.code === 1) pids = [];
194
- else throw e;
195
- }
196
- }
197
-
198
-
199
- if (manualPid) {
200
- const parts = String(manualPid)
201
- .split(/[,\s]+/)
202
- .map((part) => part.trim())
203
- .filter(Boolean);
204
- for (const part of parts) {
205
- if (!isNaN(part)) {
206
- const pidValue = String(parseInt(part, 10));
207
- if (pidValue && !pids.includes(pidValue)) {
208
- pids.push(pidValue);
209
- }
210
- }
211
- }
212
- }
213
-
214
- if (pids.length === 0) {
215
- console.info('[Lifecycle] No running instances found (already stopped).');
216
- await setMcpServerEnabled(false);
217
- return;
218
- }
219
-
220
-
221
- try {
222
- if (platform === 'win32') {
223
- const { stdout } = await execPromise(
224
- `powershell -NoProfile -Command "Get-CimInstance Win32_Process | Where-Object { $_.ProcessId -in @(${pids.join(',')}) } | Select-Object ProcessId, CommandLine"`
225
- );
226
- const lines = stdout.trim().split(/\r?\n/);
227
- for (const line of lines) {
228
- const trimmed = line.trim();
229
- if (!trimmed || trimmed.startsWith('ProcessId')) continue;
230
- const match = trimmed.match(/^(\d+)\s+(.*)$/);
231
- if (match) {
232
- cmdByPid.set(parseInt(match[1], 10), match[2]);
233
- }
234
- }
235
- } else {
236
- const { stdout } = await execPromise(`ps -o pid=,command= -p ${pids.join(',')}`);
237
- const lines = stdout.trim().split(/\r?\n/);
238
- for (const line of lines) {
239
- const match = line.trim().match(/^(\d+)\s+(.*)$/);
240
- if (match) {
241
- cmdByPid.set(parseInt(match[1], 10), match[2]);
242
- }
243
- }
244
- }
245
- } catch (_e) {
246
-
247
- }
248
-
249
-
250
- let killedCount = 0;
251
- const killedPids = [];
252
- const failedPids = [];
253
- for (const pid of pids) {
254
- try {
255
- if (platform === 'win32') {
256
- try {
257
- await execPromise(`taskkill /PID ${pid} /T`);
258
- } catch (e) {
259
- const message = String(e?.message || '');
260
- if (message.includes('not found') || message.includes('not be found')) {
261
-
262
- killedCount++;
263
- killedPids.push(pid);
264
- continue;
265
- }
266
- try {
267
- await execPromise(`taskkill /PID ${pid} /T /F`);
268
- } catch (e2) {
269
- const message2 = String(e2?.message || '');
270
- if (message2.includes('not found') || message2.includes('not be found')) {
271
- killedCount++;
272
- killedPids.push(pid);
273
- continue;
274
- }
275
- throw e2;
276
- }
277
- }
278
- } else {
279
- process.kill(parseInt(pid), 'SIGTERM');
280
- }
281
- killedCount++;
282
- killedPids.push(pid);
283
- } catch (e) {
284
-
285
- if (e.code !== 'ESRCH') {
286
- failedPids.push(pid);
287
- console.warn(`[Lifecycle] Failed to kill PID ${pid}: ${e.message}`);
288
- }
289
- }
290
- }
291
-
292
- console.info(`[Lifecycle] ✅ Stopped ${killedCount} running instance(s).`);
293
- if (killedPids.length > 0) {
294
- console.info('[Lifecycle] Killed processes:');
295
- for (const pid of killedPids) {
296
- const cmd = cmdByPid.get(parseInt(pid, 10));
297
- if (cmd) {
298
- console.info(` ${pid}: ${cmd}`);
299
- } else {
300
- console.info(` ${pid}`);
301
- }
302
- }
303
- }
304
- if (failedPids.length > 0) {
305
- console.info('[Lifecycle] Failed to kill:');
306
- for (const pid of failedPids) {
307
- const cmd = cmdByPid.get(parseInt(pid, 10));
308
- if (cmd) {
309
- console.info(` ${pid}: ${cmd}`);
310
- } else {
311
- console.info(` ${pid}`);
312
- }
313
- }
314
- }
315
-
316
- await setMcpServerEnabled(false);
317
- } catch (error) {
318
- console.warn(`[Lifecycle] Warning: Stop command encountered an error: ${error.message}`);
319
- }
320
- }
321
-
322
- export async function start(filter = null) {
323
- console.info('[Lifecycle] Ensuring server is configured...');
324
-
325
- try {
326
- const { register } = await import('./register.js');
327
- await register(filter);
328
- await setMcpServerEnabled(true);
329
- console.info('[Lifecycle] Configuration checked.');
330
- console.info(
331
- '[Lifecycle] To start the server, please reload your IDE window or restart the IDE.'
332
- );
333
- } catch (err) {
334
- console.error(`[Lifecycle] Failed to configure server: ${err.message}`);
335
- }
336
- }
337
-
338
- async function setMcpServerEnabled(enabled) {
339
- const paths = getMcpConfigPaths();
340
- const target = 'heuristic-mcp';
341
- let changed = 0;
342
-
343
- for (const { name, path: configPath, format } of paths) {
344
- try {
345
- await fs.access(configPath);
346
- } catch {
347
- continue;
348
- }
349
-
350
- try {
351
- const raw = await fs.readFile(configPath, 'utf-8');
352
- if (!raw || !raw.trim()) {
353
- continue;
354
- }
355
- if (format === 'toml') {
356
- const updatedToml = setMcpServerDisabledInToml(raw, target, !enabled);
357
- if (updatedToml === raw) {
358
- continue;
359
- }
360
- await fs.writeFile(configPath, updatedToml);
361
- changed++;
362
- continue;
363
- }
364
-
365
- const parsed = parseJsonc(raw);
366
- if (!parsed) {
367
- console.warn(
368
- `[Lifecycle] Skipping ${name} config: not valid JSON/JSONC (won't overwrite).`
369
- );
370
- continue;
371
- }
372
-
373
- const found = findMcpServerEntry(parsed, target);
374
- if (!found || !found.entry || typeof found.entry !== 'object') {
375
- continue;
376
- }
377
-
34
+
35
+ function getUserHomeDir() {
36
+ if (process.platform === 'win32' && process.env.USERPROFILE) {
37
+ return process.env.USERPROFILE;
38
+ }
39
+ return os.homedir();
40
+ }
41
+
42
+ async function listPidFilePaths() {
43
+ const pidFiles = new Set();
44
+ pidFiles.add(path.join(getUserHomeDir(), PID_FILE_NAME));
45
+ const globalCacheRoot = path.join(getGlobalCacheDir(), 'heuristic-mcp');
46
+ let cacheDirs = [];
47
+ try {
48
+ cacheDirs = await fs.readdir(globalCacheRoot);
49
+ } catch {
50
+ cacheDirs = [];
51
+ }
52
+ if (!Array.isArray(cacheDirs)) {
53
+ cacheDirs = [];
54
+ }
55
+ for (const dir of cacheDirs) {
56
+ pidFiles.add(path.join(globalCacheRoot, dir, PID_FILE_NAME));
57
+ }
58
+ return Array.from(pidFiles);
59
+ }
60
+
61
+ async function readPidFromFile(filePath) {
62
+ try {
63
+ const raw = await fs.readFile(filePath, 'utf-8');
64
+ const trimmed = String(raw || '').trim();
65
+ if (!trimmed) return null;
66
+ if (trimmed.startsWith('{')) {
67
+ try {
68
+ const parsed = JSON.parse(trimmed);
69
+ const pid = Number(parsed?.pid);
70
+ if (Number.isInteger(pid)) return pid;
71
+ } catch {}
72
+ }
73
+ const pid = parseInt(trimmed, 10);
74
+ if (!Number.isNaN(pid)) return pid;
75
+ } catch {}
76
+ return null;
77
+ }
78
+
79
+ export async function stop() {
80
+ console.info('[Lifecycle] Stopping Heuristic MCP servers...');
81
+ try {
82
+ const platform = process.platform;
83
+ const currentPid = process.pid;
84
+ let pids = [];
85
+ const cmdByPid = new Map();
86
+ const manualPid = process.env.HEURISTIC_MCP_PID;
87
+
88
+ if (platform === 'win32') {
89
+ const pidFiles = await listPidFilePaths();
90
+ for (const pidFile of pidFiles) {
91
+ const pid = await readPidFromFile(pidFile);
92
+ if (!Number.isInteger(pid) || pid === currentPid) continue;
93
+ try {
94
+ process.kill(pid, 0);
95
+ const pidValue = String(pid);
96
+ if (!pids.includes(pidValue)) pids.push(pidValue);
97
+ } catch (e) {
98
+ if (e.code === 'EPERM') {
99
+ const pidValue = String(pid);
100
+ if (!pids.includes(pidValue)) pids.push(pidValue);
101
+ } else {
102
+ await fs.unlink(pidFile).catch(() => {});
103
+ }
104
+ }
105
+ }
106
+
107
+ try {
108
+ const { stdout } = await execPromise(
109
+ `powershell -NoProfile -Command "Get-CimInstance Win32_Process | Where-Object { $_.Name -match '^node(\\\\.exe)?$' -and $_.CommandLine -and ($_.CommandLine -like '*heuristic-mcp\\\\index.js*' -or $_.CommandLine -like '*heuristic-mcp/index.js*') } | Select-Object -ExpandProperty ProcessId"`
110
+ );
111
+ const listPids = stdout
112
+ .trim()
113
+ .split(/\s+/)
114
+ .filter((p) => p && !isNaN(p) && parseInt(p) !== currentPid);
115
+
116
+ if (listPids.length > 0) {
117
+ const { stdout: cmdOut } = await execPromise(
118
+ `powershell -NoProfile -Command "Get-CimInstance Win32_Process | Where-Object { $_.ProcessId -in @(${listPids.join(',')}) } | Select-Object ProcessId, CommandLine"`
119
+ );
120
+ const lines = cmdOut.trim().split(/\r?\n/);
121
+ for (const line of lines) {
122
+ const trimmed = line.trim();
123
+ if (!trimmed || trimmed.startsWith('ProcessId')) continue;
124
+ const match = trimmed.match(/^(\d+)\s+(.*)$/);
125
+ if (match) {
126
+ const pid = parseInt(match[1], 10);
127
+ const cmd = match[2];
128
+ if (
129
+ cmd.includes('embedding-worker') ||
130
+ cmd.includes('embedding-process') ||
131
+ cmd.includes('json-worker')
132
+ ) {
133
+ continue;
134
+ }
135
+ if (pid && !pids.includes(String(pid))) {
136
+ pids.push(String(pid));
137
+ }
138
+ }
139
+ }
140
+ }
141
+ } catch (_e) {}
142
+ } else {
143
+ try {
144
+ const { stdout } = await execPromise(`pgrep -fl "heuristic-mcp"`);
145
+ const lines = stdout.trim().split(/\r?\n/);
146
+
147
+ pids = [];
148
+ for (const line of lines) {
149
+ const tokens = line.trim().split(/\s+/).filter(Boolean);
150
+ if (tokens.length === 0) continue;
151
+
152
+ const allNumeric = tokens.every((token) => /^\d+$/.test(token));
153
+ const candidatePids = allNumeric ? tokens : [tokens[0]];
154
+
155
+ for (const candidate of candidatePids) {
156
+ const pid = parseInt(candidate, 10);
157
+ if (!Number.isFinite(pid) || pid === currentPid) continue;
158
+
159
+ if (
160
+ !allNumeric &&
161
+ (line.includes('embedding-worker') ||
162
+ line.includes('embedding-process') ||
163
+ line.includes('json-worker'))
164
+ ) {
165
+ continue;
166
+ }
167
+
168
+ try {
169
+ process.kill(pid, 0);
170
+ const pidValue = String(pid);
171
+ if (!pids.includes(pidValue)) {
172
+ pids.push(pidValue);
173
+ }
174
+ } catch (_e) {}
175
+ }
176
+ }
177
+ } catch (e) {
178
+ if (e.code === 1) pids = [];
179
+ else throw e;
180
+ }
181
+ }
182
+
183
+ if (manualPid) {
184
+ const parts = String(manualPid)
185
+ .split(/[,\s]+/)
186
+ .map((part) => part.trim())
187
+ .filter(Boolean);
188
+ for (const part of parts) {
189
+ if (!isNaN(part)) {
190
+ const pidValue = String(parseInt(part, 10));
191
+ if (pidValue && !pids.includes(pidValue)) {
192
+ pids.push(pidValue);
193
+ }
194
+ }
195
+ }
196
+ }
197
+
198
+ if (pids.length === 0) {
199
+ console.info('[Lifecycle] No running instances found (already stopped).');
200
+ await setMcpServerEnabled(false);
201
+ return;
202
+ }
203
+
204
+ try {
205
+ if (platform === 'win32') {
206
+ const { stdout } = await execPromise(
207
+ `powershell -NoProfile -Command "Get-CimInstance Win32_Process | Where-Object { $_.ProcessId -in @(${pids.join(',')}) } | Select-Object ProcessId, CommandLine"`
208
+ );
209
+ const lines = stdout.trim().split(/\r?\n/);
210
+ for (const line of lines) {
211
+ const trimmed = line.trim();
212
+ if (!trimmed || trimmed.startsWith('ProcessId')) continue;
213
+ const match = trimmed.match(/^(\d+)\s+(.*)$/);
214
+ if (match) {
215
+ cmdByPid.set(parseInt(match[1], 10), match[2]);
216
+ }
217
+ }
218
+ } else {
219
+ const { stdout } = await execPromise(`ps -o pid=,command= -p ${pids.join(',')}`);
220
+ const lines = stdout.trim().split(/\r?\n/);
221
+ for (const line of lines) {
222
+ const match = line.trim().match(/^(\d+)\s+(.*)$/);
223
+ if (match) {
224
+ cmdByPid.set(parseInt(match[1], 10), match[2]);
225
+ }
226
+ }
227
+ }
228
+ } catch (_e) {}
229
+
230
+ let killedCount = 0;
231
+ const killedPids = [];
232
+ const failedPids = [];
233
+ for (const pid of pids) {
234
+ try {
235
+ if (platform === 'win32') {
236
+ try {
237
+ await execPromise(`taskkill /PID ${pid} /T`);
238
+ } catch (e) {
239
+ const message = String(e?.message || '');
240
+ if (message.includes('not found') || message.includes('not be found')) {
241
+ killedCount++;
242
+ killedPids.push(pid);
243
+ continue;
244
+ }
245
+ try {
246
+ await execPromise(`taskkill /PID ${pid} /T /F`);
247
+ } catch (e2) {
248
+ const message2 = String(e2?.message || '');
249
+ if (message2.includes('not found') || message2.includes('not be found')) {
250
+ killedCount++;
251
+ killedPids.push(pid);
252
+ continue;
253
+ }
254
+ throw e2;
255
+ }
256
+ }
257
+ } else {
258
+ process.kill(parseInt(pid), 'SIGTERM');
259
+ }
260
+ killedCount++;
261
+ killedPids.push(pid);
262
+ } catch (e) {
263
+ if (e.code !== 'ESRCH') {
264
+ failedPids.push(pid);
265
+ console.warn(`[Lifecycle] Failed to kill PID ${pid}: ${e.message}`);
266
+ }
267
+ }
268
+ }
269
+
270
+ console.info(`[Lifecycle] Stopped ${killedCount} running instance(s).`);
271
+ if (killedPids.length > 0) {
272
+ console.info('[Lifecycle] Killed processes:');
273
+ for (const pid of killedPids) {
274
+ const cmd = cmdByPid.get(parseInt(pid, 10));
275
+ if (cmd) {
276
+ console.info(` ${pid}: ${cmd}`);
277
+ } else {
278
+ console.info(` ${pid}`);
279
+ }
280
+ }
281
+ }
282
+ if (failedPids.length > 0) {
283
+ console.info('[Lifecycle] Failed to kill:');
284
+ for (const pid of failedPids) {
285
+ const cmd = cmdByPid.get(parseInt(pid, 10));
286
+ if (cmd) {
287
+ console.info(` ${pid}: ${cmd}`);
288
+ } else {
289
+ console.info(` ${pid}`);
290
+ }
291
+ }
292
+ }
293
+
294
+ await setMcpServerEnabled(false);
295
+ } catch (error) {
296
+ console.warn(`[Lifecycle] Warning: Stop command encountered an error: ${error.message}`);
297
+ }
298
+ }
299
+
300
+ export async function start(filter = null) {
301
+ console.info('[Lifecycle] Ensuring server is configured...');
302
+
303
+ try {
304
+ const { register } = await import('./register.js');
305
+ await register(filter);
306
+ await setMcpServerEnabled(true);
307
+ console.info('[Lifecycle] Configuration checked.');
308
+ console.info(
309
+ '[Lifecycle] To start the server, please reload your IDE window or restart the IDE.'
310
+ );
311
+ } catch (err) {
312
+ console.error(`[Lifecycle] Failed to configure server: ${err.message}`);
313
+ }
314
+ }
315
+
316
+ async function setMcpServerEnabled(enabled) {
317
+ const paths = getMcpConfigPaths();
318
+ const target = 'heuristic-mcp';
319
+ let changed = 0;
320
+
321
+ for (const { name, path: configPath, format } of paths) {
322
+ try {
323
+ await fs.access(configPath);
324
+ } catch {
325
+ continue;
326
+ }
327
+
328
+ try {
329
+ const raw = await fs.readFile(configPath, 'utf-8');
330
+ if (!raw || !raw.trim()) {
331
+ continue;
332
+ }
333
+ if (format === 'toml') {
334
+ const updatedToml = setMcpServerDisabledInToml(raw, target, !enabled);
335
+ if (updatedToml === raw) {
336
+ continue;
337
+ }
338
+ await fs.writeFile(configPath, updatedToml);
339
+ changed++;
340
+ continue;
341
+ }
342
+
343
+ const parsed = parseJsonc(raw);
344
+ if (!parsed) {
345
+ console.warn(
346
+ `[Lifecycle] Skipping ${name} config: not valid JSON/JSONC (won't overwrite).`
347
+ );
348
+ continue;
349
+ }
350
+
351
+ const found = findMcpServerEntry(parsed, target);
352
+ if (!found || !found.entry || typeof found.entry !== 'object') {
353
+ continue;
354
+ }
355
+
378
356
  const updatedEntry = { ...found.entry };
379
357
  if (enabled) {
380
358
  delete updatedEntry.disabled;
@@ -382,183 +360,181 @@ async function setMcpServerEnabled(enabled) {
382
360
  updatedEntry.disabled = true;
383
361
  }
384
362
  const updatedText = upsertMcpServerEntryInText(raw, target, updatedEntry);
385
- if (!updatedText) {
386
- console.warn(`[Lifecycle] Failed to update ${name} config (unparseable layout).`);
387
- continue;
388
- }
389
-
390
- await fs.writeFile(configPath, updatedText);
391
- changed++;
392
- } catch (err) {
393
- console.warn(`[Lifecycle] Failed to update ${name} config: ${err.message}`);
394
- }
395
- }
396
-
397
- if (changed > 0) {
398
- console.info(
399
- `[Lifecycle] MCP server ${enabled ? 'enabled' : 'disabled'} in ${changed} config file(s).`
400
- );
401
- }
402
- }
403
-
404
- function getMcpConfigPaths() {
405
- const home = getUserHomeDir();
406
- const configLocations = [
407
- {
408
- name: 'Antigravity',
409
- path: path.join(home, '.gemini', 'antigravity', 'mcp_config.json'),
410
- format: 'json',
411
- },
412
- {
413
- name: 'Codex',
414
- path: path.join(home, '.codex', 'config.toml'),
415
- format: 'toml',
416
- },
417
- {
418
- name: 'Claude Desktop',
419
- path: path.join(home, '.config', 'Claude', 'claude_desktop_config.json'),
420
- format: 'json',
421
- },
422
- {
423
- name: 'VS Code',
424
- path: path.join(home, '.config', 'Code', 'User', 'mcp.json'),
425
- format: 'json',
426
- },
427
- {
428
- name: 'VS Code Insiders',
429
- path: path.join(home, '.config', 'Code - Insiders', 'User', 'mcp.json'),
430
- format: 'json',
431
- },
432
- {
433
- name: 'Cursor',
434
- path: path.join(home, '.config', 'Cursor', 'User', 'settings.json'),
435
- format: 'json',
436
- },
437
- {
438
- name: 'Cursor Global',
439
- path: path.join(home, '.cursor', 'mcp.json'),
440
- format: 'json',
441
- },
442
- {
443
- name: 'Windsurf',
444
- path: path.join(home, '.codeium', 'windsurf', 'mcp_config.json'),
445
- format: 'json',
446
- },
447
- {
448
- name: 'Warp',
449
- path: path.join(home, '.warp', 'mcp_settings.json'),
450
- format: 'json',
451
- },
452
- ];
453
-
454
- if (process.platform === 'darwin') {
455
- configLocations[2].path = path.join(
456
- home,
457
- 'Library',
458
- 'Application Support',
459
- 'Claude',
460
- 'claude_desktop_config.json'
461
- );
462
- configLocations[3].path = path.join(
463
- home,
464
- 'Library',
465
- 'Application Support',
466
- 'Code',
467
- 'User',
468
- 'mcp.json'
469
- );
470
- configLocations[4].path = path.join(
471
- home,
472
- 'Library',
473
- 'Application Support',
474
- 'Code - Insiders',
475
- 'User',
476
- 'mcp.json'
477
- );
478
- configLocations[5].path = path.join(
479
- home,
480
- 'Library',
481
- 'Application Support',
482
- 'Cursor',
483
- 'User',
484
- 'settings.json'
485
- );
486
- } else if (process.platform === 'win32') {
487
- configLocations[2].path = path.join(
488
- process.env.APPDATA || '',
489
- 'Claude',
490
- 'claude_desktop_config.json'
491
- );
492
- configLocations[3].path = path.join(process.env.APPDATA || '', 'Code', 'User', 'mcp.json');
493
- configLocations[4].path = path.join(
494
- process.env.APPDATA || '',
495
- 'Code - Insiders',
496
- 'User',
497
- 'mcp.json'
498
- );
499
- configLocations[5].path = path.join(
500
- process.env.APPDATA || '',
501
- 'Cursor',
502
- 'User',
503
- 'settings.json'
504
- );
505
- configLocations.push({
506
- name: 'Warp AppData',
507
- path: path.join(process.env.APPDATA || '', 'Warp', 'mcp_settings.json'),
508
- format: 'json',
509
- });
510
- }
511
-
512
- return configLocations;
513
- }
514
-
515
- async function readTail(filePath, maxLines) {
516
- const data = await fs.readFile(filePath, 'utf-8');
517
- if (!data) return '';
518
- const lines = data.split(/\r?\n/);
519
- const tail = lines.slice(-maxLines).join('\n');
520
- return tail.trimEnd();
521
- }
522
-
523
- async function followFile(filePath, startPosition) {
524
- let position = startPosition;
525
- const watcher = fsSync.watch(filePath, { persistent: true }, async (event) => {
526
- if (event !== 'change') return;
527
- try {
528
- const stats = await fs.stat(filePath);
529
- if (stats.size < position) {
530
- position = 0;
531
- }
532
- if (stats.size === position) return;
533
- const stream = fsSync.createReadStream(filePath, { start: position, end: stats.size - 1 });
534
- stream.pipe(process.stdout, { end: false });
535
- position = stats.size;
536
- } catch {
537
-
538
- }
539
- });
540
-
541
- const stop = () => {
542
- watcher.close();
543
- process.exit(0);
544
- };
545
-
546
- process.on('SIGINT', stop);
547
- process.on('SIGTERM', stop);
548
- }
549
-
550
- function formatDurationMs(ms) {
551
- if (!Number.isFinite(ms) || ms < 0) return null;
552
- const totalSeconds = Math.round(ms / 1000);
553
- const hours = Math.floor(totalSeconds / 3600);
554
- const minutes = Math.floor((totalSeconds % 3600) / 60);
555
- const seconds = totalSeconds % 60;
556
-
557
- if (hours > 0) return `${hours}h ${minutes}m ${seconds}s`;
558
- if (minutes > 0) return `${minutes}m ${seconds}s`;
559
- return `${seconds}s`;
560
- }
561
-
363
+ if (!updatedText) {
364
+ console.warn(`[Lifecycle] Failed to update ${name} config (unparseable layout).`);
365
+ continue;
366
+ }
367
+
368
+ await fs.writeFile(configPath, updatedText);
369
+ changed++;
370
+ } catch (err) {
371
+ console.warn(`[Lifecycle] Failed to update ${name} config: ${err.message}`);
372
+ }
373
+ }
374
+
375
+ if (changed > 0) {
376
+ console.info(
377
+ `[Lifecycle] MCP server ${enabled ? 'enabled' : 'disabled'} in ${changed} config file(s).`
378
+ );
379
+ }
380
+ }
381
+
382
+ function getMcpConfigPaths() {
383
+ const home = getUserHomeDir();
384
+ const configLocations = [
385
+ {
386
+ name: 'Antigravity',
387
+ path: path.join(home, '.gemini', 'antigravity', 'mcp_config.json'),
388
+ format: 'json',
389
+ },
390
+ {
391
+ name: 'Codex',
392
+ path: path.join(home, '.codex', 'config.toml'),
393
+ format: 'toml',
394
+ },
395
+ {
396
+ name: 'Claude Desktop',
397
+ path: path.join(home, '.config', 'Claude', 'claude_desktop_config.json'),
398
+ format: 'json',
399
+ },
400
+ {
401
+ name: 'VS Code',
402
+ path: path.join(home, '.config', 'Code', 'User', 'mcp.json'),
403
+ format: 'json',
404
+ },
405
+ {
406
+ name: 'VS Code Insiders',
407
+ path: path.join(home, '.config', 'Code - Insiders', 'User', 'mcp.json'),
408
+ format: 'json',
409
+ },
410
+ {
411
+ name: 'Cursor',
412
+ path: path.join(home, '.config', 'Cursor', 'User', 'settings.json'),
413
+ format: 'json',
414
+ },
415
+ {
416
+ name: 'Cursor Global',
417
+ path: path.join(home, '.cursor', 'mcp.json'),
418
+ format: 'json',
419
+ },
420
+ {
421
+ name: 'Windsurf',
422
+ path: path.join(home, '.codeium', 'windsurf', 'mcp_config.json'),
423
+ format: 'json',
424
+ },
425
+ {
426
+ name: 'Warp',
427
+ path: path.join(home, '.warp', 'mcp_settings.json'),
428
+ format: 'json',
429
+ },
430
+ ];
431
+
432
+ if (process.platform === 'darwin') {
433
+ configLocations[2].path = path.join(
434
+ home,
435
+ 'Library',
436
+ 'Application Support',
437
+ 'Claude',
438
+ 'claude_desktop_config.json'
439
+ );
440
+ configLocations[3].path = path.join(
441
+ home,
442
+ 'Library',
443
+ 'Application Support',
444
+ 'Code',
445
+ 'User',
446
+ 'mcp.json'
447
+ );
448
+ configLocations[4].path = path.join(
449
+ home,
450
+ 'Library',
451
+ 'Application Support',
452
+ 'Code - Insiders',
453
+ 'User',
454
+ 'mcp.json'
455
+ );
456
+ configLocations[5].path = path.join(
457
+ home,
458
+ 'Library',
459
+ 'Application Support',
460
+ 'Cursor',
461
+ 'User',
462
+ 'settings.json'
463
+ );
464
+ } else if (process.platform === 'win32') {
465
+ configLocations[2].path = path.join(
466
+ process.env.APPDATA || '',
467
+ 'Claude',
468
+ 'claude_desktop_config.json'
469
+ );
470
+ configLocations[3].path = path.join(process.env.APPDATA || '', 'Code', 'User', 'mcp.json');
471
+ configLocations[4].path = path.join(
472
+ process.env.APPDATA || '',
473
+ 'Code - Insiders',
474
+ 'User',
475
+ 'mcp.json'
476
+ );
477
+ configLocations[5].path = path.join(
478
+ process.env.APPDATA || '',
479
+ 'Cursor',
480
+ 'User',
481
+ 'settings.json'
482
+ );
483
+ configLocations.push({
484
+ name: 'Warp AppData',
485
+ path: path.join(process.env.APPDATA || '', 'Warp', 'mcp_settings.json'),
486
+ format: 'json',
487
+ });
488
+ }
489
+
490
+ return configLocations;
491
+ }
492
+
493
+ async function readTail(filePath, maxLines) {
494
+ const data = await fs.readFile(filePath, 'utf-8');
495
+ if (!data) return '';
496
+ const lines = data.split(/\r?\n/);
497
+ const tail = lines.slice(-maxLines).join('\n');
498
+ return tail.trimEnd();
499
+ }
500
+
501
+ async function followFile(filePath, startPosition) {
502
+ let position = startPosition;
503
+ const watcher = fsSync.watch(filePath, { persistent: true }, async (event) => {
504
+ if (event !== 'change') return;
505
+ try {
506
+ const stats = await fs.stat(filePath);
507
+ if (stats.size < position) {
508
+ position = 0;
509
+ }
510
+ if (stats.size === position) return;
511
+ const stream = fsSync.createReadStream(filePath, { start: position, end: stats.size - 1 });
512
+ stream.pipe(process.stdout, { end: false });
513
+ position = stats.size;
514
+ } catch {}
515
+ });
516
+
517
+ const stop = () => {
518
+ watcher.close();
519
+ process.exit(0);
520
+ };
521
+
522
+ process.on('SIGINT', stop);
523
+ process.on('SIGTERM', stop);
524
+ }
525
+
526
+ function formatDurationMs(ms) {
527
+ if (!Number.isFinite(ms) || ms < 0) return null;
528
+ const totalSeconds = Math.round(ms / 1000);
529
+ const hours = Math.floor(totalSeconds / 3600);
530
+ const minutes = Math.floor((totalSeconds % 3600) / 60);
531
+ const seconds = totalSeconds % 60;
532
+
533
+ if (hours > 0) return `${hours}h ${minutes}m ${seconds}s`;
534
+ if (minutes > 0) return `${minutes}m ${seconds}s`;
535
+ return `${seconds}s`;
536
+ }
537
+
562
538
  function formatDateTime(value) {
563
539
  const date = value instanceof Date ? value : new Date(value);
564
540
  if (Number.isNaN(date.getTime())) return null;
@@ -718,216 +694,206 @@ function selectRuntimeForStatus({ pids, runtimeByPid, requestedWorkspace }) {
718
694
  }
719
695
 
720
696
  async function captureConsoleOutput(fn) {
721
- const original = {
722
- info: console.info,
723
- warn: console.warn,
724
- error: console.error,
725
- };
726
- const lines = [];
727
- const collect = (...args) => {
728
- const message = util.format(...args);
729
- if (message && message.trim()) {
730
- lines.push(message);
731
- }
732
- };
733
- console.info = collect;
734
- console.warn = collect;
735
- console.error = collect;
736
- try {
737
- const result = await fn();
738
- return { result, lines };
739
- } finally {
740
- console.info = original.info;
741
- console.warn = original.warn;
742
- console.error = original.error;
743
- }
744
- }
745
-
746
- export async function logs({ workspaceDir = null, tailLines = 200, follow = true } = {}) {
747
- const config = await loadConfig(workspaceDir);
748
- const logPath = getLogFilePath(config);
749
-
750
- try {
751
- const stats = await fs.stat(logPath);
752
- const tail = await readTail(logPath, tailLines);
753
- if (tail) {
754
- process.stdout.write(tail + '\n');
755
- }
756
-
757
- if (!follow) {
758
- return;
759
- }
760
-
761
- console.info(`[Logs] Following ${logPath} (Ctrl+C to stop)...`);
762
- await followFile(logPath, stats.size);
763
- } catch (err) {
764
- if (err.code === 'ENOENT') {
765
- console.error(`[Logs] No log file found for workspace.`);
766
- console.error(`[Logs] Expected location: ${logPath}`);
767
- console.error(`[Logs] Start the server from your IDE, then run: heuristic-mcp --logs`);
768
- return;
769
- }
770
- console.error(`[Logs] Failed to read log file: ${err.message}`);
771
- }
772
- }
773
-
774
- // Helper to get global cache dir
775
- function getGlobalCacheDir() {
776
- const home = getUserHomeDir();
777
- if (process.platform === 'win32') {
778
- return process.env.LOCALAPPDATA || path.join(home, 'AppData', 'Local');
779
- } else if (process.platform === 'darwin') {
780
- return path.join(home, 'Library', 'Caches');
781
- }
782
- return process.env.XDG_CACHE_HOME || path.join(home, '.cache');
783
- }
784
-
697
+ const original = {
698
+ info: console.info,
699
+ warn: console.warn,
700
+ error: console.error,
701
+ };
702
+ const lines = [];
703
+ const collect = (...args) => {
704
+ const message = util.format(...args);
705
+ if (message && message.trim()) {
706
+ lines.push(message);
707
+ }
708
+ };
709
+ console.info = collect;
710
+ console.warn = collect;
711
+ console.error = collect;
712
+ try {
713
+ const result = await fn();
714
+ return { result, lines };
715
+ } finally {
716
+ console.info = original.info;
717
+ console.warn = original.warn;
718
+ console.error = original.error;
719
+ }
720
+ }
721
+
722
+ export async function logs({ workspaceDir = null, tailLines = 200, follow = true } = {}) {
723
+ const config = await loadConfig(workspaceDir);
724
+ const logPath = getLogFilePath(config);
725
+
726
+ try {
727
+ const stats = await fs.stat(logPath);
728
+ const tail = await readTail(logPath, tailLines);
729
+ if (tail) {
730
+ process.stdout.write(tail + '\n');
731
+ }
732
+
733
+ if (!follow) {
734
+ return;
735
+ }
736
+
737
+ console.info(`[Logs] Following ${logPath} (Ctrl+C to stop)...`);
738
+ await followFile(logPath, stats.size);
739
+ } catch (err) {
740
+ if (err.code === 'ENOENT') {
741
+ console.error(`[Logs] No log file found for workspace.`);
742
+ console.error(`[Logs] Expected location: ${logPath}`);
743
+ console.error(`[Logs] Start the server from your IDE, then run: heuristic-mcp --logs`);
744
+ return;
745
+ }
746
+ console.error(`[Logs] Failed to read log file: ${err.message}`);
747
+ }
748
+ }
749
+
750
+ // Helper to get global cache dir
751
+ function getGlobalCacheDir() {
752
+ const home = getUserHomeDir();
753
+ if (process.platform === 'win32') {
754
+ return process.env.LOCALAPPDATA || path.join(home, 'AppData', 'Local');
755
+ } else if (process.platform === 'darwin') {
756
+ return path.join(home, 'Library', 'Caches');
757
+ }
758
+ return process.env.XDG_CACHE_HOME || path.join(home, '.cache');
759
+ }
760
+
785
761
  export async function status({ fix = false, cacheOnly = false, workspaceDir = null } = {}) {
786
762
  try {
787
763
  const pids = [];
788
- const now = new Date();
789
- const globalCacheRoot = path.join(getGlobalCacheDir(), 'heuristic-mcp');
790
- let logPath = 'unknown';
791
- let logStatus = '';
764
+ const now = new Date();
765
+ const globalCacheRoot = path.join(getGlobalCacheDir(), 'heuristic-mcp');
766
+ let logPath = 'unknown';
767
+ let logStatus = '';
792
768
  let cacheSummary = null;
793
769
  let config = null;
794
770
  let configLogs = [];
795
771
  const cmdByPid = new Map();
796
772
  let runtimeByPid = new Map();
797
773
  let selectedRuntime = null;
798
-
799
-
800
- const pidFiles = await listPidFilePaths();
801
- for (const pidFile of pidFiles) {
802
- const pid = await readPidFromFile(pidFile);
803
- if (!Number.isInteger(pid)) continue;
804
-
805
- try {
806
- process.kill(pid, 0);
807
- pids.push(pid);
808
- } catch (_e) {
809
-
810
- await fs.unlink(pidFile).catch(() => {});
811
- }
812
- }
813
-
814
-
815
- if (pids.length === 0) {
816
- try {
817
- const myPid = process.pid;
818
- if (process.platform === 'win32') {
819
- const { stdout } = await execPromise(
820
- `powershell -NoProfile -Command "Get-CimInstance Win32_Process | Where-Object { $_.Name -match '^node(\\\\.exe)?$' -and $_.CommandLine -and ($_.CommandLine -like '*heuristic-mcp\\\\index.js*' -or $_.CommandLine -like '*heuristic-mcp/index.js*') } | Select-Object -ExpandProperty ProcessId"`
821
- );
822
- const winPids = stdout
823
- .trim()
824
- .split(/\s+/)
825
- .filter((p) => p && !isNaN(p));
826
-
827
-
828
- if (winPids.length > 0) {
829
- const { stdout: cmdOut } = await execPromise(
830
- `powershell -NoProfile -Command "Get-CimInstance Win32_Process | Where-Object { $_.ProcessId -in @(${winPids.join(',')}) } | Select-Object ProcessId, CommandLine"`
831
- );
832
- const lines = cmdOut.trim().split(/\r?\n/);
833
- for (const line of lines) {
834
- const trimmed = line.trim();
835
- if (!trimmed || trimmed.startsWith('ProcessId')) continue;
836
- const match = trimmed.match(/^(\d+)\s+(.*)$/);
837
- if (match) {
838
- const pid = parseInt(match[1], 10);
839
- const cmd = match[2];
840
- if (
841
- cmd.includes('embedding-worker') ||
842
- cmd.includes('embedding-process') ||
843
- cmd.includes('json-worker')
844
- ) {
845
- continue;
846
- }
847
- if (pid && pid !== myPid) {
848
- if (!pids.includes(pid)) pids.push(pid);
849
- }
850
- }
851
- }
852
- }
853
- } else {
854
- const { stdout } = await execPromise('ps aux');
855
- const lines = stdout.split('\n');
856
- const validPids = [];
857
-
858
- for (const line of lines) {
859
- if (line.includes('heuristic-mcp/index.js') || line.includes('heuristic-mcp')) {
860
-
861
- if (
862
- line.includes('embedding-worker') ||
863
- line.includes('embedding-process') ||
864
- line.includes('json-worker')
865
- ) {
866
- continue;
867
- }
868
- const parts = line.trim().split(/\s+/);
869
- const pid = parseInt(parts[1], 10);
870
- if (pid && !isNaN(pid) && pid !== myPid && !line.includes(' grep ')) {
871
- validPids.push(pid);
872
- }
873
- }
874
- }
875
-
876
- for (const p of validPids) {
877
- if (!pids.includes(p)) pids.push(p);
878
- }
879
- }
880
- } catch (_e) {
881
-
882
- }
883
- }
884
-
885
- if (!cacheOnly) {
886
-
887
- console.info('');
888
- if (pids.length > 0) {
889
- console.info(`[Lifecycle] 🟢 Server is RUNNING. PID(s): ${pids.join(', ')}`);
890
- } else {
891
- console.info('[Lifecycle] ⚪ Server is STOPPED.');
892
- }
774
+
775
+ const pidFiles = await listPidFilePaths();
776
+ for (const pidFile of pidFiles) {
777
+ const pid = await readPidFromFile(pidFile);
778
+ if (!Number.isInteger(pid)) continue;
779
+
780
+ try {
781
+ process.kill(pid, 0);
782
+ pids.push(pid);
783
+ } catch (_e) {
784
+ await fs.unlink(pidFile).catch(() => {});
785
+ }
786
+ }
787
+
788
+ if (pids.length === 0) {
789
+ try {
790
+ const myPid = process.pid;
791
+ if (process.platform === 'win32') {
792
+ const { stdout } = await execPromise(
793
+ `powershell -NoProfile -Command "Get-CimInstance Win32_Process | Where-Object { $_.Name -match '^node(\\\\.exe)?$' -and $_.CommandLine -and ($_.CommandLine -like '*heuristic-mcp\\\\index.js*' -or $_.CommandLine -like '*heuristic-mcp/index.js*') } | Select-Object -ExpandProperty ProcessId"`
794
+ );
795
+ const winPids = stdout
796
+ .trim()
797
+ .split(/\s+/)
798
+ .filter((p) => p && !isNaN(p));
799
+
800
+ if (winPids.length > 0) {
801
+ const { stdout: cmdOut } = await execPromise(
802
+ `powershell -NoProfile -Command "Get-CimInstance Win32_Process | Where-Object { $_.ProcessId -in @(${winPids.join(',')}) } | Select-Object ProcessId, CommandLine"`
803
+ );
804
+ const lines = cmdOut.trim().split(/\r?\n/);
805
+ for (const line of lines) {
806
+ const trimmed = line.trim();
807
+ if (!trimmed || trimmed.startsWith('ProcessId')) continue;
808
+ const match = trimmed.match(/^(\d+)\s+(.*)$/);
809
+ if (match) {
810
+ const pid = parseInt(match[1], 10);
811
+ const cmd = match[2];
812
+ if (
813
+ cmd.includes('embedding-worker') ||
814
+ cmd.includes('embedding-process') ||
815
+ cmd.includes('json-worker')
816
+ ) {
817
+ continue;
818
+ }
819
+ if (pid && pid !== myPid) {
820
+ if (!pids.includes(pid)) pids.push(pid);
821
+ }
822
+ }
823
+ }
824
+ }
825
+ } else {
826
+ const { stdout } = await execPromise('ps aux');
827
+ const lines = stdout.split('\n');
828
+ const validPids = [];
829
+
830
+ for (const line of lines) {
831
+ if (line.includes('heuristic-mcp/index.js') || line.includes('heuristic-mcp')) {
832
+ if (
833
+ line.includes('embedding-worker') ||
834
+ line.includes('embedding-process') ||
835
+ line.includes('json-worker')
836
+ ) {
837
+ continue;
838
+ }
839
+ const parts = line.trim().split(/\s+/);
840
+ const pid = parseInt(parts[1], 10);
841
+ if (pid && !isNaN(pid) && pid !== myPid && !line.includes(' grep ')) {
842
+ validPids.push(pid);
843
+ }
844
+ }
845
+ }
846
+
847
+ for (const p of validPids) {
848
+ if (!pids.includes(p)) pids.push(p);
849
+ }
850
+ }
851
+ } catch (_e) {}
852
+ }
853
+
854
+ if (!cacheOnly) {
855
+ console.info('');
856
+ if (pids.length > 0) {
857
+ console.info(`[Lifecycle] 🟢 Server is RUNNING. PID(s): ${pids.join(', ')}`);
858
+ } else {
859
+ console.info('[Lifecycle] ⚪ Server is STOPPED.');
860
+ }
893
861
  if (pids.length > 1) {
894
862
  console.info('[Lifecycle] ⚠️ Multiple servers detected; progress may be inconsistent.');
895
863
  }
896
864
  if (pids.length > 0) {
897
865
  try {
898
866
  if (process.platform === 'win32') {
899
- const { stdout } = await execPromise(
900
- `powershell -NoProfile -Command "Get-CimInstance Win32_Process | Where-Object { $_.ProcessId -in @(${pids.join(',')}) } | Select-Object ProcessId, CommandLine"`
901
- );
902
- const lines = stdout.trim().split(/\r?\n/);
903
- for (const line of lines) {
904
- const trimmed = line.trim();
905
- if (!trimmed || trimmed.startsWith('ProcessId')) continue;
906
- const match = trimmed.match(/^(\d+)\s+(.*)$/);
907
- if (match) {
908
- cmdByPid.set(parseInt(match[1], 10), match[2]);
909
- }
910
- }
911
- } else {
912
- const { stdout } = await execPromise(`ps -o pid=,command= -p ${pids.join(',')}`);
913
- const lines = stdout.trim().split(/\r?\n/);
914
- for (const line of lines) {
915
- const match = line.trim().match(/^(\d+)\s+(.*)$/);
916
- if (match) {
917
- cmdByPid.set(parseInt(match[1], 10), match[2]);
918
- }
919
- }
920
- }
921
- } catch (_e) {
922
-
923
- }
867
+ const { stdout } = await execPromise(
868
+ `powershell -NoProfile -Command "Get-CimInstance Win32_Process | Where-Object { $_.ProcessId -in @(${pids.join(',')}) } | Select-Object ProcessId, CommandLine"`
869
+ );
870
+ const lines = stdout.trim().split(/\r?\n/);
871
+ for (const line of lines) {
872
+ const trimmed = line.trim();
873
+ if (!trimmed || trimmed.startsWith('ProcessId')) continue;
874
+ const match = trimmed.match(/^(\d+)\s+(.*)$/);
875
+ if (match) {
876
+ cmdByPid.set(parseInt(match[1], 10), match[2]);
877
+ }
878
+ }
879
+ } else {
880
+ const { stdout } = await execPromise(`ps -o pid=,command= -p ${pids.join(',')}`);
881
+ const lines = stdout.trim().split(/\r?\n/);
882
+ for (const line of lines) {
883
+ const match = line.trim().match(/^(\d+)\s+(.*)$/);
884
+ if (match) {
885
+ cmdByPid.set(parseInt(match[1], 10), match[2]);
886
+ }
887
+ }
888
+ }
889
+ } catch (_e) {}
924
890
  if (cmdByPid.size > 0) {
925
- console.info('[Lifecycle] Active command lines:');
926
- for (const pid of pids) {
927
- const cmd = cmdByPid.get(pid);
928
- if (cmd) {
929
- console.info(` ${pid}: ${cmd}`);
930
- }
891
+ console.info('[Lifecycle] Active command lines:');
892
+ for (const pid of pids) {
893
+ const cmd = cmdByPid.get(pid);
894
+ if (cmd) {
895
+ console.info(` ${pid}: ${cmd}`);
896
+ }
931
897
  }
932
898
  }
933
899
 
@@ -947,8 +913,8 @@ export async function status({ fix = false, cacheOnly = false, workspaceDir = nu
947
913
  requestedWorkspace: workspaceDir,
948
914
  });
949
915
  }
950
- console.info('');
951
- }
916
+ console.info('');
917
+ }
952
918
 
953
919
  if (!cacheOnly) {
954
920
  try {
@@ -960,36 +926,37 @@ export async function status({ fix = false, cacheOnly = false, workspaceDir = nu
960
926
  try {
961
927
  await fs.access(logPath);
962
928
  logStatus = '(exists)';
963
- } catch {
964
- logStatus = '(not found)';
965
- }
929
+ } catch {
930
+ logStatus = '(not found)';
931
+ }
966
932
  const statusCacheDirectory = selectedRuntime?.cacheDirectory || config?.cacheDirectory;
967
933
  if (statusCacheDirectory) {
968
934
  const metaFile = path.join(statusCacheDirectory, 'meta.json');
969
935
  const progressFile = path.join(statusCacheDirectory, 'progress.json');
970
936
  let metaData = null;
971
937
  let progressData = null;
972
- try {
973
- metaData = JSON.parse(await fs.readFile(metaFile, 'utf-8'));
974
- } catch {
975
- metaData = null;
976
- }
977
- try {
978
- progressData = JSON.parse(await fs.readFile(progressFile, 'utf-8'));
979
- } catch {
980
- progressData = null;
981
- }
938
+ try {
939
+ metaData = JSON.parse(await fs.readFile(metaFile, 'utf-8'));
940
+ } catch {
941
+ metaData = null;
942
+ }
943
+ try {
944
+ progressData = JSON.parse(await fs.readFile(progressFile, 'utf-8'));
945
+ } catch {
946
+ progressData = null;
947
+ }
982
948
  cacheSummary = {
983
949
  cacheDir: statusCacheDirectory,
984
950
  hasSnapshot: !!metaData,
985
951
  snapshotTime: metaData?.lastSaveTime || null,
986
- progress: progressData && typeof progressData.progress === 'number' ? progressData : null,
952
+ progress:
953
+ progressData && typeof progressData.progress === 'number' ? progressData : null,
987
954
  };
988
955
  }
989
- } catch {
990
- logPath = 'unknown';
991
- }
992
-
956
+ } catch {
957
+ logPath = 'unknown';
958
+ }
959
+
993
960
  const displayWorkspace = selectedRuntime?.workspace || config?.searchDirectory;
994
961
  if (displayWorkspace) {
995
962
  console.info(`[Lifecycle] Workspace: ${displayWorkspace}`);
@@ -1000,52 +967,51 @@ export async function status({ fix = false, cacheOnly = false, workspaceDir = nu
1000
967
  );
1001
968
  }
1002
969
  console.info(` Log file: ${logPath} ${logStatus}`.trimEnd());
1003
- if (cacheSummary?.cacheDir) {
1004
- const snapshotLabel = cacheSummary.hasSnapshot ? 'available' : 'none';
1005
- console.info(`[Cache] Snapshot: ${snapshotLabel}`);
1006
- if (cacheSummary.snapshotTime) {
1007
- console.info(
1008
- `[Cache] Snapshot saved: ${formatDateTime(cacheSummary.snapshotTime) || cacheSummary.snapshotTime}`
1009
- );
1010
- }
1011
- if (cacheSummary.progress) {
1012
- const progress = cacheSummary.progress;
1013
- console.info(
1014
- `[Cache] Progress: ${progress.progress}/${progress.total} (${progress.message || 'n/a'})`
1015
- );
1016
- } else {
1017
- console.info('[Cache] Progress: idle');
1018
- }
1019
- }
1020
- console.info('');
1021
-
1022
- if (configLogs.length > 0) {
1023
- for (const line of configLogs) {
1024
- console.info(line);
1025
- }
1026
- console.info('');
1027
- }
1028
- }
1029
-
1030
- if (cacheOnly) {
1031
-
1032
- console.info('[Status] Inspecting cache status...\n');
1033
-
1034
- if (fix) {
1035
- console.info('[Status] Fixing stale caches...\n');
1036
- await clearStaleCaches();
1037
- }
1038
-
1039
- const cacheDirs = await fs.readdir(globalCacheRoot).catch(() => []);
1040
-
1041
- if (cacheDirs.length === 0) {
1042
- console.info('[Status] No cache directories found.');
1043
- console.info(`[Status] Expected location: ${globalCacheRoot}`);
1044
- } else {
1045
- console.info(
1046
- `[Status] Found ${cacheDirs.length} cache director${cacheDirs.length === 1 ? 'y' : 'ies'} in ${globalCacheRoot}`
1047
- );
1048
-
970
+ if (cacheSummary?.cacheDir) {
971
+ const snapshotLabel = cacheSummary.hasSnapshot ? 'available' : 'none';
972
+ console.info(`[Cache] Snapshot: ${snapshotLabel}`);
973
+ if (cacheSummary.snapshotTime) {
974
+ console.info(
975
+ `[Cache] Snapshot saved: ${formatDateTime(cacheSummary.snapshotTime) || cacheSummary.snapshotTime}`
976
+ );
977
+ }
978
+ if (cacheSummary.progress) {
979
+ const progress = cacheSummary.progress;
980
+ console.info(
981
+ `[Cache] Progress: ${progress.progress}/${progress.total} (${progress.message || 'n/a'})`
982
+ );
983
+ } else {
984
+ console.info('[Cache] Progress: idle');
985
+ }
986
+ }
987
+ console.info('');
988
+
989
+ if (configLogs.length > 0) {
990
+ for (const line of configLogs) {
991
+ console.info(line);
992
+ }
993
+ console.info('');
994
+ }
995
+ }
996
+
997
+ if (cacheOnly) {
998
+ console.info('[Status] Inspecting cache status...\n');
999
+
1000
+ if (fix) {
1001
+ console.info('[Status] Fixing stale caches...\n');
1002
+ await clearStaleCaches();
1003
+ }
1004
+
1005
+ const cacheDirs = await fs.readdir(globalCacheRoot).catch(() => []);
1006
+
1007
+ if (cacheDirs.length === 0) {
1008
+ console.info('[Status] No cache directories found.');
1009
+ console.info(`[Status] Expected location: ${globalCacheRoot}`);
1010
+ } else {
1011
+ console.info(
1012
+ `[Status] Found ${cacheDirs.length} cache director${cacheDirs.length === 1 ? 'y' : 'ies'} in ${globalCacheRoot}`
1013
+ );
1014
+
1049
1015
  for (const dir of cacheDirs) {
1050
1016
  const cacheDir = path.join(globalCacheRoot, dir);
1051
1017
  const metaFile = path.join(cacheDir, 'meta.json');
@@ -1053,9 +1019,7 @@ export async function status({ fix = false, cacheOnly = false, workspaceDir = nu
1053
1019
  let progressData = null;
1054
1020
  try {
1055
1021
  progressData = JSON.parse(await fs.readFile(progressFile, 'utf-8'));
1056
- } catch {
1057
-
1058
- }
1022
+ } catch {}
1059
1023
  const hasNumericProgress = progressData && typeof progressData.progress === 'number';
1060
1024
  const isProgressIncomplete =
1061
1025
  !!hasNumericProgress &&
@@ -1068,70 +1032,68 @@ export async function status({ fix = false, cacheOnly = false, workspaceDir = nu
1068
1032
  console.info(`📁 Cache: ${dir}`);
1069
1033
  console.info(` Path: ${cacheDir}`);
1070
1034
 
1071
- let metaData = null;
1072
- try {
1073
- metaData = JSON.parse(await fs.readFile(metaFile, 'utf-8'));
1074
-
1075
- console.info(` Status: ✅ Valid cache`);
1076
- console.info(` Workspace: ${metaData.workspace || 'Unknown'}`);
1077
- console.info(` Files indexed: ${metaData.filesIndexed ?? 'N/A'}`);
1078
- console.info(` Chunks stored: ${metaData.chunksStored ?? 'N/A'}`);
1079
-
1080
- if (Number.isFinite(metaData.lastDiscoveredFiles)) {
1081
- console.info(` Files discovered (last run): ${metaData.lastDiscoveredFiles}`);
1082
- }
1083
- if (Number.isFinite(metaData.lastFilesProcessed)) {
1084
- console.info(` Files processed (last run): ${metaData.lastFilesProcessed}`);
1085
- }
1086
- if (
1087
- Number.isFinite(metaData.lastDiscoveredFiles) &&
1088
- Number.isFinite(metaData.lastFilesProcessed)
1089
- ) {
1090
- const delta = metaData.lastDiscoveredFiles - metaData.lastFilesProcessed;
1091
- console.info(` Discovery delta (last run): ${delta >= 0 ? delta : 0}`);
1092
- }
1093
-
1094
- if (metaData.lastSaveTime) {
1095
- const saveDate = new Date(metaData.lastSaveTime);
1096
- const ageMs = now - saveDate;
1097
- const ageHours = Math.floor(ageMs / (1000 * 60 * 60));
1098
- const ageMins = Math.floor((ageMs % (1000 * 60 * 60)) / (1000 * 60));
1099
- console.info(
1100
- ` Cached snapshot saved: ${formatDateTime(saveDate)} (${ageHours}h ${ageMins}m ago)`
1101
- );
1102
- const ageLabel = formatDurationMs(ageMs);
1103
- if (ageLabel) {
1104
- console.info(` Cached snapshot age: ${ageLabel}`);
1105
- }
1106
- console.info(` Initial index complete at: ${formatDateTime(saveDate)}`);
1107
- }
1108
- if (metaData.lastIndexStartedAt) {
1109
- console.info(` Last index started: ${formatDateTime(metaData.lastIndexStartedAt)}`);
1110
- }
1111
- if (metaData.lastIndexEndedAt) {
1112
- console.info(` Last index ended: ${formatDateTime(metaData.lastIndexEndedAt)}`);
1113
- }
1114
- if (Number.isFinite(metaData.indexDurationMs)) {
1115
- const duration = formatDurationMs(metaData.indexDurationMs);
1116
- if (duration) {
1117
- console.info(` Last full index duration: ${duration}`);
1118
- }
1119
- }
1120
- if (metaData.lastIndexMode) {
1121
- console.info(` Last index mode: ${String(metaData.lastIndexMode)}`);
1122
- }
1123
- if (Number.isFinite(metaData.lastBatchSize)) {
1124
- console.info(` Last batch size: ${metaData.lastBatchSize}`);
1125
- }
1126
- if (Number.isFinite(metaData.lastWorkerThreads)) {
1127
- console.info(` Last worker threads: ${metaData.lastWorkerThreads}`);
1128
- }
1035
+ let metaData = null;
1036
+ try {
1037
+ metaData = JSON.parse(await fs.readFile(metaFile, 'utf-8'));
1038
+
1039
+ console.info(` Status: ✅ Valid cache`);
1040
+ console.info(` Workspace: ${metaData.workspace || 'Unknown'}`);
1041
+ console.info(` Files indexed: ${metaData.filesIndexed ?? 'N/A'}`);
1042
+ console.info(` Chunks stored: ${metaData.chunksStored ?? 'N/A'}`);
1043
+
1044
+ if (Number.isFinite(metaData.lastDiscoveredFiles)) {
1045
+ console.info(` Files discovered (last run): ${metaData.lastDiscoveredFiles}`);
1046
+ }
1047
+ if (Number.isFinite(metaData.lastFilesProcessed)) {
1048
+ console.info(` Files processed (last run): ${metaData.lastFilesProcessed}`);
1049
+ }
1050
+ if (
1051
+ Number.isFinite(metaData.lastDiscoveredFiles) &&
1052
+ Number.isFinite(metaData.lastFilesProcessed)
1053
+ ) {
1054
+ const delta = metaData.lastDiscoveredFiles - metaData.lastFilesProcessed;
1055
+ console.info(` Discovery delta (last run): ${delta >= 0 ? delta : 0}`);
1056
+ }
1057
+
1058
+ if (metaData.lastSaveTime) {
1059
+ const saveDate = new Date(metaData.lastSaveTime);
1060
+ const ageMs = now - saveDate;
1061
+ const ageHours = Math.floor(ageMs / (1000 * 60 * 60));
1062
+ const ageMins = Math.floor((ageMs % (1000 * 60 * 60)) / (1000 * 60));
1063
+ console.info(
1064
+ ` Cached snapshot saved: ${formatDateTime(saveDate)} (${ageHours}h ${ageMins}m ago)`
1065
+ );
1066
+ const ageLabel = formatDurationMs(ageMs);
1067
+ if (ageLabel) {
1068
+ console.info(` Cached snapshot age: ${ageLabel}`);
1069
+ }
1070
+ console.info(` Initial index complete at: ${formatDateTime(saveDate)}`);
1071
+ }
1072
+ if (metaData.lastIndexStartedAt) {
1073
+ console.info(` Last index started: ${formatDateTime(metaData.lastIndexStartedAt)}`);
1074
+ }
1075
+ if (metaData.lastIndexEndedAt) {
1076
+ console.info(` Last index ended: ${formatDateTime(metaData.lastIndexEndedAt)}`);
1077
+ }
1078
+ if (Number.isFinite(metaData.indexDurationMs)) {
1079
+ const duration = formatDurationMs(metaData.indexDurationMs);
1080
+ if (duration) {
1081
+ console.info(` Last full index duration: ${duration}`);
1082
+ }
1083
+ }
1084
+ if (metaData.lastIndexMode) {
1085
+ console.info(` Last index mode: ${String(metaData.lastIndexMode)}`);
1086
+ }
1087
+ if (Number.isFinite(metaData.lastBatchSize)) {
1088
+ console.info(` Last batch size: ${metaData.lastBatchSize}`);
1089
+ }
1090
+ if (Number.isFinite(metaData.lastWorkerThreads)) {
1091
+ console.info(` Last worker threads: ${metaData.lastWorkerThreads}`);
1092
+ }
1129
1093
  try {
1130
1094
  const dirStats = await fs.stat(cacheDir);
1131
1095
  console.info(` Cache dir last write: ${formatDateTime(dirStats.mtime)}`);
1132
- } catch {
1133
-
1134
- }
1096
+ } catch {}
1135
1097
 
1136
1098
  const progressAfterSnapshot =
1137
1099
  hasNumericProgress &&
@@ -1142,7 +1104,6 @@ export async function status({ fix = false, cacheOnly = false, workspaceDir = nu
1142
1104
  hasNumericProgress && (isProgressIncomplete || progressAfterSnapshot)
1143
1105
  );
1144
1106
 
1145
-
1146
1107
  if (metaData.filesIndexed && metaData.filesIndexed > 0) {
1147
1108
  if (isIncrementalUpdateActive) {
1148
1109
  console.info(` Cached snapshot: ✅ COMPLETE (${metaData.filesIndexed} files)`);
@@ -1170,20 +1131,20 @@ export async function status({ fix = false, cacheOnly = false, workspaceDir = nu
1170
1131
  if (err.code === 'ENOENT') {
1171
1132
  try {
1172
1133
  const stats = await fs.stat(cacheDir);
1173
- const ageMs = new Date() - stats.mtime;
1174
- if (ageMs < 10 * 60 * 1000) {
1175
- console.info(` Status: ⏳ Initializing / Indexing in progress...`);
1176
- console.info(` (Metadata file has not been written yet using ID ${dir})`);
1177
- console.info(' Initial index: ⏳ IN PROGRESS');
1178
- } else {
1179
- console.info(` Status: ⚠️ Incomplete cache (stale)`);
1180
- }
1181
- console.info(` Cache dir last write: ${stats.mtime.toLocaleString()}`);
1182
- } catch {
1183
- console.info(` Status: ❌ Invalid cache directory`);
1184
- }
1185
- } else {
1186
- console.info(` Status: ❌ Invalid or corrupted (${err.message})`);
1134
+ const ageMs = new Date() - stats.mtime;
1135
+ if (ageMs < 10 * 60 * 1000) {
1136
+ console.info(` Status: ⏳ Initializing / Indexing in progress...`);
1137
+ console.info(` (Metadata file has not been written yet using ID ${dir})`);
1138
+ console.info(' Initial index: ⏳ IN PROGRESS');
1139
+ } else {
1140
+ console.info(` Status: ⚠️ Incomplete cache (stale)`);
1141
+ }
1142
+ console.info(` Cache dir last write: ${stats.mtime.toLocaleString()}`);
1143
+ } catch {
1144
+ console.info(` Status: ❌ Invalid cache directory`);
1145
+ }
1146
+ } else {
1147
+ console.info(` Status: ❌ Invalid or corrupted (${err.message})`);
1187
1148
  }
1188
1149
  }
1189
1150
 
@@ -1191,47 +1152,47 @@ export async function status({ fix = false, cacheOnly = false, workspaceDir = nu
1191
1152
  const updatedAt = progressData.updatedAt
1192
1153
  ? formatDateTime(progressData.updatedAt)
1193
1154
  : 'Unknown';
1194
- const progressLabel = metaData
1195
- ? 'Incremental update (post-snapshot)'
1196
- : 'Initial index progress';
1197
- console.info(
1198
- ` ${progressLabel}: ${progressData.progress}/${progressData.total} (${progressData.message || 'n/a'})`
1199
- );
1200
- console.info(` Progress updated: ${updatedAt}`);
1201
-
1202
- if (progressData.updatedAt) {
1203
- const updatedDate = new Date(progressData.updatedAt);
1204
- const ageMs = now - updatedDate;
1205
- const staleMs = 5 * 60 * 1000;
1206
- const ageLabel = formatDurationMs(ageMs);
1207
- if (ageLabel) {
1208
- console.info(` Progress age: ${ageLabel}`);
1209
- }
1210
- if (Number.isFinite(ageMs) && ageMs > staleMs) {
1211
- const staleLabel = formatDurationMs(ageMs);
1212
- console.info(` Progress stale: last update ${staleLabel} ago`);
1213
- }
1214
- }
1215
-
1216
- if (progressData.updatedAt && metaData?.lastSaveTime) {
1217
- const updatedDate = new Date(progressData.updatedAt);
1218
- const saveDate = new Date(metaData.lastSaveTime);
1219
- if (updatedDate > saveDate) {
1220
- console.info(' Note: Incremental update in progress; cached snapshot may lag.');
1221
- }
1222
- }
1223
- if (progressData.indexMode) {
1224
- console.info(` Current index mode: ${String(progressData.indexMode)}`);
1225
- }
1155
+ const progressLabel = metaData
1156
+ ? 'Incremental update (post-snapshot)'
1157
+ : 'Initial index progress';
1158
+ console.info(
1159
+ ` ${progressLabel}: ${progressData.progress}/${progressData.total} (${progressData.message || 'n/a'})`
1160
+ );
1161
+ console.info(` Progress updated: ${updatedAt}`);
1162
+
1163
+ if (progressData.updatedAt) {
1164
+ const updatedDate = new Date(progressData.updatedAt);
1165
+ const ageMs = now - updatedDate;
1166
+ const staleMs = 5 * 60 * 1000;
1167
+ const ageLabel = formatDurationMs(ageMs);
1168
+ if (ageLabel) {
1169
+ console.info(` Progress age: ${ageLabel}`);
1170
+ }
1171
+ if (Number.isFinite(ageMs) && ageMs > staleMs) {
1172
+ const staleLabel = formatDurationMs(ageMs);
1173
+ console.info(` Progress stale: last update ${staleLabel} ago`);
1174
+ }
1175
+ }
1176
+
1177
+ if (progressData.updatedAt && metaData?.lastSaveTime) {
1178
+ const updatedDate = new Date(progressData.updatedAt);
1179
+ const saveDate = new Date(metaData.lastSaveTime);
1180
+ if (updatedDate > saveDate) {
1181
+ console.info(' Note: Incremental update in progress; cached snapshot may lag.');
1182
+ }
1183
+ }
1184
+ if (progressData.indexMode) {
1185
+ console.info(` Current index mode: ${String(progressData.indexMode)}`);
1186
+ }
1226
1187
  if (
1227
1188
  progressData.workerCircuitOpen &&
1228
1189
  Number.isFinite(progressData.workersDisabledUntil)
1229
1190
  ) {
1230
- const remainingMs = progressData.workersDisabledUntil - Date.now();
1231
- const remainingLabel = formatDurationMs(Math.max(0, remainingMs));
1232
- console.info(` Workers paused: ${remainingLabel || '0s'} remaining`);
1233
- console.info(
1234
- ` Workers disabled until: ${formatDateTime(progressData.workersDisabledUntil)}`
1191
+ const remainingMs = progressData.workersDisabledUntil - Date.now();
1192
+ const remainingLabel = formatDurationMs(Math.max(0, remainingMs));
1193
+ console.info(` Workers paused: ${remainingLabel || '0s'} remaining`);
1194
+ console.info(
1195
+ ` Workers disabled until: ${formatDateTime(progressData.workersDisabledUntil)}`
1235
1196
  );
1236
1197
  }
1237
1198
  } else {
@@ -1302,156 +1263,143 @@ export async function status({ fix = false, cacheOnly = false, workspaceDir = nu
1302
1263
  }
1303
1264
 
1304
1265
  if (metaData && isProgressIncomplete) {
1305
- console.info(' Indexing state: Cached snapshot available; incremental update running.');
1266
+ console.info(
1267
+ ' Indexing state: Cached snapshot available; incremental update running.'
1268
+ );
1306
1269
  } else if (metaData) {
1307
1270
  console.info(' Indexing state: Cached snapshot available; idle.');
1308
1271
  } else if (progressData && typeof progressData.progress === 'number') {
1309
- console.info(' Indexing state: Initial index in progress; no cached snapshot yet.');
1310
- } else {
1311
- console.info(' Indexing state: No cached snapshot; idle.');
1312
- }
1313
- }
1314
- console.info(`${'─'.repeat(60)}`);
1315
- }
1316
- } else {
1317
- if (fix) {
1318
- const results = await clearStaleCaches();
1319
- if (results.removed > 0) {
1320
- console.info(
1321
- `[Status] Cache cleanup removed ${results.removed} stale cache${results.removed === 1 ? '' : 's'}`
1322
- );
1323
- }
1324
- }
1325
- const cacheDirs = await fs.readdir(globalCacheRoot).catch(() => null);
1326
- if (Array.isArray(cacheDirs)) {
1327
- console.info(
1328
- `[Status] Cache: ${cacheDirs.length} director${cacheDirs.length === 1 ? 'y' : 'ies'} in ${globalCacheRoot}`
1329
- );
1330
- } else {
1331
- console.info(`[Status] Cache: ${globalCacheRoot} (not found)`);
1332
- }
1333
- }
1334
-
1335
-
1336
- if (!cacheOnly) {
1337
-
1338
- console.info('\n[Paths] Important locations:');
1339
-
1340
-
1341
- let npmBin = 'unknown';
1342
- try {
1343
- const { stdout } = await execPromise('npm config get prefix');
1344
- npmBin = path.join(stdout.trim(), 'bin');
1345
- } catch {
1346
-
1347
- }
1348
- console.info(` 📦 Global npm bin: ${npmBin}`);
1349
-
1350
-
1351
- const configLocations = [
1352
- {
1353
- name: 'Antigravity',
1354
- path: path.join(getUserHomeDir(), '.gemini', 'antigravity', 'mcp_config.json'),
1355
- },
1356
- {
1357
- name: 'Codex',
1358
- path: path.join(getUserHomeDir(), '.codex', 'config.toml'),
1359
- },
1360
- {
1361
- name: 'Claude Desktop',
1362
- path: path.join(getUserHomeDir(), '.config', 'Claude', 'claude_desktop_config.json'),
1363
- },
1364
- {
1365
- name: 'VS Code',
1366
- path: path.join(getUserHomeDir(), '.config', 'Code', 'User', 'mcp.json'),
1367
- },
1368
- {
1369
- name: 'Cursor',
1370
- path: path.join(getUserHomeDir(), '.config', 'Cursor', 'User', 'settings.json'),
1371
- },
1372
- {
1373
- name: 'Cursor Global',
1374
- path: path.join(getUserHomeDir(), '.cursor', 'mcp.json'),
1375
- },
1376
- {
1377
- name: 'Windsurf',
1378
- path: path.join(getUserHomeDir(), '.codeium', 'windsurf', 'mcp_config.json'),
1379
- },
1380
- {
1381
- name: 'Warp',
1382
- path: path.join(getUserHomeDir(), '.warp', 'mcp_settings.json'),
1383
- },
1384
- ];
1385
-
1386
-
1387
- if (process.platform === 'darwin') {
1388
- configLocations[2].path = path.join(
1389
- os.homedir(),
1390
-
1391
-
1392
- 'Library',
1393
- 'Application Support',
1394
- 'Claude',
1395
- 'claude_desktop_config.json'
1396
- );
1397
- configLocations[3].path = path.join(
1398
- os.homedir(),
1399
- 'Library',
1400
- 'Application Support',
1401
- 'Code',
1402
- 'User',
1403
- 'mcp.json'
1404
- );
1405
- configLocations[4].path = path.join(
1406
- os.homedir(),
1407
- 'Library',
1408
- 'Application Support',
1409
- 'Cursor',
1410
- 'User',
1411
- 'settings.json'
1412
- );
1413
- } else if (process.platform === 'win32') {
1414
- configLocations[2].path = path.join(
1415
- process.env.APPDATA || '',
1416
- 'Claude',
1417
- 'claude_desktop_config.json'
1418
- );
1419
- configLocations[3].path = path.join(
1420
- process.env.APPDATA || '',
1421
- 'Code',
1422
- 'User',
1423
- 'mcp.json'
1424
- );
1425
- configLocations[4].path = path.join(
1426
- process.env.APPDATA || '',
1427
- 'Cursor',
1428
- 'User',
1429
- 'settings.json'
1430
- );
1431
- configLocations.push({
1432
- name: 'Warp AppData',
1433
- path: path.join(process.env.APPDATA || '', 'Warp', 'mcp_settings.json'),
1434
- });
1435
- }
1436
-
1437
- console.info(' ⚙️ MCP configs:');
1438
- for (const loc of configLocations) {
1439
- let status = '(not found)';
1440
- try {
1441
- await fs.access(loc.path);
1442
- status = '(exists)';
1443
- } catch {
1444
-
1445
- }
1446
- console.info(` - ${loc.name}: ${loc.path} ${status}`);
1447
- }
1448
-
1449
- console.info(` 📝 Log file: ${logPath} ${logStatus}`.trimEnd());
1450
- console.info(` 💾 Cache root: ${globalCacheRoot}`);
1451
- console.info(` 📁 Current dir: ${process.cwd()}`);
1452
- console.info('');
1453
- }
1454
- } catch (error) {
1455
- console.error(`[Lifecycle] Failed to check status: ${error.message}`);
1456
- }
1457
- }
1272
+ console.info(' Indexing state: Initial index in progress; no cached snapshot yet.');
1273
+ } else {
1274
+ console.info(' Indexing state: No cached snapshot; idle.');
1275
+ }
1276
+ }
1277
+ console.info(`${'─'.repeat(60)}`);
1278
+ }
1279
+ } else {
1280
+ if (fix) {
1281
+ const results = await clearStaleCaches();
1282
+ if (results.removed > 0) {
1283
+ console.info(
1284
+ `[Status] Cache cleanup removed ${results.removed} stale cache${results.removed === 1 ? '' : 's'}`
1285
+ );
1286
+ }
1287
+ }
1288
+ const cacheDirs = await fs.readdir(globalCacheRoot).catch(() => null);
1289
+ if (Array.isArray(cacheDirs)) {
1290
+ console.info(
1291
+ `[Status] Cache: ${cacheDirs.length} director${cacheDirs.length === 1 ? 'y' : 'ies'} in ${globalCacheRoot}`
1292
+ );
1293
+ } else {
1294
+ console.info(`[Status] Cache: ${globalCacheRoot} (not found)`);
1295
+ }
1296
+ }
1297
+
1298
+ if (!cacheOnly) {
1299
+ console.info('\n[Paths] Important locations:');
1300
+
1301
+ let npmBin = 'unknown';
1302
+ try {
1303
+ const { stdout } = await execPromise('npm config get prefix');
1304
+ npmBin = path.join(stdout.trim(), 'bin');
1305
+ } catch {}
1306
+ console.info(` 📦 Global npm bin: ${npmBin}`);
1307
+
1308
+ const configLocations = [
1309
+ {
1310
+ name: 'Antigravity',
1311
+ path: path.join(getUserHomeDir(), '.gemini', 'antigravity', 'mcp_config.json'),
1312
+ },
1313
+ {
1314
+ name: 'Codex',
1315
+ path: path.join(getUserHomeDir(), '.codex', 'config.toml'),
1316
+ },
1317
+ {
1318
+ name: 'Claude Desktop',
1319
+ path: path.join(getUserHomeDir(), '.config', 'Claude', 'claude_desktop_config.json'),
1320
+ },
1321
+ {
1322
+ name: 'VS Code',
1323
+ path: path.join(getUserHomeDir(), '.config', 'Code', 'User', 'mcp.json'),
1324
+ },
1325
+ {
1326
+ name: 'Cursor',
1327
+ path: path.join(getUserHomeDir(), '.config', 'Cursor', 'User', 'settings.json'),
1328
+ },
1329
+ {
1330
+ name: 'Cursor Global',
1331
+ path: path.join(getUserHomeDir(), '.cursor', 'mcp.json'),
1332
+ },
1333
+ {
1334
+ name: 'Windsurf',
1335
+ path: path.join(getUserHomeDir(), '.codeium', 'windsurf', 'mcp_config.json'),
1336
+ },
1337
+ {
1338
+ name: 'Warp',
1339
+ path: path.join(getUserHomeDir(), '.warp', 'mcp_settings.json'),
1340
+ },
1341
+ ];
1342
+
1343
+ if (process.platform === 'darwin') {
1344
+ configLocations[2].path = path.join(
1345
+ os.homedir(),
1346
+
1347
+ 'Library',
1348
+ 'Application Support',
1349
+ 'Claude',
1350
+ 'claude_desktop_config.json'
1351
+ );
1352
+ configLocations[3].path = path.join(
1353
+ os.homedir(),
1354
+ 'Library',
1355
+ 'Application Support',
1356
+ 'Code',
1357
+ 'User',
1358
+ 'mcp.json'
1359
+ );
1360
+ configLocations[4].path = path.join(
1361
+ os.homedir(),
1362
+ 'Library',
1363
+ 'Application Support',
1364
+ 'Cursor',
1365
+ 'User',
1366
+ 'settings.json'
1367
+ );
1368
+ } else if (process.platform === 'win32') {
1369
+ configLocations[2].path = path.join(
1370
+ process.env.APPDATA || '',
1371
+ 'Claude',
1372
+ 'claude_desktop_config.json'
1373
+ );
1374
+ configLocations[3].path = path.join(process.env.APPDATA || '', 'Code', 'User', 'mcp.json');
1375
+ configLocations[4].path = path.join(
1376
+ process.env.APPDATA || '',
1377
+ 'Cursor',
1378
+ 'User',
1379
+ 'settings.json'
1380
+ );
1381
+ configLocations.push({
1382
+ name: 'Warp AppData',
1383
+ path: path.join(process.env.APPDATA || '', 'Warp', 'mcp_settings.json'),
1384
+ });
1385
+ }
1386
+
1387
+ console.info(' ⚙️ MCP configs:');
1388
+ for (const loc of configLocations) {
1389
+ let status = '(not found)';
1390
+ try {
1391
+ await fs.access(loc.path);
1392
+ status = '(exists)';
1393
+ } catch {}
1394
+ console.info(` - ${loc.name}: ${loc.path} ${status}`);
1395
+ }
1396
+
1397
+ console.info(` 📝 Log file: ${logPath} ${logStatus}`.trimEnd());
1398
+ console.info(` 💾 Cache root: ${globalCacheRoot}`);
1399
+ console.info(` 📁 Current dir: ${process.cwd()}`);
1400
+ console.info('');
1401
+ }
1402
+ } catch (error) {
1403
+ console.error(`[Lifecycle] Failed to check status: ${error.message}`);
1404
+ }
1405
+ }