@softerist/heuristic-mcp 3.0.17 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -54,13 +54,13 @@ async function readPidFromFile(filePath) {
54
54
  const pid = Number(parsed?.pid);
55
55
  if (Number.isInteger(pid)) return pid;
56
56
  } catch {
57
- // fall through
57
+
58
58
  }
59
59
  }
60
60
  const pid = parseInt(trimmed, 10);
61
61
  if (!Number.isNaN(pid)) return pid;
62
62
  } catch {
63
- // ignore missing/invalid pid file
63
+
64
64
  }
65
65
  return null;
66
66
  }
@@ -75,7 +75,7 @@ export async function stop() {
75
75
  const manualPid = process.env.HEURISTIC_MCP_PID;
76
76
 
77
77
  if (platform === 'win32') {
78
- // 1. Try PID files first for reliability (per-workspace)
78
+
79
79
  const pidFiles = await listPidFilePaths();
80
80
  for (const pidFile of pidFiles) {
81
81
  const pid = await readPidFromFile(pidFile);
@@ -85,7 +85,7 @@ export async function stop() {
85
85
  const pidValue = String(pid);
86
86
  if (!pids.includes(pidValue)) pids.push(pidValue);
87
87
  } catch (e) {
88
- // If we lack permission, still attempt to stop by PID.
88
+
89
89
  if (e.code === 'EPERM') {
90
90
  const pidValue = String(pid);
91
91
  if (!pids.includes(pidValue)) pids.push(pidValue);
@@ -95,35 +95,16 @@ export async function stop() {
95
95
  }
96
96
  }
97
97
 
98
- // 2. Fallback to WMIC when CIM access is denied
99
- if (pids.length === 0) {
100
- try {
101
- const { stdout } = await execPromise(
102
- `wmic process where "CommandLine like '%heuristic-mcp%'" get ProcessId /FORMAT:LIST`
103
- );
104
- const matches = stdout.match(/ProcessId=(\d+)/g) || [];
105
- for (const match of matches) {
106
- const pid = match.replace('ProcessId=', '');
107
- if (pid && !isNaN(pid) && parseInt(pid, 10) !== currentPid) {
108
- if (!pids.includes(pid)) pids.push(pid);
109
- }
110
- }
111
- } catch (_wmicErr) {
112
- // ignore secondary failures
113
- }
114
- }
115
-
116
- // 3. Fallback to process list with fuzzier matching (kill all heuristic-mcp instances)
117
98
  try {
118
99
  const { stdout } = await execPromise(
119
- `powershell -NoProfile -Command "Get-CimInstance Win32_Process | Where-Object { $_.CommandLine -and ($_.CommandLine -like '*heuristic-mcp*' -or $_.CommandLine -like '*heuristic-mcp\\\\index.js*' -or $_.CommandLine -like '*heuristic-mcp/index.js*') } | Select-Object -ExpandProperty ProcessId"`
100
+ `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"`
120
101
  );
121
102
  const listPids = stdout
122
103
  .trim()
123
104
  .split(/\s+/)
124
105
  .filter((p) => p && !isNaN(p) && parseInt(p) !== currentPid);
125
106
 
126
- // Retrieve command lines to filter out workers
107
+
127
108
  if (listPids.length > 0) {
128
109
  const { stdout: cmdOut } = await execPromise(
129
110
  `powershell -NoProfile -Command "Get-CimInstance Win32_Process | Where-Object { $_.ProcessId -in @(${listPids.join(',')}) } | Select-Object ProcessId, CommandLine"`
@@ -150,15 +131,15 @@ export async function stop() {
150
131
  }
151
132
  }
152
133
  } catch (_e) {
153
- /* ignore */
134
+
154
135
  }
155
136
  } else {
156
- // Unix: Use pgrep to get all matching PIDs
137
+
157
138
  try {
158
139
  const { stdout } = await execPromise(`pgrep -fl "heuristic-mcp"`);
159
140
  const lines = stdout.trim().split(/\r?\n/);
160
141
 
161
- // Filter out current PID, dead processes, and workers
142
+
162
143
  pids = [];
163
144
  for (const line of lines) {
164
145
  const tokens = line.trim().split(/\s+/).filter(Boolean);
@@ -171,7 +152,7 @@ export async function stop() {
171
152
  const pid = parseInt(candidate, 10);
172
153
  if (!Number.isFinite(pid) || pid === currentPid) continue;
173
154
 
174
- // Exclude workers when command line is present
155
+
175
156
  if (
176
157
  !allNumeric &&
177
158
  (line.includes('embedding-worker') ||
@@ -188,18 +169,18 @@ export async function stop() {
188
169
  pids.push(pidValue);
189
170
  }
190
171
  } catch (_e) {
191
- /* ignore */
172
+
192
173
  }
193
174
  }
194
175
  }
195
176
  } catch (e) {
196
- // pgrep returns code 1 if no processes found, which is fine
177
+
197
178
  if (e.code === 1) pids = [];
198
179
  else throw e;
199
180
  }
200
181
  }
201
182
 
202
- // Manual PID override (best-effort)
183
+
203
184
  if (manualPid) {
204
185
  const parts = String(manualPid)
205
186
  .split(/[,\s]+/)
@@ -221,7 +202,7 @@ export async function stop() {
221
202
  return;
222
203
  }
223
204
 
224
- // Capture command lines before killing (best-effort)
205
+
225
206
  try {
226
207
  if (platform === 'win32') {
227
208
  const { stdout } = await execPromise(
@@ -247,10 +228,10 @@ export async function stop() {
247
228
  }
248
229
  }
249
230
  } catch (_e) {
250
- // ignore command line lookup failures
231
+
251
232
  }
252
233
 
253
- // Kill each process (Windows uses taskkill for compatibility)
234
+
254
235
  let killedCount = 0;
255
236
  const killedPids = [];
256
237
  const failedPids = [];
@@ -262,7 +243,7 @@ export async function stop() {
262
243
  } catch (e) {
263
244
  const message = String(e?.message || '');
264
245
  if (message.includes('not found') || message.includes('not be found')) {
265
- // Process already exited; treat as success.
246
+
266
247
  killedCount++;
267
248
  killedPids.push(pid);
268
249
  continue;
@@ -285,7 +266,7 @@ export async function stop() {
285
266
  killedCount++;
286
267
  killedPids.push(pid);
287
268
  } catch (e) {
288
- // Ignore if process already gone
269
+
289
270
  if (e.code !== 'ESRCH') {
290
271
  failedPids.push(pid);
291
272
  console.warn(`[Lifecycle] Failed to kill PID ${pid}: ${e.message}`);
@@ -325,7 +306,7 @@ export async function stop() {
325
306
 
326
307
  export async function start(filter = null) {
327
308
  console.info('[Lifecycle] Ensuring server is configured...');
328
- // Re-use the registration logic to ensure the config is present and correct
309
+
329
310
  try {
330
311
  const { register } = await import('./register.js');
331
312
  await register(filter);
@@ -379,8 +360,13 @@ async function setMcpServerEnabled(enabled) {
379
360
  continue;
380
361
  }
381
362
 
382
- const updatedEntry = { ...found.entry, disabled: !enabled };
383
- const updatedText = upsertMcpServerEntryInText(raw, target, updatedEntry);
363
+ const updatedEntry = { ...found.entry };
364
+ if (enabled) {
365
+ delete updatedEntry.disabled;
366
+ } else {
367
+ updatedEntry.disabled = true;
368
+ }
369
+ const updatedText = upsertMcpServerEntryInText(raw, target, updatedEntry);
384
370
  if (!updatedText) {
385
371
  console.warn(`[Lifecycle] Failed to update ${name} config (unparseable layout).`);
386
372
  continue;
@@ -533,7 +519,7 @@ async function followFile(filePath, startPosition) {
533
519
  stream.pipe(process.stdout, { end: false });
534
520
  position = stats.size;
535
521
  } catch {
536
- // ignore read errors while watching
522
+
537
523
  }
538
524
  });
539
525
 
@@ -558,13 +544,165 @@ function formatDurationMs(ms) {
558
544
  return `${seconds}s`;
559
545
  }
560
546
 
561
- function formatDateTime(value) {
562
- const date = value instanceof Date ? value : new Date(value);
563
- if (Number.isNaN(date.getTime())) return null;
564
- return `${date.toLocaleString()} (${date.toISOString()})`;
565
- }
566
-
567
- async function captureConsoleOutput(fn) {
547
+ function formatDateTime(value) {
548
+ const date = value instanceof Date ? value : new Date(value);
549
+ if (Number.isNaN(date.getTime())) return null;
550
+ return `${date.toLocaleString()} (${date.toISOString()})`;
551
+ }
552
+
553
+ function parseFileProgressSummary(progressData) {
554
+ const message = String(progressData?.message || '');
555
+ if (!message) return null;
556
+
557
+ const indexedMatch = message.match(/Indexed\s+(\d+)\s*\/\s*(\d+)\s+files/i);
558
+ if (indexedMatch) {
559
+ return {
560
+ indexed: Number(indexedMatch[1]),
561
+ total: Number(indexedMatch[2]),
562
+ };
563
+ }
564
+
565
+ const completeMatch = message.match(/Complete:\s+\d+\s+chunks\s+from\s+(\d+)\s+files/i);
566
+ if (completeMatch) {
567
+ const total = Number(completeMatch[1]);
568
+ return { indexed: total, total };
569
+ }
570
+
571
+ const processingMatch = message.match(/Processing\s+(\d+)\s+changed files/i);
572
+ if (processingMatch) {
573
+ return { indexed: null, total: Number(processingMatch[1]) };
574
+ }
575
+
576
+ return null;
577
+ }
578
+
579
+ function normalizePathForCompare(targetPath) {
580
+ if (!targetPath) return '';
581
+ const resolved = path.resolve(targetPath);
582
+ return process.platform === 'win32' ? resolved.toLowerCase() : resolved;
583
+ }
584
+
585
+ function extractWorkspaceFromCommandLine(commandLine) {
586
+ if (!commandLine || typeof commandLine !== 'string') return null;
587
+
588
+ const regex = /--workspace(?:=|\s+)("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|\S+)/g;
589
+ let match;
590
+ while ((match = regex.exec(commandLine)) !== null) {
591
+ let candidate = match[1] || '';
592
+ if (
593
+ (candidate.startsWith('"') && candidate.endsWith('"')) ||
594
+ (candidate.startsWith("'") && candidate.endsWith("'"))
595
+ ) {
596
+ candidate = candidate.slice(1, -1);
597
+ }
598
+ if (!candidate || candidate.includes('${')) continue;
599
+ return candidate;
600
+ }
601
+
602
+ return null;
603
+ }
604
+
605
+ async function collectRuntimeByPid({ pids, globalCacheRoot }) {
606
+ const pidSet = new Set((Array.isArray(pids) ? pids : []).map((pid) => Number(pid)));
607
+ const runtimeByPid = new Map();
608
+ if (pidSet.size === 0) return runtimeByPid;
609
+
610
+ const cacheDirs = await fs.readdir(globalCacheRoot).catch(() => []);
611
+ if (!Array.isArray(cacheDirs)) return runtimeByPid;
612
+
613
+ for (const dir of cacheDirs) {
614
+ const cacheDirectory = path.join(globalCacheRoot, dir);
615
+ const lockPath = path.join(cacheDirectory, 'server.lock.json');
616
+ const localPidPath = path.join(cacheDirectory, PID_FILE_NAME);
617
+ const logFile = path.join(cacheDirectory, 'logs', 'server.log');
618
+ const metaPath = path.join(cacheDirectory, 'meta.json');
619
+
620
+ let lockData = null;
621
+ try {
622
+ lockData = JSON.parse(await fs.readFile(lockPath, 'utf-8'));
623
+ } catch {
624
+ lockData = null;
625
+ }
626
+ const lockPid = Number(lockData?.pid);
627
+ if (Number.isInteger(lockPid) && pidSet.has(lockPid)) {
628
+ runtimeByPid.set(lockPid, {
629
+ pid: lockPid,
630
+ cacheDirectory,
631
+ workspace:
632
+ typeof lockData?.workspace === 'string' && lockData.workspace.trim()
633
+ ? lockData.workspace
634
+ : null,
635
+ workspaceSource: 'lock',
636
+ logFile,
637
+ });
638
+ continue;
639
+ }
640
+
641
+ const pidFromCache = await readPidFromFile(localPidPath);
642
+ if (!Number.isInteger(pidFromCache) || !pidSet.has(pidFromCache)) {
643
+ continue;
644
+ }
645
+ if (runtimeByPid.has(pidFromCache)) {
646
+ continue;
647
+ }
648
+
649
+ let metaWorkspace = null;
650
+ try {
651
+ const metaData = JSON.parse(await fs.readFile(metaPath, 'utf-8'));
652
+ if (typeof metaData?.workspace === 'string' && metaData.workspace.trim()) {
653
+ metaWorkspace = metaData.workspace;
654
+ }
655
+ } catch {
656
+ metaWorkspace = null;
657
+ }
658
+
659
+ runtimeByPid.set(pidFromCache, {
660
+ pid: pidFromCache,
661
+ cacheDirectory,
662
+ workspace: metaWorkspace,
663
+ workspaceSource: metaWorkspace ? 'meta' : 'cache',
664
+ logFile,
665
+ });
666
+ }
667
+
668
+ return runtimeByPid;
669
+ }
670
+
671
+ function selectRuntimeForStatus({ pids, runtimeByPid, requestedWorkspace }) {
672
+ if (!Array.isArray(pids) || pids.length === 0) return null;
673
+ if (!(runtimeByPid instanceof Map) || runtimeByPid.size === 0) return null;
674
+
675
+ if (requestedWorkspace) {
676
+ const requestedNormalized = normalizePathForCompare(requestedWorkspace);
677
+ for (const pid of pids) {
678
+ const runtime = runtimeByPid.get(pid);
679
+ if (!runtime?.workspace) continue;
680
+ if (normalizePathForCompare(runtime.workspace) === requestedNormalized) {
681
+ return runtime;
682
+ }
683
+ }
684
+ }
685
+
686
+ if (pids.length === 1) {
687
+ return runtimeByPid.get(pids[0]) || null;
688
+ }
689
+
690
+ for (const pid of pids) {
691
+ const runtime = runtimeByPid.get(pid);
692
+ if (runtime?.workspace) {
693
+ return runtime;
694
+ }
695
+ }
696
+
697
+ for (const pid of pids) {
698
+ const runtime = runtimeByPid.get(pid);
699
+ if (runtime) return runtime;
700
+ }
701
+
702
+ return null;
703
+ }
704
+
705
+ async function captureConsoleOutput(fn) {
568
706
  const original = {
569
707
  info: console.info,
570
708
  warn: console.warn,
@@ -629,46 +767,49 @@ function getGlobalCacheDir() {
629
767
  return process.env.XDG_CACHE_HOME || path.join(home, '.cache');
630
768
  }
631
769
 
632
- export async function status({ fix = false, cacheOnly = false, workspaceDir = null } = {}) {
633
- try {
634
- const pids = [];
770
+ export async function status({ fix = false, cacheOnly = false, workspaceDir = null } = {}) {
771
+ try {
772
+ const pids = [];
635
773
  const now = new Date();
636
774
  const globalCacheRoot = path.join(getGlobalCacheDir(), 'heuristic-mcp');
637
775
  let logPath = 'unknown';
638
776
  let logStatus = '';
639
- let cacheSummary = null;
640
- let config = null;
641
- let configLogs = [];
642
-
643
- // 1. Check PID files first (per-workspace)
777
+ let cacheSummary = null;
778
+ let config = null;
779
+ let configLogs = [];
780
+ const cmdByPid = new Map();
781
+ let runtimeByPid = new Map();
782
+ let selectedRuntime = null;
783
+
784
+
644
785
  const pidFiles = await listPidFilePaths();
645
786
  for (const pidFile of pidFiles) {
646
787
  const pid = await readPidFromFile(pidFile);
647
788
  if (!Number.isInteger(pid)) continue;
648
- // Check if running
789
+
649
790
  try {
650
791
  process.kill(pid, 0);
651
792
  pids.push(pid);
652
793
  } catch (_e) {
653
- // Stale PID file
794
+
654
795
  await fs.unlink(pidFile).catch(() => {});
655
796
  }
656
797
  }
657
798
 
658
- // 2. Fallback to process list if no PID file found or process dead
799
+
659
800
  if (pids.length === 0) {
660
801
  try {
661
802
  const myPid = process.pid;
662
803
  if (process.platform === 'win32') {
663
804
  const { stdout } = await execPromise(
664
- `powershell -NoProfile -Command "Get-CimInstance Win32_Process -Filter \\"CommandLine LIKE '%heuristic-mcp%index.js%'\\" | Select-Object -ExpandProperty ProcessId"`
805
+ `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"`
665
806
  );
666
807
  const winPids = stdout
667
808
  .trim()
668
809
  .split(/\s+/)
669
810
  .filter((p) => p && !isNaN(p));
670
811
 
671
- // Retrieve command lines to filter out workers
812
+
672
813
  if (winPids.length > 0) {
673
814
  const { stdout: cmdOut } = await execPromise(
674
815
  `powershell -NoProfile -Command "Get-CimInstance Win32_Process | Where-Object { $_.ProcessId -in @(${winPids.join(',')}) } | Select-Object ProcessId, CommandLine"`
@@ -701,7 +842,7 @@ export async function status({ fix = false, cacheOnly = false, workspaceDir = nu
701
842
 
702
843
  for (const line of lines) {
703
844
  if (line.includes('heuristic-mcp/index.js') || line.includes('heuristic-mcp')) {
704
- // Exclude workers
845
+
705
846
  if (
706
847
  line.includes('embedding-worker') ||
707
848
  line.includes('embedding-process') ||
@@ -716,31 +857,30 @@ export async function status({ fix = false, cacheOnly = false, workspaceDir = nu
716
857
  }
717
858
  }
718
859
  }
719
- // Merge validPids into pids if not already present
860
+
720
861
  for (const p of validPids) {
721
862
  if (!pids.includes(p)) pids.push(p);
722
863
  }
723
864
  }
724
865
  } catch (_e) {
725
- /* ignore */
866
+
726
867
  }
727
868
  }
728
869
 
729
870
  if (!cacheOnly) {
730
- // STATUS OUTPUT
731
- console.info(''); // spacer
871
+
872
+ console.info('');
732
873
  if (pids.length > 0) {
733
874
  console.info(`[Lifecycle] 🟢 Server is RUNNING. PID(s): ${pids.join(', ')}`);
734
875
  } else {
735
876
  console.info('[Lifecycle] ⚪ Server is STOPPED.');
736
877
  }
737
- if (pids.length > 1) {
738
- console.info('[Lifecycle] ⚠️ Multiple servers detected; progress may be inconsistent.');
739
- }
740
- if (pids.length > 0) {
741
- const cmdByPid = new Map();
742
- try {
743
- if (process.platform === 'win32') {
878
+ if (pids.length > 1) {
879
+ console.info('[Lifecycle] ⚠️ Multiple servers detected; progress may be inconsistent.');
880
+ }
881
+ if (pids.length > 0) {
882
+ try {
883
+ if (process.platform === 'win32') {
744
884
  const { stdout } = await execPromise(
745
885
  `powershell -NoProfile -Command "Get-CimInstance Win32_Process | Where-Object { $_.ProcessId -in @(${pids.join(',')}) } | Select-Object ProcessId, CommandLine"`
746
886
  );
@@ -764,38 +904,56 @@ export async function status({ fix = false, cacheOnly = false, workspaceDir = nu
764
904
  }
765
905
  }
766
906
  } catch (_e) {
767
- // ignore command line lookup failures
907
+
768
908
  }
769
- if (cmdByPid.size > 0) {
770
- console.info('[Lifecycle] Active command lines:');
771
- for (const pid of pids) {
909
+ if (cmdByPid.size > 0) {
910
+ console.info('[Lifecycle] Active command lines:');
911
+ for (const pid of pids) {
772
912
  const cmd = cmdByPid.get(pid);
773
913
  if (cmd) {
774
914
  console.info(` ${pid}: ${cmd}`);
775
915
  }
776
- }
777
- }
778
- }
779
- console.info(''); // spacer
780
- } // End if (!cacheOnly) - server status
781
-
782
- if (!cacheOnly) {
783
- try {
784
- const captured = await captureConsoleOutput(() => loadConfig(workspaceDir));
785
- config = captured.result;
786
- configLogs = captured.lines;
787
- logPath = getLogFilePath(config);
788
- try {
789
- await fs.access(logPath);
790
- logStatus = '(exists)';
916
+ }
917
+ }
918
+
919
+ runtimeByPid = await collectRuntimeByPid({ pids, globalCacheRoot });
920
+ for (const [pid, runtime] of runtimeByPid.entries()) {
921
+ if (runtime?.workspace) continue;
922
+ const cmd = cmdByPid.get(pid);
923
+ const workspaceFromCmd = extractWorkspaceFromCommandLine(cmd);
924
+ if (workspaceFromCmd) {
925
+ runtime.workspace = workspaceFromCmd;
926
+ runtime.workspaceSource = 'cmd';
927
+ }
928
+ }
929
+ selectedRuntime = selectRuntimeForStatus({
930
+ pids,
931
+ runtimeByPid,
932
+ requestedWorkspace: workspaceDir,
933
+ });
934
+ }
935
+ console.info('');
936
+ }
937
+
938
+ if (!cacheOnly) {
939
+ try {
940
+ const configWorkspaceDir = workspaceDir || selectedRuntime?.workspace || null;
941
+ const captured = await captureConsoleOutput(() => loadConfig(configWorkspaceDir));
942
+ config = captured.result;
943
+ configLogs = captured.lines;
944
+ logPath = selectedRuntime?.logFile || getLogFilePath(config);
945
+ try {
946
+ await fs.access(logPath);
947
+ logStatus = '(exists)';
791
948
  } catch {
792
949
  logStatus = '(not found)';
793
950
  }
794
- if (config?.cacheDirectory) {
795
- const metaFile = path.join(config.cacheDirectory, 'meta.json');
796
- const progressFile = path.join(config.cacheDirectory, 'progress.json');
797
- let metaData = null;
798
- let progressData = null;
951
+ const statusCacheDirectory = selectedRuntime?.cacheDirectory || config?.cacheDirectory;
952
+ if (statusCacheDirectory) {
953
+ const metaFile = path.join(statusCacheDirectory, 'meta.json');
954
+ const progressFile = path.join(statusCacheDirectory, 'progress.json');
955
+ let metaData = null;
956
+ let progressData = null;
799
957
  try {
800
958
  metaData = JSON.parse(await fs.readFile(metaFile, 'utf-8'));
801
959
  } catch {
@@ -806,21 +964,27 @@ export async function status({ fix = false, cacheOnly = false, workspaceDir = nu
806
964
  } catch {
807
965
  progressData = null;
808
966
  }
809
- cacheSummary = {
810
- cacheDir: config.cacheDirectory,
811
- hasSnapshot: !!metaData,
812
- snapshotTime: metaData?.lastSaveTime || null,
813
- progress: progressData && typeof progressData.progress === 'number' ? progressData : null,
814
- };
815
- }
967
+ cacheSummary = {
968
+ cacheDir: statusCacheDirectory,
969
+ hasSnapshot: !!metaData,
970
+ snapshotTime: metaData?.lastSaveTime || null,
971
+ progress: progressData && typeof progressData.progress === 'number' ? progressData : null,
972
+ };
973
+ }
816
974
  } catch {
817
975
  logPath = 'unknown';
818
976
  }
819
977
 
820
- if (config?.searchDirectory) {
821
- console.info(`[Lifecycle] Workspace: ${config.searchDirectory}`);
822
- }
823
- console.info(` Log file: ${logPath} ${logStatus}`.trimEnd());
978
+ const displayWorkspace = selectedRuntime?.workspace || config?.searchDirectory;
979
+ if (displayWorkspace) {
980
+ console.info(`[Lifecycle] Workspace: ${displayWorkspace}`);
981
+ }
982
+ if (selectedRuntime?.workspace && selectedRuntime.workspace !== config?.searchDirectory) {
983
+ console.info(
984
+ ` Workspace source: running server (${selectedRuntime.workspaceSource || 'runtime'})`
985
+ );
986
+ }
987
+ console.info(` Log file: ${logPath} ${logStatus}`.trimEnd());
824
988
  if (cacheSummary?.cacheDir) {
825
989
  const snapshotLabel = cacheSummary.hasSnapshot ? 'available' : 'none';
826
990
  console.info(`[Cache] Snapshot: ${snapshotLabel}`);
@@ -838,18 +1002,18 @@ export async function status({ fix = false, cacheOnly = false, workspaceDir = nu
838
1002
  console.info('[Cache] Progress: idle');
839
1003
  }
840
1004
  }
841
- console.info(''); // spacer
1005
+ console.info('');
842
1006
 
843
1007
  if (configLogs.length > 0) {
844
1008
  for (const line of configLogs) {
845
1009
  console.info(line);
846
1010
  }
847
- console.info(''); // spacer
1011
+ console.info('');
848
1012
  }
849
1013
  }
850
1014
 
851
1015
  if (cacheOnly) {
852
- // APPEND LOGS INFO (Cache Status)
1016
+
853
1017
  console.info('[Status] Inspecting cache status...\n');
854
1018
 
855
1019
  if (fix) {
@@ -867,15 +1031,28 @@ export async function status({ fix = false, cacheOnly = false, workspaceDir = nu
867
1031
  `[Status] Found ${cacheDirs.length} cache director${cacheDirs.length === 1 ? 'y' : 'ies'} in ${globalCacheRoot}`
868
1032
  );
869
1033
 
870
- for (const dir of cacheDirs) {
871
- const cacheDir = path.join(globalCacheRoot, dir);
872
- const metaFile = path.join(cacheDir, 'meta.json');
873
- const progressFile = path.join(cacheDir, 'progress.json');
874
-
875
- console.info(`${'─'.repeat(60)}`);
876
- console.info(`📁 Cache: ${dir}`);
877
- console.info(` Path: ${cacheDir}`);
878
-
1034
+ for (const dir of cacheDirs) {
1035
+ const cacheDir = path.join(globalCacheRoot, dir);
1036
+ const metaFile = path.join(cacheDir, 'meta.json');
1037
+ const progressFile = path.join(cacheDir, 'progress.json');
1038
+ let progressData = null;
1039
+ try {
1040
+ progressData = JSON.parse(await fs.readFile(progressFile, 'utf-8'));
1041
+ } catch {
1042
+
1043
+ }
1044
+ const hasNumericProgress = progressData && typeof progressData.progress === 'number';
1045
+ const isProgressIncomplete =
1046
+ !!hasNumericProgress &&
1047
+ Number.isFinite(progressData.total) &&
1048
+ progressData.total > 0 &&
1049
+ progressData.progress < progressData.total;
1050
+ const fileProgressSummary = parseFileProgressSummary(progressData);
1051
+
1052
+ console.info(`${'─'.repeat(60)}`);
1053
+ console.info(`📁 Cache: ${dir}`);
1054
+ console.info(` Path: ${cacheDir}`);
1055
+
879
1056
  let metaData = null;
880
1057
  try {
881
1058
  metaData = JSON.parse(await fs.readFile(metaFile, 'utf-8'));
@@ -934,25 +1111,50 @@ export async function status({ fix = false, cacheOnly = false, workspaceDir = nu
934
1111
  if (Number.isFinite(metaData.lastWorkerThreads)) {
935
1112
  console.info(` Last worker threads: ${metaData.lastWorkerThreads}`);
936
1113
  }
937
- try {
938
- const dirStats = await fs.stat(cacheDir);
939
- console.info(` Cache dir last write: ${formatDateTime(dirStats.mtime)}`);
940
- } catch {
941
- // ignore cache dir stat errors
942
- }
943
-
944
- // Verify indexing completion
945
- if (metaData.filesIndexed && metaData.filesIndexed > 0) {
946
- console.info(` Cached index: ✅ COMPLETE (${metaData.filesIndexed} files)`);
947
- } else if (metaData.filesIndexed === 0) {
948
- console.info(` Cached index: ⚠️ NO FILES (check excludePatterns)`);
949
- } else {
950
- console.info(` Cached index: ⚠️ INCOMPLETE`);
951
- }
952
- } catch (err) {
953
- if (err.code === 'ENOENT') {
954
- try {
955
- const stats = await fs.stat(cacheDir);
1114
+ try {
1115
+ const dirStats = await fs.stat(cacheDir);
1116
+ console.info(` Cache dir last write: ${formatDateTime(dirStats.mtime)}`);
1117
+ } catch {
1118
+
1119
+ }
1120
+
1121
+ const progressAfterSnapshot =
1122
+ hasNumericProgress &&
1123
+ progressData.updatedAt &&
1124
+ metaData.lastSaveTime &&
1125
+ new Date(progressData.updatedAt) > new Date(metaData.lastSaveTime);
1126
+ const isIncrementalUpdateActive = Boolean(
1127
+ hasNumericProgress && (isProgressIncomplete || progressAfterSnapshot)
1128
+ );
1129
+
1130
+
1131
+ if (metaData.filesIndexed && metaData.filesIndexed > 0) {
1132
+ if (isIncrementalUpdateActive) {
1133
+ console.info(` Cached snapshot: ✅ COMPLETE (${metaData.filesIndexed} files)`);
1134
+ } else {
1135
+ console.info(` Cached index: ✅ COMPLETE (${metaData.filesIndexed} files)`);
1136
+ }
1137
+ } else if (metaData.filesIndexed === 0) {
1138
+ console.info(` Cached index: ⚠️ NO FILES (check excludePatterns)`);
1139
+ } else {
1140
+ console.info(` Cached index: ⚠️ INCOMPLETE`);
1141
+ }
1142
+
1143
+ if (
1144
+ isIncrementalUpdateActive &&
1145
+ Number.isFinite(fileProgressSummary?.total) &&
1146
+ Number.isFinite(metaData.filesIndexed) &&
1147
+ fileProgressSummary.total > metaData.filesIndexed
1148
+ ) {
1149
+ const delta = fileProgressSummary.total - metaData.filesIndexed;
1150
+ console.info(
1151
+ ` Current run target: ${fileProgressSummary.total} files (${delta} more than cached snapshot)`
1152
+ );
1153
+ }
1154
+ } catch (err) {
1155
+ if (err.code === 'ENOENT') {
1156
+ try {
1157
+ const stats = await fs.stat(cacheDir);
956
1158
  const ageMs = new Date() - stats.mtime;
957
1159
  if (ageMs < 10 * 60 * 1000) {
958
1160
  console.info(` Status: ⏳ Initializing / Indexing in progress...`);
@@ -967,21 +1169,13 @@ export async function status({ fix = false, cacheOnly = false, workspaceDir = nu
967
1169
  }
968
1170
  } else {
969
1171
  console.info(` Status: ❌ Invalid or corrupted (${err.message})`);
970
- }
971
- }
972
-
973
- // Show latest indexing progress if available
974
- let progressData = null;
975
- try {
976
- progressData = JSON.parse(await fs.readFile(progressFile, 'utf-8'));
977
- } catch {
978
- // no progress file
979
- }
980
-
981
- if (progressData && typeof progressData.progress === 'number') {
982
- const updatedAt = progressData.updatedAt
983
- ? formatDateTime(progressData.updatedAt)
984
- : 'Unknown';
1172
+ }
1173
+ }
1174
+
1175
+ if (progressData && typeof progressData.progress === 'number') {
1176
+ const updatedAt = progressData.updatedAt
1177
+ ? formatDateTime(progressData.updatedAt)
1178
+ : 'Unknown';
985
1179
  const progressLabel = metaData
986
1180
  ? 'Incremental update (post-snapshot)'
987
1181
  : 'Initial index progress';
@@ -1026,18 +1220,18 @@ export async function status({ fix = false, cacheOnly = false, workspaceDir = nu
1026
1220
  );
1027
1221
  }
1028
1222
  } else {
1029
- if (metaData) {
1030
- console.info(' Summary: Cached snapshot available; no update running.');
1031
- } else {
1032
- console.info(' Summary: No cached snapshot yet; indexing has not started.');
1033
- }
1034
- }
1035
-
1036
- if (metaData && progressData && typeof progressData.progress === 'number') {
1037
- console.info(' Indexing state: Cached snapshot available; incremental update running.');
1038
- } else if (metaData) {
1039
- console.info(' Indexing state: Cached snapshot available; idle.');
1040
- } else if (progressData && typeof progressData.progress === 'number') {
1223
+ if (metaData) {
1224
+ console.info(' Summary: Cached snapshot available; no update running.');
1225
+ } else {
1226
+ console.info(' Summary: No cached snapshot yet; indexing has not started.');
1227
+ }
1228
+ }
1229
+
1230
+ if (metaData && isProgressIncomplete) {
1231
+ console.info(' Indexing state: Cached snapshot available; incremental update running.');
1232
+ } else if (metaData) {
1233
+ console.info(' Indexing state: Cached snapshot available; idle.');
1234
+ } else if (progressData && typeof progressData.progress === 'number') {
1041
1235
  console.info(' Indexing state: Initial index in progress; no cached snapshot yet.');
1042
1236
  } else {
1043
1237
  console.info(' Indexing state: No cached snapshot; idle.');
@@ -1064,22 +1258,22 @@ export async function status({ fix = false, cacheOnly = false, workspaceDir = nu
1064
1258
  }
1065
1259
  }
1066
1260
 
1067
- // Show paths only for --status command
1261
+
1068
1262
  if (!cacheOnly) {
1069
- // SHOW PATHS
1263
+
1070
1264
  console.info('\n[Paths] Important locations:');
1071
1265
 
1072
- // Global npm bin
1266
+
1073
1267
  let npmBin = 'unknown';
1074
1268
  try {
1075
1269
  const { stdout } = await execPromise('npm config get prefix');
1076
1270
  npmBin = path.join(stdout.trim(), 'bin');
1077
1271
  } catch {
1078
- /* ignore */
1272
+
1079
1273
  }
1080
1274
  console.info(` 📦 Global npm bin: ${npmBin}`);
1081
1275
 
1082
- // Configs
1276
+
1083
1277
  const configLocations = [
1084
1278
  {
1085
1279
  name: 'Antigravity',
@@ -1115,12 +1309,12 @@ export async function status({ fix = false, cacheOnly = false, workspaceDir = nu
1115
1309
  },
1116
1310
  ];
1117
1311
 
1118
- // Platform specific logic for config paths
1312
+
1119
1313
  if (process.platform === 'darwin') {
1120
1314
  configLocations[2].path = path.join(
1121
1315
  os.homedir(),
1122
- // Keep platform-native macOS path behavior.
1123
- // Home directory above is used only for Windows/Linux defaults.
1316
+
1317
+
1124
1318
  'Library',
1125
1319
  'Application Support',
1126
1320
  'Claude',
@@ -1173,7 +1367,7 @@ export async function status({ fix = false, cacheOnly = false, workspaceDir = nu
1173
1367
  await fs.access(loc.path);
1174
1368
  status = '(exists)';
1175
1369
  } catch {
1176
- /* ignore */
1370
+
1177
1371
  }
1178
1372
  console.info(` - ${loc.name}: ${loc.path} ${status}`);
1179
1373
  }
@@ -1182,7 +1376,7 @@ export async function status({ fix = false, cacheOnly = false, workspaceDir = nu
1182
1376
  console.info(` 💾 Cache root: ${globalCacheRoot}`);
1183
1377
  console.info(` 📁 Current dir: ${process.cwd()}`);
1184
1378
  console.info('');
1185
- } // End if (!cacheOnly) - paths
1379
+ }
1186
1380
  } catch (error) {
1187
1381
  console.error(`[Lifecycle] Failed to check status: ${error.message}`);
1188
1382
  }