@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.
- package/README.md +104 -104
- package/config.jsonc +173 -173
- package/features/ann-config.js +131 -0
- package/features/clear-cache.js +84 -0
- package/features/find-similar-code.js +291 -0
- package/features/hybrid-search.js +544 -0
- package/features/index-codebase.js +3268 -0
- package/features/lifecycle.js +1189 -0
- package/features/package-version.js +302 -0
- package/features/register.js +408 -0
- package/features/resources.js +156 -0
- package/features/set-workspace.js +265 -0
- package/index.js +96 -96
- package/lib/cache-ops.js +22 -22
- package/lib/cache-utils.js +565 -565
- package/lib/cache.js +1870 -1870
- package/lib/call-graph.js +396 -396
- package/lib/cli.js +1 -1
- package/lib/config.js +517 -517
- package/lib/constants.js +39 -39
- package/lib/embed-query-process.js +7 -7
- package/lib/embedding-process.js +7 -7
- package/lib/embedding-worker.js +299 -299
- package/lib/ignore-patterns.js +316 -316
- package/lib/json-worker.js +14 -14
- package/lib/json-writer.js +337 -337
- package/lib/logging.js +164 -164
- package/lib/memory-logger.js +13 -13
- package/lib/onnx-backend.js +193 -193
- package/lib/project-detector.js +84 -84
- package/lib/server-lifecycle.js +165 -165
- package/lib/settings-editor.js +754 -754
- package/lib/tokenizer.js +256 -256
- package/lib/utils.js +428 -428
- package/lib/vector-store-binary.js +627 -627
- package/lib/vector-store-sqlite.js +95 -95
- package/lib/workspace-env.js +28 -28
- package/mcp_config.json +9 -9
- package/package.json +86 -75
- package/scripts/clear-cache.js +20 -0
- package/scripts/download-model.js +43 -0
- package/scripts/mcp-launcher.js +49 -0
- package/scripts/postinstall.js +12 -0
- package/search-configs.js +36 -36
- package/.prettierrc +0 -7
- package/debug-pids.js +0 -30
- package/eslint.config.js +0 -36
- package/specs/plan.md +0 -23
- 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
|
+
}
|