@softerist/heuristic-mcp 3.0.17 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/config.js CHANGED
@@ -1,16 +1,19 @@
1
- import fs from 'fs/promises';
2
- import path from 'path';
3
- import os from 'os';
4
- import crypto from 'crypto';
5
- import { fileURLToPath } from 'url';
6
- import { ProjectDetector } from './project-detector.js';
7
- import { parseJsonc } from './settings-editor.js';
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ import os from 'os';
4
+ import { fileURLToPath } from 'url';
5
+ import { ProjectDetector } from './project-detector.js';
6
+ import { parseJsonc } from './settings-editor.js';
7
+ import {
8
+ getLegacyWorkspaceCachePath,
9
+ getWorkspaceCachePath,
10
+ } from './workspace-cache-key.js';
8
11
  import {
9
12
  EMBEDDING_PROCESS_DEFAULT_GC_MAX_REQUESTS_WITHOUT_COLLECTION,
10
13
  EMBEDDING_PROCESS_DEFAULT_GC_MIN_INTERVAL_MS,
11
14
  EMBEDDING_PROCESS_DEFAULT_GC_RSS_THRESHOLD_MB,
12
15
  } from './constants.js';
13
- import { getWorkspaceEnvKeys } from './workspace-env.js';
16
+ import { getWorkspaceEnvDiagnosticKeys, getWorkspaceEnvKeys } from './workspace-env.js';
14
17
 
15
18
  const DEFAULT_MEMORY_CLEANUP_CONFIG = {
16
19
  enableExplicitGc: true, // Require --expose-gc for more aggressive memory cleanup
@@ -36,6 +39,7 @@ const DEFAULT_INDEXING_CONFIG = {
36
39
  prefilterContentMaxBytes: 512 * 1024, // 512KB - cache content during prefilter to avoid double reads
37
40
  maxResults: 5, // Maximum number of semantic search results to return
38
41
  watchFiles: true, // Enable file system watcher to re-index changed files in real-time
42
+ indexCheckpointIntervalMs: 5000, // Periodic cache checkpoint during full indexing (0 disables)
39
43
  };
40
44
 
41
45
  const DEFAULT_LOGGING_CONFIG = {
@@ -43,10 +47,11 @@ const DEFAULT_LOGGING_CONFIG = {
43
47
  memoryLogIntervalMs: 5000, // Verbose memory log cadence during indexing (ms)
44
48
  };
45
49
 
46
- const DEFAULT_CACHE_CONFIG = {
47
- enableCache: true, // Whether to persist and reload embeddings between sessions
48
- saveReaderWaitTimeoutMs: 5000, // Max wait for active reads before saving binary cache
49
- cacheVectorAssumeFinite: true, // Assume vectors are finite (skip validation)
50
+ const DEFAULT_CACHE_CONFIG = {
51
+ enableCache: true, // Whether to persist and reload embeddings between sessions
52
+ allowSystemWorkspaceCache: false, // Safety: block cache writes for IDE/system install directories unless explicitly overridden
53
+ saveReaderWaitTimeoutMs: 5000, // Max wait for active reads before saving binary cache
54
+ cacheVectorAssumeFinite: true, // Assume vectors are finite (skip validation)
50
55
  cacheVectorFloatDigits: null, // Decimal precision for cached vectors (null = default)
51
56
  cacheWriteHighWaterMark: 262144, // Write stream highWaterMark for cache files
52
57
  cacheVectorFlushChars: 262144, // Flush threshold (chars) for JSON writer
@@ -56,13 +61,14 @@ const DEFAULT_CACHE_CONFIG = {
56
61
  cacheVectorJoinChunkSize: 2048, // Chunk size for JSON join optimization
57
62
  };
58
63
 
59
- const DEFAULT_WORKER_CONFIG = {
60
- workerThreads: 'auto', // 0 = run in main thread (no workers), "auto" = CPU cores - 1, or set a number
61
- workerBatchTimeoutMs: 120000, // Timeout per worker batch before fallback (ms)
62
- workerFailureThreshold: 1, // Open circuit after N worker failures
63
- workerFailureCooldownMs: 10 * 60 * 1000, // Cooldown before retrying workers
64
- workerMaxChunksPerBatch: 100, // Cap chunks per worker batch to reduce hang risk
65
- allowSingleThreadFallback: false, // Allow fallback to main-thread embeddings if workers fail
64
+ const DEFAULT_WORKER_CONFIG = {
65
+ workerThreads: 'auto', // 0 = run in main thread (no workers), "auto" = CPU cores - 1, or set a number
66
+ workerDisableHeavyModelOnWindows: true, // Safety guard: disable worker-pool mode for heavy models on Windows unless explicitly opted in
67
+ workerBatchTimeoutMs: 120000, // Timeout per worker batch before fallback (ms)
68
+ workerFailureThreshold: 1, // Open circuit after N worker failures
69
+ workerFailureCooldownMs: 10 * 60 * 1000, // Cooldown before retrying workers
70
+ workerMaxChunksPerBatch: 100, // Cap chunks per worker batch to reduce hang risk
71
+ allowSingleThreadFallback: false, // Allow fallback to main-thread embeddings if workers fail
66
72
  failFastEmbeddingErrors: false, // Abort worker embedding batch after repeated consecutive embed failures
67
73
  };
68
74
 
@@ -126,9 +132,11 @@ const SEARCH_KEYS = Object.freeze(Object.keys(DEFAULT_SEARCH_CONFIG));
126
132
  const CALL_GRAPH_KEYS = Object.freeze(Object.keys(DEFAULT_CALL_GRAPH_CONFIG));
127
133
  const ANN_KEYS = Object.freeze(Object.keys(DEFAULT_ANN_CONFIG));
128
134
 
129
- const DEFAULT_CONFIG = {
130
- searchDirectory: '.',
131
- fileExtensions: [
135
+ const DEFAULT_CONFIG = {
136
+ searchDirectory: '.',
137
+ autoStopOtherServersOnStartup: true, // Ensure newly started workspace server can replace stale older heuristic-mcp instances
138
+ requireTrustedWorkspaceSignalForTools: false, // Fail workspace-bound tools when no current roots/env workspace signal is available
139
+ fileExtensions: [
132
140
  // JavaScript/TypeScript
133
141
  'js',
134
142
  'ts',
@@ -383,14 +391,16 @@ const DEFAULT_CONFIG = {
383
391
  removeDuplicates: true, // Remove duplicate workspace caches
384
392
  },
385
393
  watchFiles: DEFAULT_INDEXING_CONFIG.watchFiles,
394
+ indexCheckpointIntervalMs: DEFAULT_INDEXING_CONFIG.indexCheckpointIntervalMs,
386
395
  verbose: DEFAULT_LOGGING_CONFIG.verbose,
387
396
  memoryLogIntervalMs: DEFAULT_LOGGING_CONFIG.memoryLogIntervalMs,
388
397
  saveReaderWaitTimeoutMs: DEFAULT_CACHE_CONFIG.saveReaderWaitTimeoutMs,
389
- workerThreads: DEFAULT_WORKER_CONFIG.workerThreads,
390
- workerBatchTimeoutMs: DEFAULT_WORKER_CONFIG.workerBatchTimeoutMs,
391
- workerFailureThreshold: DEFAULT_WORKER_CONFIG.workerFailureThreshold,
392
- workerFailureCooldownMs: DEFAULT_WORKER_CONFIG.workerFailureCooldownMs,
393
- workerMaxChunksPerBatch: DEFAULT_WORKER_CONFIG.workerMaxChunksPerBatch,
398
+ workerThreads: DEFAULT_WORKER_CONFIG.workerThreads,
399
+ workerDisableHeavyModelOnWindows: DEFAULT_WORKER_CONFIG.workerDisableHeavyModelOnWindows,
400
+ workerBatchTimeoutMs: DEFAULT_WORKER_CONFIG.workerBatchTimeoutMs,
401
+ workerFailureThreshold: DEFAULT_WORKER_CONFIG.workerFailureThreshold,
402
+ workerFailureCooldownMs: DEFAULT_WORKER_CONFIG.workerFailureCooldownMs,
403
+ workerMaxChunksPerBatch: DEFAULT_WORKER_CONFIG.workerMaxChunksPerBatch,
394
404
  allowSingleThreadFallback: DEFAULT_WORKER_CONFIG.allowSingleThreadFallback,
395
405
  failFastEmbeddingErrors: DEFAULT_WORKER_CONFIG.failFastEmbeddingErrors,
396
406
  embeddingProcessPerBatch: DEFAULT_EMBEDDING_CONFIG.embeddingProcessPerBatch,
@@ -630,6 +640,32 @@ async function resolveWorkspaceCandidate(rawValue) {
630
640
  return candidate;
631
641
  }
632
642
 
643
+ function formatWorkspaceProbeValue(rawValue, maxLength = 120) {
644
+ const value = String(rawValue || '').trim();
645
+ if (!value) return '';
646
+ if (value.length <= maxLength) return value;
647
+ return `${value.slice(0, maxLength - 3)}...`;
648
+ }
649
+
650
+ function logWorkspaceEnvProbe(entries) {
651
+ if (!entries || entries.length === 0) {
652
+ console.info('[Config] Workspace env probe: no workspace-like environment variables were set.');
653
+ return;
654
+ }
655
+
656
+ const preview = entries.slice(0, 10).map((entry) => {
657
+ const scope = entry.priority ? 'priority' : 'diagnostic';
658
+ const status = entry.ignoredNonProject
659
+ ? `ignored-non-project:${entry.resolvedPath}`
660
+ : entry.resolvedPath
661
+ ? `valid:${entry.resolvedPath}`
662
+ : `invalid:${entry.value}`;
663
+ return `${entry.key}[${scope}]=${status}`;
664
+ });
665
+ const suffix = entries.length > 10 ? ` (+${entries.length - 10} more)` : '';
666
+ console.info(`[Config] Workspace env probe: ${preview.join('; ')}${suffix}`);
667
+ }
668
+
633
669
  function logWorkspaceResolution(resolution) {
634
670
  if (!resolution || !resolution.path) return;
635
671
 
@@ -659,6 +695,24 @@ function logWorkspaceResolution(resolution) {
659
695
  console.info(`[Config] Workspace resolution: process.cwd() -> ${resolution.path}`);
660
696
  }
661
697
 
698
+ /**
699
+ * Heuristic check: is this path likely an IDE install or system directory
700
+ * rather than a user workspace? Used to warn when workspace resolution
701
+ * falls back to process.cwd() and lands in a non-project location.
702
+ */
703
+ export function isNonProjectDirectory(dir) {
704
+ const normalized = dir.replace(/\\/g, '/').toLowerCase();
705
+ const markers = [
706
+ '/program files/',
707
+ '/program files (x86)/',
708
+ '/appdata/local/programs/',
709
+ '/appdata/roaming/',
710
+ '/applications/',
711
+ '/contents/resources/',
712
+ ];
713
+ return markers.some((m) => normalized.includes(m));
714
+ }
715
+
662
716
  async function resolveWorkspaceDir(workspaceDir) {
663
717
  if (workspaceDir) {
664
718
  return {
@@ -673,42 +727,90 @@ async function resolveWorkspaceDir(workspaceDir) {
673
727
  };
674
728
  }
675
729
 
676
- for (const key of getWorkspaceEnvKeys()) {
677
- const candidate = await resolveWorkspaceCandidate(process.env[key]);
678
- if (candidate) {
679
- return {
680
- path: candidate,
681
- source: 'env',
682
- envKey: key,
730
+ const prioritizedEnvKeys = getWorkspaceEnvKeys();
731
+ const prioritizedEnvKeySet = new Set(prioritizedEnvKeys);
732
+ const workspaceEnvProbe = [];
733
+
734
+ for (const key of prioritizedEnvKeys) {
735
+ const rawValue = process.env[key];
736
+ if (rawValue === undefined || rawValue === null || String(rawValue).trim() === '') {
737
+ continue;
738
+ }
739
+ const value = formatWorkspaceProbeValue(rawValue);
740
+ const candidate = await resolveWorkspaceCandidate(rawValue);
741
+ workspaceEnvProbe.push({
742
+ key,
743
+ value,
744
+ resolvedPath: candidate,
745
+ priority: true,
746
+ ignoredNonProject: Boolean(candidate && isNonProjectDirectory(candidate)),
747
+ });
748
+ if (candidate && !isNonProjectDirectory(candidate)) {
749
+ return {
750
+ path: candidate,
751
+ source: 'env',
752
+ envKey: key,
753
+ workspaceEnvProbe,
683
754
  };
684
755
  }
685
756
  }
686
757
 
758
+ for (const key of getWorkspaceEnvDiagnosticKeys()) {
759
+ if (prioritizedEnvKeySet.has(key)) continue;
760
+ const rawValue = process.env[key];
761
+ if (rawValue === undefined || rawValue === null || String(rawValue).trim() === '') {
762
+ continue;
763
+ }
764
+ const value = formatWorkspaceProbeValue(rawValue);
765
+ const candidate = await resolveWorkspaceCandidate(rawValue);
766
+ workspaceEnvProbe.push({
767
+ key,
768
+ value,
769
+ resolvedPath: candidate,
770
+ priority: false,
771
+ });
772
+ }
773
+
774
+ logWorkspaceEnvProbe(workspaceEnvProbe);
775
+
687
776
  const cwd = path.resolve(process.cwd());
777
+
778
+ // Note: if CWD is an IDE/system dir, MCP roots detection in index.js will auto-correct.
779
+ if (isNonProjectDirectory(cwd)) {
780
+ console.info(
781
+ `[Config] CWD "${cwd}" appears to be an IDE/system directory. MCP roots detection will attempt auto-correction.`
782
+ );
783
+ }
784
+
688
785
  const root = await findWorkspaceRoot(cwd);
689
786
  if (root !== cwd) {
690
787
  return {
691
788
  path: root,
692
789
  source: 'cwd-root-search',
693
790
  fromPath: cwd,
791
+ workspaceEnvProbe,
694
792
  };
695
793
  }
696
794
  return {
697
795
  path: cwd,
698
796
  source: 'cwd',
797
+ workspaceEnvProbe,
699
798
  };
700
799
  }
701
800
 
702
801
  export async function loadConfig(workspaceDir = null) {
802
+ let workspaceResolution = null;
803
+ let baseDir = null;
804
+ let searchDirectoryFromConfig = false;
805
+
703
806
  try {
704
807
  // Determine the base directory for configuration
705
- let baseDir;
706
808
  let configPath;
707
809
 
708
810
  let serverDir = null;
709
811
  if (workspaceDir) {
710
812
  // Workspace mode: load config from workspace root
711
- const workspaceResolution = await resolveWorkspaceDir(workspaceDir);
813
+ workspaceResolution = await resolveWorkspaceDir(workspaceDir);
712
814
  baseDir = workspaceResolution.path;
713
815
  console.info(`[Config] Workspace mode: ${baseDir}`);
714
816
  logWorkspaceResolution(workspaceResolution);
@@ -717,7 +819,7 @@ export async function loadConfig(workspaceDir = null) {
717
819
  // but use process.cwd() as base for searching if not specified otherwise
718
820
  const scriptDir = path.dirname(fileURLToPath(import.meta.url));
719
821
  serverDir = path.resolve(scriptDir, '..');
720
- const workspaceResolution = await resolveWorkspaceDir(null);
822
+ workspaceResolution = await resolveWorkspaceDir(null);
721
823
  baseDir = workspaceResolution.path;
722
824
  logWorkspaceResolution(workspaceResolution);
723
825
  }
@@ -772,6 +874,7 @@ export async function loadConfig(workspaceDir = null) {
772
874
 
773
875
  // Set search directory (respect user override when provided)
774
876
  if (userConfig.searchDirectory) {
877
+ searchDirectoryFromConfig = true;
775
878
  config.searchDirectory = path.isAbsolute(userConfig.searchDirectory)
776
879
  ? userConfig.searchDirectory
777
880
  : path.join(baseDir, userConfig.searchDirectory);
@@ -779,25 +882,35 @@ export async function loadConfig(workspaceDir = null) {
779
882
  config.searchDirectory = baseDir;
780
883
  }
781
884
 
782
- // Determine cache directory
783
- if (userConfig.cacheDirectory) {
784
- // User explicitly set a cache path in their config file
785
- config.cacheDirectory = path.isAbsolute(userConfig.cacheDirectory)
786
- ? userConfig.cacheDirectory
787
- : path.join(baseDir, userConfig.cacheDirectory);
788
- } else {
789
- // Use global cache directory to prevent cluttering project root
790
- // Hash the absolute path to ensure uniqueness per project
791
- const projectHash = crypto
792
- .createHash('md5')
793
- .update(config.searchDirectory)
794
- .digest('hex')
795
- .slice(0, 12);
796
- const globalCacheRoot = getGlobalCacheDir();
797
- config.cacheDirectory = path.join(globalCacheRoot, 'heuristic-mcp', projectHash);
798
-
799
- // Support legacy .smart-coding-cache if it already exists in the project root
800
- const legacyPath = path.join(baseDir, '.smart-coding-cache');
885
+ // Determine cache directory
886
+ if (userConfig.cacheDirectory) {
887
+ // User explicitly set a cache path in their config file
888
+ config.cacheDirectory = path.isAbsolute(userConfig.cacheDirectory)
889
+ ? userConfig.cacheDirectory
890
+ : path.join(baseDir, userConfig.cacheDirectory);
891
+ } else {
892
+ // Use global cache directory to prevent cluttering project root.
893
+ // Workspace path is normalized for hashing so Windows drive-letter case
894
+ // differences (e.g. F:\ vs f:\) resolve to the same cache id.
895
+ const globalCacheRoot = getGlobalCacheDir();
896
+ const normalizedCachePath = getWorkspaceCachePath(config.searchDirectory, globalCacheRoot);
897
+ const legacyCachePath = getLegacyWorkspaceCachePath(config.searchDirectory, globalCacheRoot);
898
+
899
+ if (
900
+ normalizedCachePath !== legacyCachePath &&
901
+ !(await pathExists(normalizedCachePath)) &&
902
+ (await pathExists(legacyCachePath))
903
+ ) {
904
+ config.cacheDirectory = legacyCachePath;
905
+ console.info(
906
+ `[Config] Using legacy cache path for compatibility: ${path.basename(legacyCachePath)}`
907
+ );
908
+ } else {
909
+ config.cacheDirectory = normalizedCachePath;
910
+ }
911
+
912
+ // Support legacy .smart-coding-cache if it already exists in the project root
913
+ const legacyPath = path.join(baseDir, '.smart-coding-cache');
801
914
  try {
802
915
  const stats = await fs.stat(legacyPath);
803
916
  if (stats.isDirectory()) {
@@ -840,6 +953,16 @@ export async function loadConfig(workspaceDir = null) {
840
953
  console.warn(`[Config] Error: ${error.message}`);
841
954
  }
842
955
 
956
+ config.workspaceResolution = {
957
+ source: workspaceResolution?.source || 'unknown',
958
+ envKey: workspaceResolution?.envKey || null,
959
+ fromPath: workspaceResolution?.fromPath || null,
960
+ baseDirectory: baseDir,
961
+ searchDirectory: config.searchDirectory,
962
+ searchDirectoryFromConfig,
963
+ workspaceEnvProbe: workspaceResolution?.workspaceEnvProbe || [],
964
+ };
965
+
843
966
  // Apply environment variable overrides (prefix: SMART_CODING_) with validation
844
967
  if (process.env.SMART_CODING_VERBOSE !== undefined) {
845
968
  const value = process.env.SMART_CODING_VERBOSE;
@@ -932,10 +1055,43 @@ export async function loadConfig(workspaceDir = null) {
932
1055
  }
933
1056
  }
934
1057
 
935
- if (process.env.SMART_CODING_WATCH_FILES !== undefined) {
936
- const value = process.env.SMART_CODING_WATCH_FILES;
937
- if (value === 'true' || value === 'false') {
938
- config.watchFiles = value === 'true';
1058
+ if (process.env.SMART_CODING_WATCH_FILES !== undefined) {
1059
+ const value = process.env.SMART_CODING_WATCH_FILES;
1060
+ if (value === 'true' || value === 'false') {
1061
+ config.watchFiles = value === 'true';
1062
+ }
1063
+ }
1064
+
1065
+ if (process.env.SMART_CODING_REQUIRE_TRUSTED_WORKSPACE_SIGNAL_FOR_TOOLS !== undefined) {
1066
+ const value = process.env.SMART_CODING_REQUIRE_TRUSTED_WORKSPACE_SIGNAL_FOR_TOOLS;
1067
+ if (value === 'true' || value === 'false') {
1068
+ config.requireTrustedWorkspaceSignalForTools = value === 'true';
1069
+ } else {
1070
+ console.warn(
1071
+ `[Config] Invalid SMART_CODING_REQUIRE_TRUSTED_WORKSPACE_SIGNAL_FOR_TOOLS: ${value}, using default`
1072
+ );
1073
+ }
1074
+ }
1075
+
1076
+ if (process.env.SMART_CODING_AUTO_STOP_OTHER_SERVERS_ON_STARTUP !== undefined) {
1077
+ const value = process.env.SMART_CODING_AUTO_STOP_OTHER_SERVERS_ON_STARTUP;
1078
+ if (value === 'true' || value === 'false') {
1079
+ config.autoStopOtherServersOnStartup = value === 'true';
1080
+ } else {
1081
+ console.warn(
1082
+ `[Config] Invalid SMART_CODING_AUTO_STOP_OTHER_SERVERS_ON_STARTUP: ${value}, using default`
1083
+ );
1084
+ }
1085
+ }
1086
+
1087
+ if (process.env.SMART_CODING_INDEX_CHECKPOINT_INTERVAL_MS !== undefined) {
1088
+ const value = parseInt(process.env.SMART_CODING_INDEX_CHECKPOINT_INTERVAL_MS, 10);
1089
+ if (!isNaN(value) && value >= 0) {
1090
+ config.indexCheckpointIntervalMs = value;
1091
+ } else {
1092
+ console.warn(
1093
+ `[Config] Invalid SMART_CODING_INDEX_CHECKPOINT_INTERVAL_MS: ${process.env.SMART_CODING_INDEX_CHECKPOINT_INTERVAL_MS}, using default`
1094
+ );
939
1095
  }
940
1096
  }
941
1097
 
@@ -1138,7 +1294,7 @@ export async function loadConfig(workspaceDir = null) {
1138
1294
  }
1139
1295
  }
1140
1296
 
1141
- if (process.env.SMART_CODING_WORKER_THREADS !== undefined) {
1297
+ if (process.env.SMART_CODING_WORKER_THREADS !== undefined) {
1142
1298
  const value = process.env.SMART_CODING_WORKER_THREADS.trim().toLowerCase();
1143
1299
  if (value === 'auto') {
1144
1300
  config.workerThreads = 'auto';
@@ -1152,7 +1308,21 @@ export async function loadConfig(workspaceDir = null) {
1152
1308
  );
1153
1309
  }
1154
1310
  }
1155
- }
1311
+ }
1312
+
1313
+ const workerDisableHeavyModelOnWindowsEnv =
1314
+ process.env.SMART_CODING_WORKER_DISABLE_HEAVY_MODEL_ON_WINDOWS ??
1315
+ process.env.SMART_CODING_WORKER_DISABLE_HEAVY_MODEL_WINDOWS;
1316
+ if (workerDisableHeavyModelOnWindowsEnv !== undefined) {
1317
+ const value = workerDisableHeavyModelOnWindowsEnv;
1318
+ if (value === 'true' || value === 'false') {
1319
+ config.workerDisableHeavyModelOnWindows = value === 'true';
1320
+ } else {
1321
+ console.warn(
1322
+ `[Config] Invalid SMART_CODING_WORKER_DISABLE_HEAVY_MODEL_ON_WINDOWS: ${value}, using default`
1323
+ );
1324
+ }
1325
+ }
1156
1326
 
1157
1327
  if (process.env.SMART_CODING_EMBEDDING_FAIL_FAST_BREAKER !== undefined) {
1158
1328
  const value = process.env.SMART_CODING_EMBEDDING_FAIL_FAST_BREAKER;
package/lib/constants.js CHANGED
@@ -25,15 +25,35 @@ export const WORKSPACE_ENV_VARS = Object.freeze([
25
25
  ]);
26
26
 
27
27
  /**
28
- * Prefix for dynamic workspace-related env vars (provider-specific).
28
+ * Prefixes for dynamic workspace-related env vars (provider-specific).
29
+ */
30
+ export const DYNAMIC_WORKSPACE_ENV_PREFIXES = Object.freeze([
31
+ 'CODEX_',
32
+ 'ANTIGRAVITY_',
33
+ 'CURSOR_',
34
+ 'CLAUDE_',
35
+ 'WINDSURF_',
36
+ 'WARP_',
37
+ 'MCP_',
38
+ 'VSCODE_',
39
+ ]);
40
+
41
+ /**
42
+ * Backward-compatible alias for legacy single-prefix consumers.
29
43
  */
30
- export const DYNAMIC_WORKSPACE_ENV_PREFIX = 'CODEX_';
44
+ export const DYNAMIC_WORKSPACE_ENV_PREFIX = DYNAMIC_WORKSPACE_ENV_PREFIXES[0];
31
45
 
32
46
  /**
33
47
  * Pattern used when ranking provider-specific workspace env vars.
34
48
  */
35
49
  export const WORKSPACE_ENV_KEY_PATTERN = /(WORKSPACE|PROJECT|ROOT|CWD|DIR)/i;
36
50
 
51
+ /**
52
+ * Broad token used to discover unknown provider keys safely.
53
+ * We only auto-discover generic env keys containing "WORKSPACE".
54
+ */
55
+ export const WORKSPACE_ENV_GENERIC_DISCOVERY_PATTERN = /WORKSPACE/i;
56
+
37
57
  // ================================
38
58
  // Chunking Constants
39
59
  // ================================
@@ -6,7 +6,7 @@ import readline from 'readline';
6
6
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
7
  const EMBEDDING_PROCESS_PATH = path.join(__dirname, 'embedding-process.js');
8
8
 
9
- // Persistent child process pool - single instance
9
+
10
10
  let persistentChild = null;
11
11
  let childReadline = null;
12
12
  let idleTimer = null;
@@ -14,16 +14,13 @@ let currentConfig = null;
14
14
  let pendingRequests = [];
15
15
  let isProcessingRequest = false;
16
16
 
17
- // Default idle timeout: 30 seconds - child exits after this to free memory
17
+
18
18
  const DEFAULT_IDLE_TIMEOUT_MS = 30000;
19
19
 
20
- /**
21
- * Get or create the persistent embedding child process.
22
- * The child stays alive for consecutive queries, then exits after idle timeout.
23
- */
20
+
24
21
  function getOrCreateChild(config) {
25
22
  if (persistentChild && !persistentChild.killed) {
26
- // Reset idle timer on each use
23
+
27
24
  resetIdleTimer(config);
28
25
  return persistentChild;
29
26
  }
@@ -41,7 +38,7 @@ function getOrCreateChild(config) {
41
38
 
42
39
  currentConfig = config;
43
40
 
44
- // Set up readline for line-by-line response parsing
41
+
45
42
  childReadline = readline.createInterface({
46
43
  input: persistentChild.stdout,
47
44
  crlfDelay: Infinity,
@@ -50,7 +47,7 @@ function getOrCreateChild(config) {
50
47
  childReadline.on('line', (line) => {
51
48
  if (!line.trim()) return;
52
49
 
53
- // Process the response for the current pending request
50
+
54
51
  if (pendingRequests.length > 0) {
55
52
  const { resolve, reject, startTime } = pendingRequests.shift();
56
53
  try {
@@ -93,7 +90,7 @@ function getOrCreateChild(config) {
93
90
  persistentChild.on('error', (err) => {
94
91
  console.error(`[EmbedPool] Child process error: ${err.message}`);
95
92
  cleanupChild();
96
- // Reject all pending requests
93
+
97
94
  for (const { reject } of pendingRequests) {
98
95
  reject(new Error(`Child process error: ${err.message}`));
99
96
  }
@@ -105,7 +102,7 @@ function getOrCreateChild(config) {
105
102
  console.info(`[EmbedPool] Child process exited with code ${code}`);
106
103
  }
107
104
  cleanupChild();
108
- // Reject remaining pending requests so they can retry
105
+
109
106
  for (const { reject } of pendingRequests) {
110
107
  reject(new Error(`Child process exited unexpectedly with code ${code}`));
111
108
  }
@@ -154,10 +151,10 @@ function cleanupChild() {
154
151
  function shutdownChild() {
155
152
  if (persistentChild && !persistentChild.killed) {
156
153
  try {
157
- // Send shutdown command
154
+
158
155
  persistentChild.stdin.write(JSON.stringify({ type: 'shutdown' }) + '\n');
159
156
  } catch {
160
- // If write fails, force kill
157
+
161
158
  persistentChild.kill();
162
159
  }
163
160
  }
@@ -183,15 +180,7 @@ function processNextRequest() {
183
180
  }
184
181
  }
185
182
 
186
- /**
187
- * Embed a single query string using a persistent child process.
188
- * The child process stays alive for consecutive queries, then exits after idle timeout.
189
- * This gives fast consecutive searches + memory cleanup after idle period.
190
- *
191
- * @param {string} query - The query text to embed
192
- * @param {object} config - Configuration object with embeddingModel and embeddingProcessNumThreads
193
- * @returns {Promise<Float32Array>} - The embedding vector
194
- */
183
+
195
184
  export async function embedQueryInChildProcess(query, config) {
196
185
  return new Promise((resolve, reject) => {
197
186
  const payload = {
@@ -212,17 +201,12 @@ export async function embedQueryInChildProcess(query, config) {
212
201
  });
213
202
  }
214
203
 
215
- /**
216
- * Force shutdown the persistent child process to immediately free memory.
217
- * Called when user explicitly wants to free memory.
218
- */
204
+
219
205
  export function forceShutdownEmbeddingPool() {
220
206
  shutdownChild();
221
207
  }
222
208
 
223
- /**
224
- * Check if the persistent child process is currently running.
225
- */
209
+
226
210
  export function isEmbeddingPoolActive() {
227
211
  return persistentChild !== null && !persistentChild.killed;
228
212
  }