@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.
@@ -1,9 +1,4 @@
1
- /**
2
- * SQLite Vector Store
3
- *
4
- * An alternative to binary/JSON vector stores using SQLite for persistence.
5
- * Provides ACID transactions, simpler concurrent access, and query flexibility.
6
- */
1
+
7
2
 
8
3
  import Database from 'better-sqlite3';
9
4
  import path from 'path';
@@ -42,7 +37,7 @@ async function bestEffortUnlink(targetPath) {
42
37
  try {
43
38
  await retryUnlink(targetPath);
44
39
  } catch {
45
- // ignore cleanup failures (e.g. antivirus locks on Windows)
40
+
46
41
  }
47
42
  }
48
43
 
@@ -63,17 +58,14 @@ async function retryRename(fromPath, toPath, retries = SQLITE_FILE_RETRY_COUNT)
63
58
  }
64
59
  }
65
60
 
66
- /**
67
- * SQLite-backed vector store for embeddings cache.
68
- * Follows the same API pattern as BinaryVectorStore for compatibility.
69
- */
61
+
70
62
  export class SqliteVectorStore {
71
63
  constructor({ db, dim, count }) {
72
64
  this.db = db;
73
65
  this.dim = dim;
74
66
  this.count = count;
75
67
 
76
- // Prepared statements for fast access
68
+
77
69
  this._stmtGetChunk = db.prepare(`
78
70
  SELECT file, startLine, endLine, content, vector FROM chunks WHERE id = ?
79
71
  `);
@@ -83,9 +75,7 @@ export class SqliteVectorStore {
83
75
  this._stmtGetChunksForFile = db.prepare(`SELECT id FROM chunks WHERE file = ?`);
84
76
  }
85
77
 
86
- /**
87
- * Close the database connection
88
- */
78
+
89
79
  close() {
90
80
  if (this._closed) return;
91
81
  this._closed = true;
@@ -94,44 +84,40 @@ export class SqliteVectorStore {
94
84
  this.db.close();
95
85
  }
96
86
  } catch {
97
- // Ignore close errors (e.g., already closed, concurrent close)
87
+
98
88
  }
99
89
  }
100
90
 
101
- /**
102
- * Get the path to the SQLite file
103
- */
91
+
104
92
  static getPath(cacheDir) {
105
93
  return path.join(cacheDir, SQLITE_FILE);
106
94
  }
107
95
 
108
- /**
109
- * Load an existing SQLite vector store from disk
110
- */
96
+
111
97
  static async load(cacheDir, _options = {}) {
112
98
  const dbPath = SqliteVectorStore.getPath(cacheDir);
113
99
 
114
100
  try {
115
101
  await fs.access(dbPath);
116
102
  } catch {
117
- return null; // File doesn't exist
103
+ return null;
118
104
  }
119
105
 
120
106
  let db;
121
107
  try {
122
108
  db = new Database(dbPath, { readonly: true });
123
109
  } catch (err) {
124
- // Database file may be corrupted or locked
110
+
125
111
  console.warn(`[SQLite] Failed to open database: ${err.message}`);
126
112
  return null;
127
113
  }
128
114
 
129
- // Read metadata
115
+
130
116
  let meta;
131
117
  try {
132
118
  meta = db.prepare(`SELECT key, value FROM metadata`).all();
133
119
  } catch (err) {
134
- // Database schema may be corrupted
120
+
135
121
  console.warn(`[SQLite] Failed to read metadata: ${err.message}`);
136
122
  db.close();
137
123
  return null;
@@ -141,7 +127,7 @@ export class SqliteVectorStore {
141
127
  const version = parseInt(metaMap.get('version') || '0', 10);
142
128
  if (version !== STORE_VERSION) {
143
129
  db.close();
144
- return null; // Version mismatch, need reindex
130
+ return null;
145
131
  }
146
132
 
147
133
  const dim = parseInt(metaMap.get('dim') || '0', 10);
@@ -150,16 +136,12 @@ export class SqliteVectorStore {
150
136
  return new SqliteVectorStore({ db, dim, count });
151
137
  }
152
138
 
153
- /**
154
- * Get the number of chunks in the store
155
- */
139
+
156
140
  length() {
157
141
  return this.count;
158
142
  }
159
143
 
160
- /**
161
- * Get a chunk record by index (0-based)
162
- */
144
+
163
145
  getRecord(index) {
164
146
  if (index < 0 || index >= this.count) return null;
165
147
 
@@ -173,27 +155,22 @@ export class SqliteVectorStore {
173
155
  };
174
156
  }
175
157
 
176
- /**
177
- * Get a vector by index (0-based)
178
- * Returns Float32Array
179
- */
158
+
180
159
  getVector(index) {
181
160
  if (index < 0 || index >= this.count) return null;
182
161
 
183
162
  const row = this._stmtGetVector.get(index);
184
163
  if (!row || !row.vector) return null;
185
164
 
186
- // Vector is stored as a Buffer of Float32 values
165
+
187
166
  const expectedBytes = this.dim * Float32Array.BYTES_PER_ELEMENT;
188
167
  if (row.vector.byteLength < expectedBytes) return null;
189
- // Copy the buffer to avoid issues with SQLite reusing internal buffers
168
+
190
169
  const view = new Float32Array(row.vector.buffer, row.vector.byteOffset, this.dim);
191
170
  return new Float32Array(view);
192
171
  }
193
172
 
194
- /**
195
- * Get content by index (0-based)
196
- */
173
+
197
174
  getContent(index) {
198
175
  if (index < 0 || index >= this.count) return null;
199
176
 
@@ -201,9 +178,7 @@ export class SqliteVectorStore {
201
178
  return row ? row.content : null;
202
179
  }
203
180
 
204
- /**
205
- * Get all chunks as lightweight views (for search iteration)
206
- */
181
+
207
182
  toChunkViews({ includeContent = false, includeVector = true } = {}) {
208
183
  const views = [];
209
184
  const stmt = this.db.prepare(`
@@ -227,7 +202,7 @@ export class SqliteVectorStore {
227
202
  if (includeVector && row.vector) {
228
203
  const expectedBytes = this.dim * Float32Array.BYTES_PER_ELEMENT;
229
204
  if (row.vector.byteLength >= expectedBytes) {
230
- // Copy the buffer to avoid issues with SQLite reusing internal buffers
205
+
231
206
  const bufferView = new Float32Array(
232
207
  row.vector.buffer,
233
208
  row.vector.byteOffset,
@@ -243,9 +218,7 @@ export class SqliteVectorStore {
243
218
  return views;
244
219
  }
245
220
 
246
- /**
247
- * Get all unique file paths and their chunk indices
248
- */
221
+
249
222
  getAllFileIndices() {
250
223
  const fileIndices = new Map();
251
224
 
@@ -258,12 +231,7 @@ export class SqliteVectorStore {
258
231
  return fileIndices;
259
232
  }
260
233
 
261
- /**
262
- * Write chunks to a new SQLite database
263
- * @param {string} cacheDir - Directory to write the database
264
- * @param {Array} chunks - Array of chunk objects with vector, file, startLine, endLine, content
265
- * @param {Object} options - { getContent, preRename }
266
- */
234
+
267
235
  static async write(cacheDir, chunks, { getContent, getVector, preRename } = {}) {
268
236
  if (!chunks || chunks.length === 0) {
269
237
  return null;
@@ -331,14 +299,14 @@ export class SqliteVectorStore {
331
299
  ? (await resolveVector(denseChunks[0], denseSourceIndices[0])).length
332
300
  : 0;
333
301
 
334
- // Create new database
302
+
335
303
  const db = new Database(writePath);
336
304
 
337
- // Enable WAL mode for better concurrent read performance (use DELETE on Windows to reduce locks)
305
+
338
306
  db.pragma(`journal_mode = ${useTemp ? 'WAL' : 'DELETE'}`);
339
307
  db.pragma('synchronous = NORMAL');
340
308
 
341
- // Create tables
309
+
342
310
  if (!useTemp) {
343
311
  db.exec(`
344
312
  DROP TABLE IF EXISTS metadata;
@@ -364,14 +332,14 @@ export class SqliteVectorStore {
364
332
  CREATE INDEX idx_chunks_file ON chunks(file);
365
333
  `);
366
334
 
367
- // Insert metadata
335
+
368
336
  const insertMeta = db.prepare(`INSERT INTO metadata (key, value) VALUES (?, ?)`);
369
337
  insertMeta.run('version', String(STORE_VERSION));
370
338
  insertMeta.run('dim', String(dim));
371
339
  insertMeta.run('count', String(denseChunks.length));
372
340
  insertMeta.run('createdAt', new Date().toISOString());
373
341
 
374
- // Insert chunks in a transaction for speed without pre-materializing all vectors/content.
342
+
375
343
  const insertChunk = db.prepare(`
376
344
  INSERT INTO chunks (id, file, startLine, endLine, content, vector)
377
345
  VALUES (?, ?, ?, ?, ?, ?)
@@ -405,31 +373,31 @@ export class SqliteVectorStore {
405
373
  try {
406
374
  db.exec('ROLLBACK');
407
375
  } catch {
408
- // ignore rollback errors
376
+
409
377
  }
410
378
  throw error;
411
379
  }
412
380
 
413
- // Optimize the database
381
+
414
382
  db.exec('ANALYZE');
415
383
  db.close();
416
384
  if (process.platform === 'win32') {
417
- // Windows needs more time to release file locks after WAL mode close
385
+
418
386
  await new Promise((resolve) => setTimeout(resolve, 200));
419
387
  }
420
388
 
421
- // Call preRename hook if provided (for atomicity coordination)
389
+
422
390
  if (typeof preRename === 'function') {
423
391
  await preRename();
424
392
  }
425
393
 
426
- // Atomic rename (temp path only)
394
+
427
395
  if (useTemp) {
428
396
  await retryUnlink(dbPath);
429
397
  await retryRename(tempPath, dbPath);
430
398
  }
431
399
 
432
- // Clean up WAL files from temp
400
+
433
401
  if (useTemp) {
434
402
  await bestEffortUnlink(tempPath + '-wal');
435
403
  await bestEffortUnlink(tempPath + '-shm');
@@ -438,7 +406,7 @@ export class SqliteVectorStore {
438
406
  await bestEffortUnlink(dbPath + '-shm');
439
407
  }
440
408
 
441
- // Return a new instance opened readonly
409
+
442
410
  return SqliteVectorStore.load(cacheDir);
443
411
  }
444
412
  }
@@ -0,0 +1,36 @@
1
+ import crypto from 'crypto';
2
+ import path from 'path';
3
+
4
+ export function normalizeWorkspacePathForCacheKey(workspacePath) {
5
+ const resolved = path.resolve(workspacePath);
6
+ if (process.platform !== 'win32') {
7
+ return resolved;
8
+ }
9
+
10
+ // Windows paths are case-insensitive for drive letters; normalize only the
11
+ // drive prefix so F:\repo and f:\repo map to the same cache key while
12
+ // preserving existing segment casing for backward compatibility.
13
+ if (/^[A-Za-z]:/.test(resolved)) {
14
+ return `${resolved[0].toLowerCase()}${resolved.slice(1)}`;
15
+ }
16
+
17
+ return resolved;
18
+ }
19
+
20
+ export function getWorkspaceCacheKey(workspacePath) {
21
+ const normalized = normalizeWorkspacePathForCacheKey(workspacePath);
22
+ return crypto.createHash('md5').update(normalized).digest('hex').slice(0, 12);
23
+ }
24
+
25
+ export function getLegacyWorkspaceCacheKey(workspacePath) {
26
+ const resolved = path.resolve(workspacePath);
27
+ return crypto.createHash('md5').update(resolved).digest('hex').slice(0, 12);
28
+ }
29
+
30
+ export function getWorkspaceCachePath(workspacePath, globalCacheRoot) {
31
+ return path.join(globalCacheRoot, 'heuristic-mcp', getWorkspaceCacheKey(workspacePath));
32
+ }
33
+
34
+ export function getLegacyWorkspaceCachePath(workspacePath, globalCacheRoot) {
35
+ return path.join(globalCacheRoot, 'heuristic-mcp', getLegacyWorkspaceCacheKey(workspacePath));
36
+ }
@@ -1,10 +1,26 @@
1
- import {
2
- DYNAMIC_WORKSPACE_ENV_PREFIX,
3
- WORKSPACE_ENV_KEY_PATTERN,
4
- WORKSPACE_ENV_VARS,
5
- } from './constants.js';
6
-
7
- export function scoreWorkspaceEnvKey(key) {
1
+ import {
2
+ DYNAMIC_WORKSPACE_ENV_PREFIXES,
3
+ WORKSPACE_ENV_GENERIC_DISCOVERY_PATTERN,
4
+ WORKSPACE_ENV_KEY_PATTERN,
5
+ WORKSPACE_ENV_VARS,
6
+ } from './constants.js';
7
+
8
+ const EXCLUDED_DYNAMIC_WORKSPACE_ENV_KEYS = new Set([
9
+ 'ANTIGRAVITY_EDITOR_APP_ROOT',
10
+ ]);
11
+
12
+ function isTruthy(value) {
13
+ return /^(1|true|yes|on)$/i.test(String(value || '').trim());
14
+ }
15
+
16
+ export function isDynamicWorkspaceEnvEnabled(env = process.env, options = {}) {
17
+ if (typeof options.enableDynamic === 'boolean') {
18
+ return options.enableDynamic;
19
+ }
20
+ return isTruthy(env.HEURISTIC_MCP_ENABLE_DYNAMIC_WORKSPACE_ENV);
21
+ }
22
+
23
+ export function scoreWorkspaceEnvKey(key) {
8
24
  const upper = String(key || '').toUpperCase();
9
25
  let score = 0;
10
26
  if (upper.includes('WORKSPACE')) score += 8;
@@ -15,14 +31,39 @@ export function scoreWorkspaceEnvKey(key) {
15
31
  return score;
16
32
  }
17
33
 
18
- export function getDynamicWorkspaceEnvKeys(env = process.env) {
19
- return Object.keys(env)
20
- .filter((key) => key.startsWith(DYNAMIC_WORKSPACE_ENV_PREFIX))
21
- .filter((key) => WORKSPACE_ENV_KEY_PATTERN.test(key))
22
- .filter((key) => !WORKSPACE_ENV_VARS.includes(key))
34
+ function hasDynamicWorkspacePrefix(key) {
35
+ return DYNAMIC_WORKSPACE_ENV_PREFIXES.some((prefix) => key.startsWith(prefix));
36
+ }
37
+
38
+ export function getDynamicWorkspaceEnvKeys(env = process.env, options = {}) {
39
+ if (!isDynamicWorkspaceEnvEnabled(env, options)) {
40
+ return [];
41
+ }
42
+
43
+ return Object.keys(env)
44
+ .filter((key) => !EXCLUDED_DYNAMIC_WORKSPACE_ENV_KEYS.has(String(key || '').toUpperCase()))
45
+ .filter((key) => !WORKSPACE_ENV_VARS.includes(key))
46
+ .filter((key) => {
47
+ const providerSpecific = hasDynamicWorkspacePrefix(key) && WORKSPACE_ENV_KEY_PATTERN.test(key);
48
+ const genericWorkspace = WORKSPACE_ENV_GENERIC_DISCOVERY_PATTERN.test(key);
49
+ return providerSpecific || genericWorkspace;
50
+ })
23
51
  .sort((a, b) => scoreWorkspaceEnvKey(b) - scoreWorkspaceEnvKey(a));
24
52
  }
25
53
 
26
- export function getWorkspaceEnvKeys(env = process.env) {
27
- return [...WORKSPACE_ENV_VARS, ...getDynamicWorkspaceEnvKeys(env)];
54
+ export function getWorkspaceEnvKeys(env = process.env, options = {}) {
55
+ return [...WORKSPACE_ENV_VARS, ...getDynamicWorkspaceEnvKeys(env, options)];
56
+ }
57
+
58
+
59
+ export function getWorkspaceEnvDiagnosticKeys(env = process.env, options = {}) {
60
+ const prioritized = getWorkspaceEnvKeys(env, options);
61
+ const prioritizedSet = new Set(prioritized);
62
+
63
+ const extraKeys = Object.keys(env)
64
+ .filter((key) => !prioritizedSet.has(key))
65
+ .filter((key) => WORKSPACE_ENV_KEY_PATTERN.test(key))
66
+ .sort((a, b) => scoreWorkspaceEnvKey(b) - scoreWorkspaceEnvKey(a));
67
+
68
+ return [...prioritized, ...extraKeys];
28
69
  }
package/package.json CHANGED
@@ -1,86 +1,86 @@
1
- {
2
- "name": "@softerist/heuristic-mcp",
3
- "version": "3.0.17",
4
- "description": "An enhanced MCP server providing intelligent semantic code search with find-similar-code, recency ranking, and improved chunking. Fork of smart-coding-mcp.",
5
- "type": "module",
6
- "main": "index.js",
7
- "bin": {
8
- "heuristic-mcp": "index.js"
9
- },
10
- "files": [
11
- "index.js",
12
- "config.jsonc",
13
- "mcp_config.json",
14
- "search-configs.js",
15
- "lib/",
16
- "features/",
17
- "scripts/",
18
- "README.md",
19
- "LICENSE"
20
- ],
21
- "scripts": {
22
- "start": "node --expose-gc index.js",
23
- "dev": "node --expose-gc --watch index.js",
24
- "test": "vitest run",
25
- "test:watch": "vitest",
26
- "clean": "node scripts/clear-cache.js",
27
- "lint": "eslint .",
28
- "format": "prettier --write .",
29
- "postinstall": "node scripts/postinstall.js && node scripts/download-model.js"
30
- },
31
- "keywords": [
32
- "mcp",
33
- "semantic-search",
34
- "code-search",
35
- "embeddings",
36
- "ai",
37
- "model-context-protocol",
38
- "hybrid-search",
39
- "code-intelligence",
40
- "cursor",
41
- "vscode",
42
- "claude",
43
- "codex",
44
- "openai",
45
- "gemini",
46
- "anthropic",
47
- "antigravity",
48
- "heuristic"
49
- ],
50
- "author": {
51
- "name": "Softerist",
52
- "url": "https://github.com/softerist"
53
- },
54
- "repository": {
55
- "type": "git",
56
- "url": "git+https://github.com/softerist/heuristic-mcp.git"
57
- },
58
- "homepage": "https://github.com/softerist/heuristic-mcp#readme",
59
- "license": "MIT",
60
- "dependencies": {
61
- "@huggingface/transformers": "^3.8.1",
62
- "@modelcontextprotocol/sdk": "^1.0.4",
63
- "better-sqlite3": "^12.6.2",
64
- "chokidar": "^3.5.3",
65
- "fdir": "^6.5.0",
66
- "ignore": "^7.0.5",
67
- "punycode": "^2.3.1"
68
- },
69
- "optionalDependencies": {
70
- "hnswlib-node": "^3.0.0"
71
- },
72
- "engines": {
73
- "node": ">=18.0.0"
74
- },
75
- "overrides": {
76
- "punycode": "^2.3.1"
77
- },
78
- "devDependencies": {
79
- "@eslint/js": "^9.39.2",
80
- "@vitest/coverage-v8": "^4.0.18",
81
- "eslint": "^9.39.2",
82
- "globals": "^17.1.0",
83
- "prettier": "^3.8.1",
84
- "vitest": "^4.0.16"
85
- }
86
- }
1
+ {
2
+ "name": "@softerist/heuristic-mcp",
3
+ "version": "3.1.0",
4
+ "description": "An enhanced MCP server providing intelligent semantic code search with find-similar-code, recency ranking, and improved chunking. Fork of smart-coding-mcp.",
5
+ "type": "module",
6
+ "main": "index.js",
7
+ "bin": {
8
+ "heuristic-mcp": "index.js"
9
+ },
10
+ "files": [
11
+ "index.js",
12
+ "config.jsonc",
13
+ "mcp_config.json",
14
+ "search-configs.js",
15
+ "lib/",
16
+ "features/",
17
+ "scripts/",
18
+ "README.md",
19
+ "LICENSE"
20
+ ],
21
+ "scripts": {
22
+ "start": "node --expose-gc index.js",
23
+ "dev": "node --expose-gc --watch index.js",
24
+ "test": "vitest run",
25
+ "test:watch": "vitest",
26
+ "clean": "node scripts/clear-cache.js",
27
+ "lint": "eslint .",
28
+ "format": "prettier --write .",
29
+ "postinstall": "node scripts/postinstall.js && node scripts/download-model.js"
30
+ },
31
+ "keywords": [
32
+ "mcp",
33
+ "semantic-search",
34
+ "code-search",
35
+ "embeddings",
36
+ "ai",
37
+ "model-context-protocol",
38
+ "hybrid-search",
39
+ "code-intelligence",
40
+ "cursor",
41
+ "vscode",
42
+ "claude",
43
+ "codex",
44
+ "openai",
45
+ "gemini",
46
+ "anthropic",
47
+ "antigravity",
48
+ "heuristic"
49
+ ],
50
+ "author": {
51
+ "name": "Softerist",
52
+ "url": "https://github.com/softerist"
53
+ },
54
+ "repository": {
55
+ "type": "git",
56
+ "url": "git+https://github.com/softerist/heuristic-mcp.git"
57
+ },
58
+ "homepage": "https://github.com/softerist/heuristic-mcp#readme",
59
+ "license": "MIT",
60
+ "dependencies": {
61
+ "@huggingface/transformers": "^3.8.1",
62
+ "@modelcontextprotocol/sdk": "^1.0.4",
63
+ "better-sqlite3": "^12.6.2",
64
+ "chokidar": "^3.5.3",
65
+ "fdir": "^6.5.0",
66
+ "ignore": "^7.0.5",
67
+ "punycode": "^2.3.1"
68
+ },
69
+ "optionalDependencies": {
70
+ "hnswlib-node": "^3.0.0"
71
+ },
72
+ "engines": {
73
+ "node": ">=18.0.0"
74
+ },
75
+ "overrides": {
76
+ "punycode": "^2.3.1"
77
+ },
78
+ "devDependencies": {
79
+ "@eslint/js": "^9.39.2",
80
+ "@vitest/coverage-v8": "^4.0.18",
81
+ "eslint": "^9.39.2",
82
+ "globals": "^17.1.0",
83
+ "prettier": "^3.8.1",
84
+ "vitest": "^4.0.16"
85
+ }
86
+ }