@softerist/heuristic-mcp 2.1.11 → 2.1.22

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.
@@ -6,30 +6,55 @@ export async function stop() {
6
6
  console.log('[Lifecycle] Stopping Heuristic MCP servers...');
7
7
  try {
8
8
  const platform = process.platform;
9
- let command = '';
9
+ const currentPid = process.pid;
10
+ let pids = [];
10
11
 
11
12
  if (platform === 'win32') {
12
- // Windows: Use wmic to find node processes running our script
13
- command = `wmic process where "CommandLine like '%heuristic-mcp/index.js%'" delete`;
13
+ const { stdout } = await execPromise(`wmic process where "CommandLine like '%heuristic-mcp/index.js%'" get ProcessId`);
14
+ pids = stdout.trim().split(/\s+/).filter(p => p && !isNaN(p) && parseInt(p) !== currentPid);
14
15
  } else {
15
- // Unix/Linux/Mac: Use pkill to find the process matching the script path
16
- // We exclude the current process to avoid suicide if we are running from the same script
17
- // however, usually the CLI runner is a different PID than the server.
18
- command = `pkill -f "heuristic-mcp/index.js"`;
16
+ // Unix: Use pgrep to get all matching PIDs
17
+ try {
18
+ const { stdout } = await execPromise(`pgrep -f \"heuristic-mcp.*index.js\"`);
19
+ const allPids = stdout.trim().split(/\s+/).filter(p => p && !isNaN(p));
20
+
21
+ // Filter out current PID and dead processes
22
+ pids = [];
23
+ for (const p of allPids) {
24
+ const pid = parseInt(p);
25
+ if (pid === currentPid) continue;
26
+ try {
27
+ process.kill(pid, 0);
28
+ pids.push(p);
29
+ } catch (e) {}
30
+ }
31
+ } catch (e) {
32
+ // pgrep returns code 1 if no processes found, which is fine
33
+ if (e.code === 1) pids = [];
34
+ else throw e;
35
+ }
19
36
  }
20
37
 
21
- await execPromise(command);
22
- console.log('[Lifecycle] ✅ Stopped all running instances.');
23
- } catch (error) {
24
- // pkill (Linux/Mac) returns exit code 1 if no process matched.
25
- // wmic (Windows) might throw specific errors if none found.
26
- // We treat exit code 1 as "Success, nothing was running".
27
- if (error.code === 1 || error.code === '1' || error.message.includes('No Instance(s) Available')) {
38
+ if (pids.length === 0) {
28
39
  console.log('[Lifecycle] No running instances found (already stopped).');
29
- } else {
30
- // Don't fail hard, just warn
31
- console.warn(`[Lifecycle] Warning: Stop command finished with unexpected result: ${error.message}`);
40
+ return;
41
+ }
42
+
43
+ // Kill each process
44
+ let killedCount = 0;
45
+ for (const pid of pids) {
46
+ try {
47
+ process.kill(parseInt(pid), 'SIGTERM');
48
+ killedCount++;
49
+ } catch (e) {
50
+ // Ignore if process already gone
51
+ if (e.code !== 'ESRCH') console.warn(`[Lifecycle] Failed to kill PID ${pid}: ${e.message}`);
52
+ }
32
53
  }
54
+
55
+ console.log(`[Lifecycle] ✅ Stopped ${killedCount} running instance(s).`);
56
+ } catch (error) {
57
+ console.warn(`[Lifecycle] Warning: Stop command encountered an error: ${error.message}`);
33
58
  }
34
59
  }
35
60
 
@@ -49,17 +74,37 @@ export async function start() {
49
74
  export async function status() {
50
75
  try {
51
76
  const platform = process.platform;
52
- let command = '';
77
+ const currentPid = process.pid;
78
+ let pids = [];
53
79
 
54
80
  if (platform === 'win32') {
55
- command = `wmic process where "CommandLine like '%heuristic-mcp/index.js%'" get ProcessId`;
81
+ const { stdout } = await execPromise(`wmic process where "CommandLine like '%heuristic-mcp/index.js%'" get ProcessId`);
82
+ pids = stdout.trim().split(/\s+/).filter(p => p && !isNaN(p) && parseInt(p) !== currentPid);
56
83
  } else {
57
- // pgrep -f matches the full command line
58
- command = `pgrep -f "heuristic-mcp/index.js"`;
59
- }
84
+ try {
85
+ const { stdout } = await execPromise(`pgrep -f "heuristic-mcp.*index.js"`);
86
+ const allPids = stdout.trim().split(/\s+/).filter(p => p && !isNaN(p));
60
87
 
61
- const { stdout } = await execPromise(command);
62
- const pids = stdout.trim().split(/\s+/).filter(pid => pid && !isNaN(pid));
88
+ // Filter out current PID and dead processes (e.g. ephemeral shell wrappers)
89
+ const validPids = [];
90
+ for (const p of allPids) {
91
+ const pid = parseInt(p);
92
+ if (pid === currentPid) continue;
93
+
94
+ try {
95
+ // Check if process is still alive
96
+ process.kill(pid, 0);
97
+ validPids.push(p);
98
+ } catch (e) {
99
+ // Process is dead or access denied
100
+ }
101
+ }
102
+ pids = validPids;
103
+ } catch (e) {
104
+ if (e.code === 1) pids = [];
105
+ else throw e;
106
+ }
107
+ }
63
108
 
64
109
  if (pids.length > 0) {
65
110
  console.log(`[Lifecycle] 🟢 Server is RUNNING. PID(s): ${pids.join(', ')}`);
@@ -67,11 +112,142 @@ export async function status() {
67
112
  console.log('[Lifecycle] ⚪ Server is STOPPED.');
68
113
  }
69
114
  } catch (error) {
70
- // pgrep returns exit code 1 if no process found
71
- if (error.code === 1 || error.code === '1' || error.message.includes('No Instance(s) Available')) {
72
- console.log('[Lifecycle] ⚪ Server is STOPPED.');
73
- } else {
74
- console.error(`[Lifecycle] Failed to check status: ${error.message}`);
115
+ console.error(`[Lifecycle] Failed to check status: ${error.message}`);
116
+ }
117
+ }
118
+
119
+ export async function logs() {
120
+ const fs = await import('fs/promises');
121
+ const path = await import('path');
122
+ const os = await import('os');
123
+ const crypto = await import('crypto');
124
+
125
+ console.log('[Logs] Searching for cache directories...\n');
126
+
127
+ // Determine global cache root
128
+ function getGlobalCacheDir() {
129
+ if (process.platform === 'win32') {
130
+ return process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local');
131
+ } else if (process.platform === 'darwin') {
132
+ return path.join(os.homedir(), 'Library', 'Caches');
133
+ }
134
+ return process.env.XDG_CACHE_HOME || path.join(os.homedir(), '.cache');
135
+ }
136
+
137
+ const globalCacheRoot = path.join(getGlobalCacheDir(), 'heuristic-mcp');
138
+
139
+ try {
140
+ // List all cache directories
141
+ const cacheDirs = await fs.readdir(globalCacheRoot).catch(() => []);
142
+
143
+ if (cacheDirs.length === 0) {
144
+ console.log('[Logs] No cache directories found.');
145
+ console.log(`[Logs] Expected location: ${globalCacheRoot}`);
146
+ return;
75
147
  }
148
+
149
+ console.log(`[Logs] Found ${cacheDirs.length} cache director${cacheDirs.length === 1 ? 'y' : 'ies'} in ${globalCacheRoot}\n`);
150
+
151
+ for (const dir of cacheDirs) {
152
+ const cacheDir = path.join(globalCacheRoot, dir);
153
+ const metaFile = path.join(cacheDir, 'meta.json');
154
+
155
+ console.log(`${'─'.repeat(60)}`);
156
+ console.log(`📁 Cache: ${dir}`);
157
+ console.log(` Path: ${cacheDir}`);
158
+
159
+ try {
160
+ const metaData = JSON.parse(await fs.readFile(metaFile, 'utf-8'));
161
+
162
+ console.log(` Status: ✅ Valid cache`);
163
+ console.log(` Workspace: ${metaData.workspace || 'Unknown'}`);
164
+ console.log(` Files indexed: ${metaData.filesIndexed ?? 'N/A'}`);
165
+ console.log(` Chunks stored: ${metaData.chunksStored ?? 'N/A'}`);
166
+ console.log(` Embedding model: ${metaData.embeddingModel}`);
167
+
168
+ if (metaData.lastSaveTime) {
169
+ const saveDate = new Date(metaData.lastSaveTime);
170
+ const now = new Date();
171
+ const ageMs = now - saveDate;
172
+ const ageHours = Math.floor(ageMs / (1000 * 60 * 60));
173
+ const ageMins = Math.floor((ageMs % (1000 * 60 * 60)) / (1000 * 60));
174
+
175
+ console.log(` Last saved: ${saveDate.toLocaleString()} (${ageHours}h ${ageMins}m ago)`);
176
+ }
177
+
178
+ // Check file sizes
179
+ const files = ['embeddings.json', 'file-hashes.json', 'call-graph.json', 'ann-index.bin'];
180
+ const sizes = [];
181
+ for (const file of files) {
182
+ try {
183
+ const stat = await fs.stat(path.join(cacheDir, file));
184
+ sizes.push(`${file}: ${(stat.size / 1024).toFixed(1)}KB`);
185
+ } catch {}
186
+ }
187
+ if (sizes.length > 0) {
188
+ console.log(` Files: ${sizes.join(', ')}`);
189
+ }
190
+
191
+ // Verify indexing completion
192
+ if (metaData.filesIndexed && metaData.filesIndexed > 0 && metaData.chunksStored && metaData.chunksStored > 0) {
193
+ console.log(` Indexing: ✅ COMPLETE (${metaData.filesIndexed} files → ${metaData.chunksStored} chunks)`);
194
+ } else if (metaData.filesIndexed === 0) {
195
+ console.log(` Indexing: ⚠️ NO FILES (check excludePatterns in config)`);
196
+ } else {
197
+ console.log(` Indexing: ⚠️ INCOMPLETE or UNKNOWN`);
198
+ }
199
+
200
+ } catch (e) {
201
+ console.log(` Status: ❌ Invalid or corrupted (${e.message})`);
202
+ }
203
+ }
204
+
205
+ console.log(`${'─'.repeat(60)}\n`);
206
+
207
+ // Show important paths
208
+ console.log('[Paths] Important locations:');
209
+
210
+ // Global npm bin
211
+ const { execSync } = await import('child_process');
212
+ let npmBin = 'unknown';
213
+ try {
214
+ npmBin = execSync('npm bin -g', { encoding: 'utf-8' }).trim();
215
+ } catch {}
216
+
217
+ // Check all known MCP config paths (same as register.js)
218
+ const home = os.homedir();
219
+ const mcpConfigs = [];
220
+
221
+ // Antigravity
222
+ const antigravityConfig = process.platform === 'win32'
223
+ ? path.join(home, '.gemini', 'antigravity', 'mcp_config.json')
224
+ : path.join(home, '.gemini', 'antigravity', 'mcp_config.json');
225
+ const antigravityExists = await fs.access(antigravityConfig).then(() => true).catch(() => false);
226
+ mcpConfigs.push({ name: 'Antigravity', path: antigravityConfig, exists: antigravityExists });
227
+
228
+ // Claude Desktop
229
+ let claudeConfig = null;
230
+ if (process.platform === 'darwin') {
231
+ claudeConfig = path.join(home, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json');
232
+ } else if (process.platform === 'win32') {
233
+ claudeConfig = path.join(process.env.APPDATA || '', 'Claude', 'claude_desktop_config.json');
234
+ }
235
+ if (claudeConfig) {
236
+ const claudeExists = await fs.access(claudeConfig).then(() => true).catch(() => false);
237
+ mcpConfigs.push({ name: 'Claude Desktop', path: claudeConfig, exists: claudeExists });
238
+ }
239
+
240
+ console.log(` 📦 Global npm bin: ${npmBin}`);
241
+ console.log(` ⚙️ MCP configs:`);
242
+ for (const cfg of mcpConfigs) {
243
+ const status = cfg.exists ? '\x1b[32m(exists)\x1b[0m' : '\x1b[90m(not found)\x1b[0m';
244
+ console.log(` - ${cfg.name}: ${cfg.path} ${status}`);
245
+ }
246
+ console.log(` 💾 Cache root: ${globalCacheRoot}`);
247
+ console.log(` 📁 Current dir: ${process.cwd()}`);
248
+ console.log('');
249
+
250
+ } catch (error) {
251
+ console.error(`[Logs] Error reading cache: ${error.message}`);
76
252
  }
77
253
  }
@@ -135,6 +135,15 @@ export async function register(filter = null) {
135
135
  forceLog('\n\x1b[36m' + '='.repeat(60));
136
136
  forceLog(' 🚀 Heuristic MCP Installed & Configured! ');
137
137
  forceLog('='.repeat(60) + '\x1b[0m');
138
+
139
+ // Show important paths
140
+ const home = os.homedir();
141
+ const cacheRoot = process.platform === 'win32'
142
+ ? path.join(process.env.LOCALAPPDATA || path.join(home, 'AppData', 'Local'), 'heuristic-mcp')
143
+ : process.platform === 'darwin'
144
+ ? path.join(home, 'Library', 'Caches', 'heuristic-mcp')
145
+ : path.join(process.env.XDG_CACHE_HOME || path.join(home, '.cache'), 'heuristic-mcp');
146
+
138
147
  forceLog(`
139
148
  \x1b[33mACTION REQUIRED:\x1b[0m
140
149
  1. \x1b[1mRestart your IDE\x1b[0m (or reload the window) to load the new config.
@@ -145,7 +154,13 @@ export async function register(filter = null) {
145
154
  - \x1b[1mIndexing:\x1b[0m Will begin immediately after restart.
146
155
  - \x1b[1mUsage:\x1b[0m You can work while it indexes (it catches up!).
147
156
 
157
+ \x1b[90mPATHS:\x1b[0m
158
+ - \x1b[1mMCP Config:\x1b[0m ${configPaths.map(p => p.path).join(', ')}
159
+ - \x1b[1mCache:\x1b[0m ${cacheRoot}
160
+ - \x1b[1mCheck status:\x1b[0m heuristic-mcp --logs
161
+
148
162
  \x1b[36mHappy Coding! 🤖\x1b[0m
149
163
  `);
164
+ forceLog(`\n\x1b[90m(Please wait while npm finalizes the installation...)\x1b[0m`);
150
165
  }
151
166
  }
package/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
- import { stop, start, status } from "./features/lifecycle.js";
3
+ import { stop, start, status, logs } from "./features/lifecycle.js";
4
4
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
5
  import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
6
6
  import { pipeline } from "@xenova/transformers";
@@ -41,6 +41,11 @@ if (args.includes('--status')) {
41
41
  process.exit(0);
42
42
  }
43
43
 
44
+ if (args.includes('--logs')) {
45
+ await logs();
46
+ process.exit(0);
47
+ }
48
+
44
49
  // Check if --register flag is present
45
50
  if (args.includes('--register')) {
46
51
  // Extract optional filter (e.g. --register antigravity)
@@ -134,6 +139,7 @@ async function initialize() {
134
139
 
135
140
  // Initialize cache
136
141
  cache = new EmbeddingsCache(config);
142
+ console.error(`[Server] Cache directory: ${config.cacheDirectory}`);
137
143
  await cache.load();
138
144
 
139
145
  // Initialize features
@@ -240,6 +246,20 @@ process.on('SIGINT', async () => {
240
246
 
241
247
  process.on('SIGTERM', async () => {
242
248
  console.error("\n[Server] Received SIGTERM, shutting down...");
249
+
250
+ // Stop file watcher
251
+ if (indexer && indexer.watcher) {
252
+ await indexer.watcher.close();
253
+ console.error("[Server] File watcher stopped");
254
+ }
255
+
256
+ // Save cache
257
+ if (cache) {
258
+ await cache.save();
259
+ console.error("[Server] Cache saved");
260
+ }
261
+
262
+ console.error("[Server] Goodbye!");
243
263
  process.exit(0);
244
264
  });
245
265
 
package/lib/cache.js CHANGED
@@ -193,9 +193,16 @@ export class EmbeddingsCache {
193
193
  const cacheFile = path.join(this.config.cacheDirectory, "embeddings.json");
194
194
  const hashFile = path.join(this.config.cacheDirectory, "file-hashes.json");
195
195
  const metaFile = path.join(this.config.cacheDirectory, CACHE_META_FILE);
196
+
197
+ // Include indexing stats in meta for verification
198
+ const uniqueFiles = new Set(this.vectorStore.map(chunk => chunk.file));
196
199
  this.cacheMeta = {
197
200
  version: CACHE_META_VERSION,
198
- embeddingModel: this.config.embeddingModel
201
+ embeddingModel: this.config.embeddingModel,
202
+ lastSaveTime: new Date().toISOString(),
203
+ filesIndexed: uniqueFiles.size,
204
+ chunksStored: this.vectorStore.length,
205
+ workspace: this.config.searchDirectory || null
199
206
  };
200
207
 
201
208
  await Promise.all([
package/lib/config.js CHANGED
@@ -1,5 +1,7 @@
1
1
  import fs from "fs/promises";
2
2
  import path from "path";
3
+ import os from "os";
4
+ import crypto from "crypto";
3
5
  import { fileURLToPath } from "url";
4
6
  import { ProjectDetector } from "./project-detector.js";
5
7
 
@@ -58,7 +60,7 @@ const DEFAULT_CONFIG = {
58
60
  maxFileSize: 1048576, // 1MB - skip files larger than this
59
61
  maxResults: 5,
60
62
  enableCache: true,
61
- cacheDirectory: "./.smart-coding-cache",
63
+ cacheDirectory: null, // Will be set dynamically by loadConfig()
62
64
  watchFiles: true,
63
65
  verbose: false,
64
66
  workerThreads: "auto", // "auto" = CPU cores - 1, or set a number
@@ -127,7 +129,34 @@ export async function loadConfig(workspaceDir = null) {
127
129
 
128
130
  // Set search and cache directories
129
131
  config.searchDirectory = baseDir;
130
- config.cacheDirectory = path.join(baseDir, ".smart-coding-cache");
132
+
133
+ // Determine cache directory
134
+ if (userConfig.cacheDirectory) {
135
+ // User explicitly set a cache path in their config.json
136
+ config.cacheDirectory = path.isAbsolute(userConfig.cacheDirectory)
137
+ ? userConfig.cacheDirectory
138
+ : path.join(baseDir, userConfig.cacheDirectory);
139
+ } else {
140
+ // Use global cache directory to prevent cluttering project root
141
+ // Hash the absolute path to ensure uniqueness per project
142
+ const projectHash = crypto.createHash('md5').update(baseDir).digest('hex').slice(0, 12);
143
+ const globalCacheRoot = getGlobalCacheDir();
144
+ config.cacheDirectory = path.join(globalCacheRoot, "heuristic-mcp", projectHash);
145
+
146
+ // Support legacy .smart-coding-cache if it already exists in the project root
147
+ const legacyPath = path.join(baseDir, ".smart-coding-cache");
148
+ try {
149
+ const stats = await fs.stat(legacyPath);
150
+ if (stats.isDirectory()) {
151
+ config.cacheDirectory = legacyPath;
152
+ if (config.verbose) {
153
+ console.error(`[Config] Using existing local cache: ${legacyPath}`);
154
+ }
155
+ }
156
+ } catch {
157
+ // Legacy folder doesn't exist, using global path
158
+ }
159
+ }
131
160
 
132
161
  // Smart project detection
133
162
  if (config.smartIndexing !== false) {
@@ -375,6 +404,19 @@ export async function loadConfig(workspaceDir = null) {
375
404
  return config;
376
405
  }
377
406
 
407
+ /**
408
+ * Get platform-specific global cache directory
409
+ */
410
+ function getGlobalCacheDir() {
411
+ if (process.platform === 'win32') {
412
+ return process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local');
413
+ }
414
+ if (process.platform === 'darwin') {
415
+ return path.join(os.homedir(), 'Library', 'Caches');
416
+ }
417
+ return process.env.XDG_CACHE_HOME || path.join(os.homedir(), '.cache');
418
+ }
419
+
378
420
  export function getConfig() {
379
421
  return config;
380
422
  }
package/package.json CHANGED
@@ -1,20 +1,20 @@
1
1
  {
2
2
  "name": "@softerist/heuristic-mcp",
3
- "version": "2.1.11",
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
- "scripts": {
11
- "start": "node index.js",
12
- "dev": "node --watch index.js",
13
- "test": "vitest run",
14
- "test:watch": "vitest",
15
- "clear-cache": "node scripts/clear-cache.js",
16
- "postinstall": "node index.js --register"
17
- },
3
+ "version": "2.1.22",
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
+ "scripts": {
11
+ "start": "node index.js",
12
+ "dev": "node --watch index.js",
13
+ "test": "vitest run",
14
+ "test:watch": "vitest",
15
+ "clear-cache": "node scripts/clear-cache.js",
16
+ "postinstall": "node scripts/postinstall.js"
17
+ },
18
18
  "keywords": [
19
19
  "mcp",
20
20
  "semantic-search",
@@ -0,0 +1,9 @@
1
+ import { register } from '../features/register.js';
2
+
3
+ // Run the registration process
4
+ console.log('[PostInstall] Running Heuristic MCP registration...');
5
+ register().catch(err => {
6
+ console.error('[PostInstall] Registration failed:', err.message);
7
+ // Don't fail the install if registration fails, just warn
8
+ process.exit(0);
9
+ });