@softerist/heuristic-mcp 3.0.15 → 3.0.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +104 -104
  2. package/config.jsonc +173 -173
  3. package/features/ann-config.js +131 -0
  4. package/features/clear-cache.js +84 -0
  5. package/features/find-similar-code.js +291 -0
  6. package/features/hybrid-search.js +544 -0
  7. package/features/index-codebase.js +3268 -0
  8. package/features/lifecycle.js +1189 -0
  9. package/features/package-version.js +302 -0
  10. package/features/register.js +408 -0
  11. package/features/resources.js +156 -0
  12. package/features/set-workspace.js +265 -0
  13. package/index.js +96 -96
  14. package/lib/cache-ops.js +22 -22
  15. package/lib/cache-utils.js +565 -565
  16. package/lib/cache.js +1870 -1870
  17. package/lib/call-graph.js +396 -396
  18. package/lib/cli.js +1 -1
  19. package/lib/config.js +517 -517
  20. package/lib/constants.js +39 -39
  21. package/lib/embed-query-process.js +7 -7
  22. package/lib/embedding-process.js +7 -7
  23. package/lib/embedding-worker.js +299 -299
  24. package/lib/ignore-patterns.js +316 -316
  25. package/lib/json-worker.js +14 -14
  26. package/lib/json-writer.js +337 -337
  27. package/lib/logging.js +164 -164
  28. package/lib/memory-logger.js +13 -13
  29. package/lib/onnx-backend.js +193 -193
  30. package/lib/project-detector.js +84 -84
  31. package/lib/server-lifecycle.js +165 -165
  32. package/lib/settings-editor.js +754 -754
  33. package/lib/tokenizer.js +256 -256
  34. package/lib/utils.js +428 -428
  35. package/lib/vector-store-binary.js +627 -627
  36. package/lib/vector-store-sqlite.js +95 -95
  37. package/lib/workspace-env.js +28 -28
  38. package/mcp_config.json +9 -9
  39. package/package.json +86 -75
  40. package/scripts/clear-cache.js +20 -0
  41. package/scripts/download-model.js +43 -0
  42. package/scripts/mcp-launcher.js +49 -0
  43. package/scripts/postinstall.js +12 -0
  44. package/search-configs.js +36 -36
  45. package/.prettierrc +0 -7
  46. package/debug-pids.js +0 -30
  47. package/eslint.config.js +0 -36
  48. package/specs/plan.md +0 -23
  49. package/vitest.config.js +0 -39
@@ -0,0 +1,156 @@
1
+ /**
2
+ * MCP Resources Feature
3
+ *
4
+ * Exposes workspace files as MCP resources for discovery and reading.
5
+ */
6
+
7
+ import fs from 'fs/promises';
8
+ import path from 'path';
9
+ import { fdir } from 'fdir';
10
+ import { getMimeType } from '../lib/constants.js';
11
+
12
+ /**
13
+ * Convert a file path to a file:// URI.
14
+ * @param {string} filePath - Absolute file path
15
+ * @returns {string}
16
+ */
17
+ function pathToUri(filePath) {
18
+ // Normalize path separators and encode for URI
19
+ const normalized = filePath.replace(/\\/g, '/');
20
+ // On Windows, paths start with drive letter like C:/
21
+ if (/^[a-zA-Z]:/.test(normalized)) {
22
+ return `file:///${normalized}`;
23
+ }
24
+ return `file://${normalized}`;
25
+ }
26
+
27
+ /**
28
+ * Convert a file:// URI back to a file path.
29
+ * @param {string} uri
30
+ * @returns {string}
31
+ */
32
+ function uriToPath(uri) {
33
+ if (!uri.startsWith('file://')) {
34
+ throw new Error(`Invalid file URI: ${uri}`);
35
+ }
36
+ let filePath = uri.slice(7); // Remove 'file://'
37
+ // Handle Windows paths (file:///C:/...)
38
+ if (/^\/[a-zA-Z]:/.test(filePath)) {
39
+ filePath = filePath.slice(1); // Remove leading /
40
+ }
41
+ // Decode URI components
42
+ filePath = decodeURIComponent(filePath);
43
+ // Normalize to OS path separators
44
+ if (process.platform === 'win32') {
45
+ filePath = filePath.replace(/\//g, '\\');
46
+ }
47
+ return filePath;
48
+ }
49
+
50
+ /**
51
+ * Check if a path is within the workspace directory.
52
+ * @param {string} filePath - Absolute file path
53
+ * @param {string} workspaceDir - Workspace directory
54
+ * @returns {boolean}
55
+ */
56
+ function isWithinWorkspace(filePath, workspaceDir) {
57
+ const resolvedPath = path.resolve(filePath);
58
+ const resolvedWorkspace = path.resolve(workspaceDir);
59
+ const relativePath = path.relative(resolvedWorkspace, resolvedPath);
60
+ return !relativePath.startsWith('..') && !path.isAbsolute(relativePath);
61
+ }
62
+
63
+ /**
64
+ * List resources handler for MCP.
65
+ * @param {object} config - Server configuration
66
+ * @returns {Promise<{resources: Array}>}
67
+ */
68
+ export async function handleListResources(config) {
69
+ const workspaceDir = config.searchDirectory;
70
+ const maxResults = 500; // Limit to avoid overwhelming clients
71
+
72
+ // Build set of allowed extensions from config
73
+ const allowedExtensions = new Set(
74
+ (config.fileExtensions || []).map(ext => `.${ext.toLowerCase()}`)
75
+ );
76
+
77
+ // Extract directory names from exclude patterns (e.g., '**/node_modules/**' -> 'node_modules')
78
+ const excludedDirs = new Set();
79
+ for (const pattern of config.excludePatterns || []) {
80
+ // Match patterns like '**/dirname/**' or 'dirname/**'
81
+ const match = pattern.match(/(?:\*\*\/)?([^/*]+)(?:\/\*\*)?$/);
82
+ if (match && match[1] && !match[1].includes('*')) {
83
+ excludedDirs.add(match[1]);
84
+ }
85
+ }
86
+
87
+ try {
88
+ // Use fdir to scan workspace
89
+ const crawler = new fdir()
90
+ .withBasePath()
91
+ .withMaxDepth(10)
92
+ .exclude((dirName) => {
93
+ return excludedDirs.has(dirName);
94
+ })
95
+ .filter((filePath) => {
96
+ const ext = path.extname(filePath).toLowerCase();
97
+ return allowedExtensions.has(ext);
98
+ })
99
+ .crawl(workspaceDir);
100
+
101
+ const files = await crawler.withPromise();
102
+ const limitedFiles = files.slice(0, maxResults);
103
+
104
+ const resources = limitedFiles.map((filePath) => {
105
+ const relativePath = path.relative(workspaceDir, filePath);
106
+ return {
107
+ uri: pathToUri(filePath),
108
+ name: relativePath.replace(/\\/g, '/'),
109
+ mimeType: getMimeType(path.extname(filePath)),
110
+ };
111
+ });
112
+
113
+ return { resources };
114
+ } catch (error) {
115
+ console.error(`[Resources] Error listing resources: ${error.message}`);
116
+ return { resources: [] };
117
+ }
118
+ }
119
+
120
+ /**
121
+ * Read resource handler for MCP.
122
+ * @param {string} uri - Resource URI
123
+ * @param {object} config - Server configuration
124
+ * @returns {Promise<{contents: Array}>}
125
+ */
126
+ export async function handleReadResource(uri, config) {
127
+ const workspaceDir = config.searchDirectory;
128
+
129
+ try {
130
+ const filePath = uriToPath(uri);
131
+
132
+ // Security check: ensure path is within workspace
133
+ if (!isWithinWorkspace(filePath, workspaceDir)) {
134
+ throw new Error(`Access denied: ${uri} is outside workspace`);
135
+ }
136
+
137
+ // Check file exists
138
+ await fs.access(filePath);
139
+
140
+ // Read file content
141
+ const content = await fs.readFile(filePath, 'utf-8');
142
+
143
+ return {
144
+ contents: [
145
+ {
146
+ uri,
147
+ mimeType: getMimeType(path.extname(filePath)),
148
+ text: content,
149
+ },
150
+ ],
151
+ };
152
+ } catch (error) {
153
+ console.error(`[Resources] Error reading resource ${uri}: ${error.message}`);
154
+ throw error;
155
+ }
156
+ }
@@ -0,0 +1,265 @@
1
+ /**
2
+ * Runtime Workspace Switching Tool
3
+ *
4
+ * Changes the workspace path at runtime, reinitializing the cache
5
+ * and optionally triggering reindexing.
6
+ */
7
+
8
+ import path from 'path';
9
+ import fs from 'fs/promises';
10
+ import crypto from 'crypto';
11
+ import { acquireWorkspaceLock, releaseWorkspaceLock } from '../lib/server-lifecycle.js';
12
+
13
+ /**
14
+ * Generate a workspace-specific cache directory path
15
+ */
16
+ function getWorkspaceCacheDir(workspacePath, globalCacheDir) {
17
+ const normalized = path.resolve(workspacePath);
18
+ const hash = crypto.createHash('md5').update(normalized).digest('hex').slice(0, 12);
19
+ return path.join(globalCacheDir, 'heuristic-mcp', hash);
20
+ }
21
+
22
+ // MCP Tool definition
23
+ export function getToolDefinition() {
24
+ return {
25
+ name: 'f_set_workspace',
26
+ description:
27
+ 'Changes the current workspace path at runtime. This updates the search directory and cache, and optionally triggers a full reindex. Useful for multi-project workflows.',
28
+ inputSchema: {
29
+ type: 'object',
30
+ properties: {
31
+ workspacePath: {
32
+ type: 'string',
33
+ description: 'Absolute path to the new workspace directory',
34
+ },
35
+ reindex: {
36
+ type: 'boolean',
37
+ description: 'Whether to trigger a full reindex after switching (default: true)',
38
+ default: true,
39
+ },
40
+ },
41
+ required: ['workspacePath'],
42
+ },
43
+ annotations: {
44
+ title: 'Set Workspace',
45
+ readOnlyHint: false,
46
+ destructiveHint: false,
47
+ idempotentHint: true,
48
+ openWorldHint: false,
49
+ },
50
+ };
51
+ }
52
+
53
+ /**
54
+ * Create the SetWorkspace feature class
55
+ * This needs access to shared state (config, cache, indexer) to actually perform the switch
56
+ */
57
+ export class SetWorkspaceFeature {
58
+ constructor(config, cache, indexer, getGlobalCacheDir) {
59
+ this.config = config;
60
+ this.cache = cache;
61
+ this.indexer = indexer;
62
+ this.getGlobalCacheDir = getGlobalCacheDir;
63
+ }
64
+
65
+ async execute({ workspacePath, reindex = true }) {
66
+ // Validate workspace path
67
+ if (!workspacePath || typeof workspacePath !== 'string') {
68
+ return {
69
+ success: false,
70
+ error: 'workspacePath is required and must be a string',
71
+ };
72
+ }
73
+
74
+ const normalizedPath = path.resolve(workspacePath);
75
+
76
+ // Check if directory exists
77
+ try {
78
+ const stat = await fs.stat(normalizedPath);
79
+ if (!stat.isDirectory()) {
80
+ return {
81
+ success: false,
82
+ error: `Path is not a directory: ${normalizedPath}`,
83
+ };
84
+ }
85
+ } catch (err) {
86
+ return {
87
+ success: false,
88
+ error: `Cannot access directory: ${normalizedPath} (${err.message})`,
89
+ };
90
+ }
91
+
92
+ const previousWorkspace = this.config.searchDirectory;
93
+ const previousCache = this.config.cacheDirectory;
94
+
95
+ // Update config
96
+ this.config.searchDirectory = normalizedPath;
97
+
98
+ // Calculate new cache directory (match config.js behavior)
99
+ const globalCacheDir = this.getGlobalCacheDir();
100
+ let newCacheDir = getWorkspaceCacheDir(normalizedPath, globalCacheDir);
101
+
102
+ // Prefer legacy local cache if present
103
+ const legacyPath = path.join(normalizedPath, '.smart-coding-cache');
104
+ try {
105
+ const legacyStats = await fs.stat(legacyPath);
106
+ if (legacyStats.isDirectory()) {
107
+ newCacheDir = legacyPath;
108
+ }
109
+ } catch {
110
+ // ignore missing legacy cache
111
+ }
112
+ this.config.cacheDirectory = newCacheDir;
113
+
114
+ // Create cache directory if needed
115
+ try {
116
+ await fs.mkdir(newCacheDir, { recursive: true });
117
+ } catch (err) {
118
+ // Revert config on failure
119
+ this.config.searchDirectory = previousWorkspace;
120
+ this.config.cacheDirectory = previousCache;
121
+ return {
122
+ success: false,
123
+ error: `Failed to create cache directory: ${err.message}`,
124
+ };
125
+ }
126
+
127
+ // Acquire new workspace lock before proceeding
128
+ const lock = await acquireWorkspaceLock({
129
+ cacheDirectory: newCacheDir,
130
+ workspaceDir: normalizedPath,
131
+ });
132
+ if (!lock.acquired) {
133
+ // Revert config on failure
134
+ this.config.searchDirectory = previousWorkspace;
135
+ this.config.cacheDirectory = previousCache;
136
+ return {
137
+ success: false,
138
+ error: `Workspace is already locked by another server (pid ${lock.ownerPid ?? 'unknown'})`,
139
+ };
140
+ }
141
+ let indexerUpdateError = null;
142
+
143
+ // Update indexer's workspace root and related state
144
+ if (this.indexer) {
145
+ if (typeof this.indexer.terminateWorkers === 'function') {
146
+ try {
147
+ await this.indexer.terminateWorkers();
148
+ } catch (err) {
149
+ console.warn(`[SetWorkspace] Failed to terminate workers: ${err.message}`);
150
+ }
151
+ }
152
+ try {
153
+ if (typeof this.indexer.updateWorkspaceState === 'function') {
154
+ await this.indexer.updateWorkspaceState({ restartWatcher: true });
155
+ } else {
156
+ this.indexer.workspaceRoot = normalizedPath;
157
+ this.indexer.workspaceRootReal = null; // Reset cached realpath
158
+ if (this.config.watchFiles && typeof this.indexer.setupFileWatcher === 'function') {
159
+ await this.indexer.setupFileWatcher();
160
+ }
161
+ }
162
+ } catch (err) {
163
+ indexerUpdateError = err;
164
+ }
165
+ }
166
+
167
+ if (indexerUpdateError) {
168
+ // Roll back config + lock on failure to avoid partial switch
169
+ this.config.searchDirectory = previousWorkspace;
170
+ this.config.cacheDirectory = previousCache;
171
+ await releaseWorkspaceLock({ cacheDirectory: newCacheDir });
172
+ if (this.indexer) {
173
+ try {
174
+ if (typeof this.indexer.updateWorkspaceState === 'function') {
175
+ await this.indexer.updateWorkspaceState({ restartWatcher: true });
176
+ } else {
177
+ this.indexer.workspaceRoot = previousWorkspace;
178
+ this.indexer.workspaceRootReal = null;
179
+ if (this.config.watchFiles && typeof this.indexer.setupFileWatcher === 'function') {
180
+ await this.indexer.setupFileWatcher();
181
+ }
182
+ }
183
+ } catch (rollbackErr) {
184
+ console.warn(
185
+ `[SetWorkspace] Failed to rollback indexer state: ${rollbackErr.message}`
186
+ );
187
+ }
188
+ }
189
+ return {
190
+ success: false,
191
+ error: `Failed to update workspace state: ${indexerUpdateError.message}`,
192
+ };
193
+ }
194
+
195
+ // Release old workspace lock after successful indexer update
196
+ if (previousCache) {
197
+ await releaseWorkspaceLock({ cacheDirectory: previousCache });
198
+ }
199
+
200
+ // Re-initialize cache for new workspace if cache has a load method
201
+ if (this.cache && typeof this.cache.load === 'function') {
202
+ try {
203
+ await this.cache.load();
204
+ } catch (err) {
205
+ console.warn(`[SetWorkspace] Failed to load cache: ${err.message}`);
206
+ }
207
+ }
208
+
209
+ // Optionally trigger reindex
210
+ let reindexStatus = null;
211
+ if (reindex && this.indexer && typeof this.indexer.indexAll === 'function') {
212
+ try {
213
+ // Start indexing asynchronously
214
+ this.indexer.indexAll().catch((err) => {
215
+ console.warn(`[SetWorkspace] Reindex failed: ${err.message}`);
216
+ });
217
+ reindexStatus = 'started';
218
+ } catch (err) {
219
+ reindexStatus = `failed: ${err.message}`;
220
+ }
221
+ } else if (!reindex) {
222
+ reindexStatus = 'skipped';
223
+ }
224
+
225
+ return {
226
+ success: true,
227
+ previousWorkspace,
228
+ newWorkspace: normalizedPath,
229
+ cacheDirectory: newCacheDir,
230
+ reindexStatus,
231
+ };
232
+ }
233
+ }
234
+
235
+ // Tool handler (needs instance context, so this is a factory)
236
+ export function createHandleToolCall(featureInstance) {
237
+ return async (request) => {
238
+ const args = request.params?.arguments || {};
239
+ const { workspacePath, reindex } = args;
240
+
241
+ const result = await featureInstance.execute({
242
+ workspacePath,
243
+ reindex: reindex !== false, // Default to true
244
+ });
245
+
246
+ if (result.success) {
247
+ let message = `✓ Workspace switched to: **${result.newWorkspace}**\n`;
248
+ message += `\n- Previous: \`${result.previousWorkspace || '(none)'}\``;
249
+ message += `\n- Cache: \`${result.cacheDirectory}\``;
250
+ if (result.reindexStatus) {
251
+ message += `\n- Reindex: ${result.reindexStatus}`;
252
+ }
253
+ return {
254
+ content: [{ type: 'text', text: message }],
255
+ };
256
+ } else {
257
+ return {
258
+ content: [{ type: 'text', text: `Error: ${result.error}` }],
259
+ };
260
+ }
261
+ };
262
+ }
263
+
264
+ // Export for use in registration
265
+ export { getWorkspaceCacheDir };
package/index.js CHANGED
@@ -50,18 +50,18 @@ import * as ClearCacheFeature from './features/clear-cache.js';
50
50
  import * as FindSimilarCodeFeature from './features/find-similar-code.js';
51
51
  import * as AnnConfigFeature from './features/ann-config.js';
52
52
  import * as PackageVersionFeature from './features/package-version.js';
53
- import * as SetWorkspaceFeature from './features/set-workspace.js';
54
- import { handleListResources, handleReadResource } from './features/resources.js';
55
- import { getWorkspaceEnvKeys } from './lib/workspace-env.js';
56
-
57
- import {
58
- MEMORY_LOG_INTERVAL_MS,
59
- ONNX_THREAD_LIMIT,
60
- BACKGROUND_INDEX_DELAY_MS,
61
- } from './lib/constants.js';
62
- const PID_FILE_NAME = '.heuristic-mcp.pid';
63
-
64
- async function readLogTail(logPath, maxLines = 2000) {
53
+ import * as SetWorkspaceFeature from './features/set-workspace.js';
54
+ import { handleListResources, handleReadResource } from './features/resources.js';
55
+ import { getWorkspaceEnvKeys } from './lib/workspace-env.js';
56
+
57
+ import {
58
+ MEMORY_LOG_INTERVAL_MS,
59
+ ONNX_THREAD_LIMIT,
60
+ BACKGROUND_INDEX_DELAY_MS,
61
+ } from './lib/constants.js';
62
+ const PID_FILE_NAME = '.heuristic-mcp.pid';
63
+
64
+ async function readLogTail(logPath, maxLines = 2000) {
65
65
  const data = await fs.readFile(logPath, 'utf-8');
66
66
  if (!data) return [];
67
67
  const lines = data.split(/\r?\n/).filter(Boolean);
@@ -119,75 +119,75 @@ async function printMemorySnapshot(workspaceDir) {
119
119
  // Arguments parsed in main()
120
120
 
121
121
  // Global state
122
- let embedder = null;
123
- let unloadMainEmbedder = null; // Function to unload the embedding model
124
- let cache = null;
125
- let indexer = null;
126
- let hybridSearch = null;
127
- let config = null;
128
- let setWorkspaceFeatureInstance = null;
129
- let autoWorkspaceSwitchPromise = null;
130
-
131
- async function resolveWorkspaceFromEnvValue(rawValue) {
132
- if (!rawValue || rawValue.includes('${')) return null;
133
- const resolved = path.resolve(rawValue);
134
- try {
135
- const stats = await fs.stat(resolved);
136
- if (!stats.isDirectory()) return null;
137
- return resolved;
138
- } catch {
139
- return null;
140
- }
141
- }
142
-
143
- async function detectRuntimeWorkspaceFromEnv() {
144
- for (const key of getWorkspaceEnvKeys()) {
145
- const workspacePath = await resolveWorkspaceFromEnvValue(process.env[key]);
146
- if (workspacePath) {
147
- return { workspacePath, envKey: key };
148
- }
149
- }
150
-
151
- return null;
152
- }
153
-
154
- async function maybeAutoSwitchWorkspace(request) {
155
- if (process.env.VITEST === 'true' || process.env.NODE_ENV === 'test') return;
156
- if (!setWorkspaceFeatureInstance || !config?.searchDirectory) return;
157
- if (request?.params?.name === 'f_set_workspace') return;
158
-
159
- const detected = await detectRuntimeWorkspaceFromEnv();
160
- if (!detected) return;
161
-
162
- const currentWorkspace = path.resolve(config.searchDirectory);
163
- if (detected.workspacePath === currentWorkspace) return;
164
-
165
- if (autoWorkspaceSwitchPromise) {
166
- await autoWorkspaceSwitchPromise;
167
- return;
168
- }
169
-
170
- autoWorkspaceSwitchPromise = (async () => {
171
- console.info(
172
- `[Server] Auto-switching workspace from ${currentWorkspace} to ${detected.workspacePath} (env ${detected.envKey})`
173
- );
174
- const result = await setWorkspaceFeatureInstance.execute({
175
- workspacePath: detected.workspacePath,
176
- reindex: false,
177
- });
178
- if (!result.success) {
179
- console.warn(
180
- `[Server] Auto workspace switch failed (env ${detected.envKey}): ${result.error}`
181
- );
182
- }
183
- })();
184
-
185
- try {
186
- await autoWorkspaceSwitchPromise;
187
- } finally {
188
- autoWorkspaceSwitchPromise = null;
189
- }
190
- }
122
+ let embedder = null;
123
+ let unloadMainEmbedder = null; // Function to unload the embedding model
124
+ let cache = null;
125
+ let indexer = null;
126
+ let hybridSearch = null;
127
+ let config = null;
128
+ let setWorkspaceFeatureInstance = null;
129
+ let autoWorkspaceSwitchPromise = null;
130
+
131
+ async function resolveWorkspaceFromEnvValue(rawValue) {
132
+ if (!rawValue || rawValue.includes('${')) return null;
133
+ const resolved = path.resolve(rawValue);
134
+ try {
135
+ const stats = await fs.stat(resolved);
136
+ if (!stats.isDirectory()) return null;
137
+ return resolved;
138
+ } catch {
139
+ return null;
140
+ }
141
+ }
142
+
143
+ async function detectRuntimeWorkspaceFromEnv() {
144
+ for (const key of getWorkspaceEnvKeys()) {
145
+ const workspacePath = await resolveWorkspaceFromEnvValue(process.env[key]);
146
+ if (workspacePath) {
147
+ return { workspacePath, envKey: key };
148
+ }
149
+ }
150
+
151
+ return null;
152
+ }
153
+
154
+ async function maybeAutoSwitchWorkspace(request) {
155
+ if (process.env.VITEST === 'true' || process.env.NODE_ENV === 'test') return;
156
+ if (!setWorkspaceFeatureInstance || !config?.searchDirectory) return;
157
+ if (request?.params?.name === 'f_set_workspace') return;
158
+
159
+ const detected = await detectRuntimeWorkspaceFromEnv();
160
+ if (!detected) return;
161
+
162
+ const currentWorkspace = path.resolve(config.searchDirectory);
163
+ if (detected.workspacePath === currentWorkspace) return;
164
+
165
+ if (autoWorkspaceSwitchPromise) {
166
+ await autoWorkspaceSwitchPromise;
167
+ return;
168
+ }
169
+
170
+ autoWorkspaceSwitchPromise = (async () => {
171
+ console.info(
172
+ `[Server] Auto-switching workspace from ${currentWorkspace} to ${detected.workspacePath} (env ${detected.envKey})`
173
+ );
174
+ const result = await setWorkspaceFeatureInstance.execute({
175
+ workspacePath: detected.workspacePath,
176
+ reindex: false,
177
+ });
178
+ if (!result.success) {
179
+ console.warn(
180
+ `[Server] Auto workspace switch failed (env ${detected.envKey}): ${result.error}`
181
+ );
182
+ }
183
+ })();
184
+
185
+ try {
186
+ await autoWorkspaceSwitchPromise;
187
+ } finally {
188
+ autoWorkspaceSwitchPromise = null;
189
+ }
190
+ }
191
191
 
192
192
  // Feature registry - ordered by priority (semantic_search first as primary tool)
193
193
  const features = [
@@ -443,15 +443,15 @@ async function initialize(workspaceDir) {
443
443
  // Features 5 (PackageVersion) doesn't need instance
444
444
 
445
445
  // Initialize SetWorkspace feature with shared state
446
- const setWorkspaceInstance = new SetWorkspaceFeature.SetWorkspaceFeature(
447
- config,
448
- cache,
449
- indexer,
450
- getGlobalCacheDir
451
- );
452
- setWorkspaceFeatureInstance = setWorkspaceInstance;
453
- features[6].instance = setWorkspaceInstance;
454
- features[6].handler = SetWorkspaceFeature.createHandleToolCall(setWorkspaceInstance);
446
+ const setWorkspaceInstance = new SetWorkspaceFeature.SetWorkspaceFeature(
447
+ config,
448
+ cache,
449
+ indexer,
450
+ getGlobalCacheDir
451
+ );
452
+ setWorkspaceFeatureInstance = setWorkspaceInstance;
453
+ features[6].instance = setWorkspaceInstance;
454
+ features[6].handler = SetWorkspaceFeature.createHandleToolCall(setWorkspaceInstance);
455
455
 
456
456
  // Attach hybridSearch to server for cross-feature access (e.g. cache invalidation)
457
457
  server.hybridSearch = hybridSearch;
@@ -530,12 +530,12 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
530
530
  return { tools };
531
531
  });
532
532
 
533
- // Handle tool calls
534
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
535
- await maybeAutoSwitchWorkspace(request);
536
-
537
- for (const feature of features) {
538
- const toolDef = feature.module.getToolDefinition(config);
533
+ // Handle tool calls
534
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
535
+ await maybeAutoSwitchWorkspace(request);
536
+
537
+ for (const feature of features) {
538
+ const toolDef = feature.module.getToolDefinition(config);
539
539
 
540
540
  if (request.params.name === toolDef.name) {
541
541
  // Safety check: handler may be null if initialization is incomplete
package/lib/cache-ops.js CHANGED
@@ -1,22 +1,22 @@
1
- import fs from 'fs/promises';
2
- import { loadConfig } from './config.js';
3
- import { clearStaleCaches } from './cache-utils.js';
4
-
5
- export async function clearCache(workspaceDir) {
6
- const effectiveWorkspace = workspaceDir || process.cwd();
7
- const activeConfig = await loadConfig(effectiveWorkspace);
8
-
9
- if (!activeConfig.enableCache) {
10
- console.info('[Cache] Cache disabled (enableCache=false); nothing to clear.');
11
- return;
12
- }
13
-
14
- try {
15
- await fs.rm(activeConfig.cacheDirectory, { recursive: true, force: true });
16
- console.info(`[Cache] Cleared cache directory: ${activeConfig.cacheDirectory}`);
17
- await clearStaleCaches();
18
- } catch (err) {
19
- console.error(`[Cache] Failed to clear cache: ${err.message}`);
20
- process.exit(1);
21
- }
22
- }
1
+ import fs from 'fs/promises';
2
+ import { loadConfig } from './config.js';
3
+ import { clearStaleCaches } from './cache-utils.js';
4
+
5
+ export async function clearCache(workspaceDir) {
6
+ const effectiveWorkspace = workspaceDir || process.cwd();
7
+ const activeConfig = await loadConfig(effectiveWorkspace);
8
+
9
+ if (!activeConfig.enableCache) {
10
+ console.info('[Cache] Cache disabled (enableCache=false); nothing to clear.');
11
+ return;
12
+ }
13
+
14
+ try {
15
+ await fs.rm(activeConfig.cacheDirectory, { recursive: true, force: true });
16
+ console.info(`[Cache] Cleared cache directory: ${activeConfig.cacheDirectory}`);
17
+ await clearStaleCaches();
18
+ } catch (err) {
19
+ console.error(`[Cache] Failed to clear cache: ${err.message}`);
20
+ process.exit(1);
21
+ }
22
+ }