@softerist/heuristic-mcp 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/utils.js ADDED
@@ -0,0 +1,301 @@
1
+ import crypto from "crypto";
2
+ import path from "path";
3
+ import { estimateTokens, getChunkingParams, getModelTokenLimit } from "./tokenizer.js";
4
+
5
+ // Re-export tokenizer utilities
6
+ export { estimateTokens, getChunkingParams, getModelTokenLimit, MODEL_TOKEN_LIMITS } from "./tokenizer.js";
7
+
8
+ /**
9
+ * Calculate cosine similarity between two vectors
10
+ */
11
+ export function cosineSimilarity(a, b) {
12
+ let dot = 0, normA = 0, normB = 0;
13
+ for (let i = 0; i < a.length; i++) {
14
+ dot += a[i] * b[i];
15
+ normA += a[i] * a[i];
16
+ normB += b[i] * b[i];
17
+ }
18
+ return dot / (Math.sqrt(normA) * Math.sqrt(normB));
19
+ }
20
+
21
+ /**
22
+ * Fast similarity for normalized vectors (dot product)
23
+ */
24
+ export function dotSimilarity(a, b) {
25
+ let dot = 0;
26
+ for (let i = 0; i < a.length; i++) {
27
+ dot += a[i] * b[i];
28
+ }
29
+ return dot;
30
+ }
31
+
32
+ /**
33
+ * Generate hash for file content to detect changes
34
+ */
35
+ export function hashContent(content) {
36
+ return crypto.createHash("md5").update(content).digest("hex");
37
+ }
38
+
39
+ /**
40
+ * Intelligent chunking with token limit awareness
41
+ * Tries to split by function/class boundaries while respecting token limits
42
+ *
43
+ * @param {string} content - File content to chunk
44
+ * @param {string} file - File path (for language detection)
45
+ * @param {object} config - Configuration object with embeddingModel
46
+ * @returns {Array<{text: string, startLine: number, endLine: number, tokenCount: number}>}
47
+ */
48
+ export function smartChunk(content, file, config) {
49
+ const lines = content.split("\n");
50
+ const chunks = [];
51
+ const ext = path.extname(file);
52
+
53
+ // Get model-specific chunking parameters
54
+ const { targetTokens, overlapTokens } = getChunkingParams(config.embeddingModel);
55
+
56
+ // Language-specific patterns for function/class detection
57
+ const patterns = {
58
+ // JavaScript/TypeScript
59
+ js: /^(export\s+)?(async\s+)?(function|class|const|let|var)\s+\w+/,
60
+ jsx: /^(export\s+)?(async\s+)?(function|class|const|let|var)\s+\w+/,
61
+ ts: /^(export\s+)?(async\s+)?(function|class|const|let|var|interface|type)\s+\w+/,
62
+ tsx: /^(export\s+)?(async\s+)?(function|class|const|let|var|interface|type)\s+\w+/,
63
+ mjs: /^(export\s+)?(async\s+)?(function|class|const|let|var)\s+\w+/,
64
+ cjs: /^(export\s+)?(async\s+)?(function|class|const|let|var)\s+\w+/,
65
+
66
+ // Python
67
+ py: /^(class|def|async\s+def)\s+\w+/,
68
+ pyw: /^(class|def|async\s+def)\s+\w+/,
69
+ pyx: /^(cdef|cpdef|def|class)\s+\w+/, // Cython
70
+
71
+ // Java/Kotlin/Scala
72
+ java: /^(public|private|protected)?\s*(static\s+)?(class|interface|enum|void|int|String|boolean)\s+\w+/,
73
+ kt: /^(class|interface|object|fun|val|var)\s+\w+/,
74
+ kts: /^(class|interface|object|fun|val|var)\s+\w+/,
75
+ scala: /^(class|object|trait|def|val|var)\s+\w+/,
76
+
77
+ // C/C++
78
+ c: /^(struct|enum|union|void|int|char|float|double)\s+\w+/,
79
+ cpp: /^(class|struct|namespace|template|void|int|bool)\s+\w+/,
80
+ cc: /^(class|struct|namespace|template|void|int|bool)\s+\w+/,
81
+ cxx: /^(class|struct|namespace|template|void|int|bool)\s+\w+/,
82
+ h: /^(class|struct|namespace|template|void|int|bool)\s+\w+/,
83
+ hpp: /^(class|struct|namespace|template|void|int|bool)\s+\w+/,
84
+ hxx: /^(class|struct|namespace|template|void|int|bool)\s+\w+/,
85
+
86
+ // C#
87
+ cs: /^(public|private|protected)?\s*(static\s+)?(class|interface|struct|enum|void|int|string|bool)\s+\w+/,
88
+ csx: /^(public|private|protected)?\s*(static\s+)?(class|interface|struct|enum|void|int|string|bool)\s+\w+/,
89
+
90
+ // Go
91
+ go: /^(func|type|const|var)\s+\w+/,
92
+
93
+ // Rust
94
+ rs: /^(pub\s+)?(fn|struct|enum|trait|impl|const|static|mod)\s+\w+/,
95
+
96
+ // PHP
97
+ php: /^(class|interface|trait|function|const)\s+\w+/,
98
+ phtml: /^(<\?php|class|interface|trait|function)\s*/,
99
+
100
+ // Ruby
101
+ rb: /^(class|module|def)\s+\w+/,
102
+ rake: /^(class|module|def|task|namespace)\s+\w+/,
103
+
104
+ // Swift
105
+ swift: /^(class|struct|enum|protocol|func|var|let|extension)\s+\w+/,
106
+
107
+ // R
108
+ r: /^(\w+)\s*(<-|=)\s*function/,
109
+ R: /^(\w+)\s*(<-|=)\s*function/,
110
+
111
+ // Lua
112
+ lua: /^(function|local\s+function)\s+\w+/,
113
+
114
+ // Shell scripts
115
+ sh: /^(\w+\s*\(\)|function\s+\w+)/,
116
+ bash: /^(\w+\s*\(\)|function\s+\w+)/,
117
+ zsh: /^(\w+\s*\(\)|function\s+\w+)/,
118
+ fish: /^function\s+\w+/,
119
+
120
+ // CSS/Styles
121
+ css: /^(\.|#|@media|@keyframes|@font-face|\w+)\s*[{,]/,
122
+ scss: /^(\$\w+:|@mixin|@function|@include|\.|#|@media)\s*/,
123
+ sass: /^(\$\w+:|=\w+|\+\w+|\.|#|@media)\s*/,
124
+ less: /^(@\w+:|\.|\#|@media)\s*/,
125
+ styl: /^(\$\w+\s*=|\w+\(|\.|\#)\s*/,
126
+
127
+ // Markup/HTML
128
+ html: /^(<(div|section|article|header|footer|nav|main|aside|form|table|template|script|style)\b)/i,
129
+ htm: /^(<(div|section|article|header|footer|nav|main|aside|form|table|template|script|style)\b)/i,
130
+ xml: /^(<\w+|\s*<!\[CDATA\[)/,
131
+ svg: /^(<svg|<g|<path|<defs|<symbol)\b/,
132
+
133
+ // Config files
134
+ json: /^(\s*"[\w-]+"\s*:\s*[\[{])/,
135
+ yaml: /^(\w[\w-]*:\s*[|>]?$|\w[\w-]*:\s*$)/,
136
+ yml: /^(\w[\w-]*:\s*[|>]?$|\w[\w-]*:\s*$)/,
137
+ toml: /^(\[\[?\w+\]?\]?|\w+\s*=)/,
138
+ ini: /^(\[\w+\]|\w+\s*=)/,
139
+ env: /^[A-Z_][A-Z0-9_]*=/,
140
+
141
+ // Documentation
142
+ md: /^(#{1,6}\s+|```|\*{3}|_{3})/,
143
+ mdx: /^(#{1,6}\s+|```|import\s+|export\s+)/,
144
+ txt: /^.{50,}/, // Split on long paragraphs
145
+ rst: /^(={3,}|-{3,}|~{3,}|\.\.\s+\w+::)/,
146
+
147
+ // Database
148
+ sql: /^(CREATE|ALTER|INSERT|UPDATE|DELETE|SELECT|DROP|GRANT|REVOKE|WITH|DECLARE|BEGIN|END)\s+/i,
149
+
150
+ // Perl
151
+ pl: /^(sub|package|use|require)\s+\w+/,
152
+ pm: /^(sub|package|use|require)\s+\w+/,
153
+
154
+ // Vim
155
+ vim: /^(function|command|autocmd|let\s+g:)\s*/,
156
+ };
157
+
158
+ const langPattern = patterns[ext.slice(1)] || patterns.js;
159
+ let currentChunk = [];
160
+ let chunkStartLine = 0;
161
+ let currentTokenCount = 0;
162
+
163
+ // Track bracket depth for better boundary detection
164
+ let bracketDepth = 0;
165
+ let braceDepth = 0;
166
+ let parenDepth = 0;
167
+ let inString = false;
168
+ let inComment = false;
169
+ let stringChar = null; // ' or " or `
170
+
171
+ for (let i = 0; i < lines.length; i++) {
172
+ const line = lines[i];
173
+ const lineTokens = estimateTokens(line);
174
+ const trimmed = line.trim();
175
+
176
+ // Simple state tracking for heuristics (not a full parser)
177
+ if (inComment) {
178
+ // Look for end of block comment
179
+ if (line.includes('*/')) {
180
+ const parts = line.split('*/');
181
+ // If there's content after the comment, process it (simplified)
182
+ if (parts[parts.length - 1].trim().length > 0) {
183
+ inComment = false;
184
+ // Recursive call or continue logic would be better, but for heuristic this is fine
185
+ // We just assume the line is mixed and skip granular checks
186
+ } else {
187
+ inComment = false;
188
+ }
189
+ }
190
+ } else {
191
+ for (let j = 0; j < line.length; j++) {
192
+ const char = line[j];
193
+ const nextChar = line[j+1];
194
+
195
+ if (inString) {
196
+ if (char === '\\') {
197
+ j++; // Skip escaped char
198
+ } else if (char === stringChar) {
199
+ inString = false;
200
+ stringChar = null;
201
+ }
202
+ } else {
203
+ // Check for comment start
204
+ if (char === '/' && nextChar === '*') {
205
+ inComment = true;
206
+ j++;
207
+ // Check if it ends on same line
208
+ if (line.indexOf('*/', j) !== -1) {
209
+ inComment = false;
210
+ j = line.indexOf('*/', j) + 1;
211
+ } else {
212
+ break; // Rest of line is comment
213
+ }
214
+ } else if (char === '/' && nextChar === '/') {
215
+ break; // Skip rest of line (line comment)
216
+ } else if (char === '\'' || char === '"' || char === '`') {
217
+ inString = true;
218
+ stringChar = char;
219
+ } else {
220
+ // Only count brackets if not in string or comment
221
+ if (char === '{') braceDepth++;
222
+ else if (char === '}') braceDepth = Math.max(0, braceDepth - 1);
223
+ else if (char === '[') bracketDepth++;
224
+ else if (char === ']') bracketDepth = Math.max(0, bracketDepth - 1);
225
+ else if (char === '(') parenDepth++;
226
+ else if (char === ')') parenDepth = Math.max(0, parenDepth - 1);
227
+ }
228
+ }
229
+ }
230
+ }
231
+
232
+ // Check if adding this line would exceed token limit
233
+ const wouldExceedLimit = (currentTokenCount + lineTokens) > targetTokens;
234
+
235
+ // Check if this is a good split point using multiple heuristics
236
+ const matchesPattern = langPattern.test(trimmed);
237
+ const atTopLevel = braceDepth === 0 && bracketDepth === 0 && parenDepth === 0 && !inString && !inComment;
238
+ const startsAtColumn0 = line.length > 0 && /^\S/.test(line);
239
+ const isEmptyLine = trimmed.length === 0;
240
+ const prevWasEmpty = i > 0 && currentChunk.length > 0 && currentChunk[currentChunk.length - 1].trim().length === 0;
241
+ const isCommentStart = /^\s*(\/\*\*|\/\/\s*[-=]{3,}|#\s*[-=]{3,})/.test(line);
242
+
243
+ const isGoodSplitPoint = currentChunk.length > 3 && (
244
+ (matchesPattern && (atTopLevel || braceDepth <= 1)) ||
245
+ (atTopLevel && startsAtColumn0 && !isEmptyLine) ||
246
+ (prevWasEmpty && (matchesPattern || isCommentStart))
247
+ );
248
+
249
+ const shouldSplit = wouldExceedLimit || (isGoodSplitPoint && currentTokenCount > targetTokens * 0.6);
250
+
251
+ // Avoid splitting in weird states if possible
252
+ const safeToSplit = (braceDepth <= 1 && !inString) || wouldExceedLimit;
253
+
254
+ if (shouldSplit && safeToSplit && currentChunk.length > 0) {
255
+ const chunkText = currentChunk.join("\n");
256
+ if (chunkText.trim().length > 20) {
257
+ chunks.push({
258
+ text: chunkText,
259
+ startLine: chunkStartLine + 1,
260
+ endLine: i,
261
+ tokenCount: currentTokenCount
262
+ });
263
+ }
264
+
265
+ // Calculate overlap
266
+ let overlapLines = [];
267
+ let overlapTokensCount = 0;
268
+ for (let j = currentChunk.length - 1; j >= 0 && overlapTokensCount < overlapTokens; j--) {
269
+ const lineT = estimateTokens(currentChunk[j]);
270
+ if (overlapTokensCount + lineT <= overlapTokens) {
271
+ overlapLines.unshift(currentChunk[j]);
272
+ overlapTokensCount += lineT;
273
+ } else {
274
+ break;
275
+ }
276
+ }
277
+
278
+ currentChunk = overlapLines;
279
+ currentTokenCount = overlapTokensCount;
280
+ chunkStartLine = i - overlapLines.length;
281
+ }
282
+
283
+ currentChunk.push(line);
284
+ currentTokenCount += lineTokens;
285
+ }
286
+
287
+ // Add remaining chunk
288
+ if (currentChunk.length > 0) {
289
+ const chunkText = currentChunk.join("\n");
290
+ if (chunkText.trim().length > 20) {
291
+ chunks.push({
292
+ text: chunkText,
293
+ startLine: chunkStartLine + 1,
294
+ endLine: lines.length,
295
+ tokenCount: currentTokenCount
296
+ });
297
+ }
298
+ }
299
+
300
+ return chunks;
301
+ }
package/package.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "@softerist/heuristic-mcp",
3
+ "version": "2.0.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
+ "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
+ },
17
+ "keywords": [
18
+ "mcp",
19
+ "semantic-search",
20
+ "code-search",
21
+ "embeddings",
22
+ "ai",
23
+ "model-context-protocol",
24
+ "hybrid-search",
25
+ "code-intelligence",
26
+ "cursor",
27
+ "vscode",
28
+ "claude",
29
+ "codex",
30
+ "openai",
31
+ "gemini",
32
+ "anthropic",
33
+ "antigravity",
34
+ "heuristic"
35
+ ],
36
+ "author": {
37
+ "name": "Softerist",
38
+ "url": "https://softerist.com"
39
+ },
40
+ "contributors": [
41
+ {
42
+ "name": "Omar Haris",
43
+ "url": "https://www.linkedin.com/in/omarharis/"
44
+ }
45
+ ],
46
+ "repository": {
47
+ "type": "git",
48
+ "url": "https://github.com/softerist/heuristic-mcp"
49
+ },
50
+ "homepage": "https://github.com/softerist/heuristic-mcp#readme",
51
+ "license": "MIT",
52
+ "dependencies": {
53
+ "@modelcontextprotocol/sdk": "^1.0.4",
54
+ "@xenova/transformers": "^2.17.2",
55
+ "chokidar": "^3.5.3",
56
+ "fdir": "^6.5.0",
57
+ "glob": "^10.3.10"
58
+ },
59
+ "engines": {
60
+ "node": ">=18.0.0"
61
+ },
62
+ "devDependencies": {
63
+ "vitest": "^4.0.16"
64
+ }
65
+ }
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env node
2
+ import fs from "fs/promises";
3
+ import path from "path";
4
+
5
+ async function clearCache() {
6
+ try {
7
+ const configPath = path.join(process.cwd(), "config.json");
8
+ let cacheDir = "./.smart-coding-cache";
9
+
10
+ // Try to load cache directory from config
11
+ try {
12
+ const configData = await fs.readFile(configPath, "utf-8");
13
+ const config = JSON.parse(configData);
14
+ if (config.cacheDirectory) {
15
+ cacheDir = path.resolve(config.cacheDirectory);
16
+ }
17
+ } catch {
18
+ console.log("Using default cache directory");
19
+ }
20
+
21
+ // Remove cache directory
22
+ await fs.rm(cacheDir, { recursive: true, force: true });
23
+ console.log(`Cache cleared successfully: ${cacheDir}`);
24
+ console.log("Next startup will perform a full reindex.");
25
+ } catch (error) {
26
+ console.error(`Error clearing cache: ${error.message}`);
27
+ process.exit(1);
28
+ }
29
+ }
30
+
31
+ clearCache();
@@ -0,0 +1,288 @@
1
+ /**
2
+ * Tests for CacheClearer feature
3
+ *
4
+ * Tests the cache clearing functionality including:
5
+ * - Basic cache clearing
6
+ * - Protection during indexing
7
+ * - Protection during save operations
8
+ * - Concurrent clear prevention
9
+ * - Tool handler responses
10
+ */
11
+
12
+ import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest';
13
+ import {
14
+ createTestFixtures,
15
+ cleanupFixtures,
16
+ clearTestCache,
17
+ createMockRequest
18
+ } from './helpers.js';
19
+ import * as ClearCacheFeature from '../features/clear-cache.js';
20
+ import { CacheClearer } from '../features/clear-cache.js';
21
+ import fs from 'fs/promises';
22
+ import path from 'path';
23
+
24
+ describe('CacheClearer', () => {
25
+ let fixtures;
26
+
27
+ beforeAll(async () => {
28
+ fixtures = await createTestFixtures({ workerThreads: 2 });
29
+ });
30
+
31
+ afterAll(async () => {
32
+ await cleanupFixtures(fixtures);
33
+ });
34
+
35
+ beforeEach(async () => {
36
+ // Reset state
37
+ fixtures.indexer.isIndexing = false;
38
+ fixtures.cache.isSaving = false;
39
+ fixtures.cacheClearer.isClearing = false;
40
+ });
41
+
42
+ describe('Basic Cache Clearing', () => {
43
+ it('should clear cache successfully', async () => {
44
+ // First ensure we have a cache
45
+ await fixtures.indexer.indexAll(true);
46
+
47
+ // Verify cache exists
48
+ expect(fixtures.cache.getVectorStore().length).toBeGreaterThan(0);
49
+
50
+ // Clear cache
51
+ const result = await fixtures.cacheClearer.execute();
52
+
53
+ expect(result.success).toBe(true);
54
+ expect(result.message).toContain('Cache cleared successfully');
55
+ expect(result.cacheDirectory).toBe(fixtures.config.cacheDirectory);
56
+ });
57
+
58
+ it('should empty vectorStore and fileHashes', async () => {
59
+ // Create some cache
60
+ await fixtures.indexer.indexAll(true);
61
+
62
+ // Clear
63
+ await fixtures.cacheClearer.execute();
64
+
65
+ // Both should be empty
66
+ expect(fixtures.cache.getVectorStore().length).toBe(0);
67
+ expect(fixtures.cache.fileHashes.size).toBe(0);
68
+ });
69
+
70
+ it('should delete cache directory', async () => {
71
+ // Create cache
72
+ await fixtures.indexer.indexAll(true);
73
+
74
+ // Verify cache directory exists
75
+ await expect(fs.access(fixtures.config.cacheDirectory)).resolves.not.toThrow();
76
+
77
+ // Clear
78
+ await fixtures.cacheClearer.execute();
79
+
80
+ // Directory should not exist
81
+ await expect(fs.access(fixtures.config.cacheDirectory)).rejects.toThrow();
82
+ });
83
+ });
84
+
85
+ describe('Protection During Indexing', () => {
86
+ it('should prevent clear while indexing is in progress', async () => {
87
+ // Simulate indexing in progress
88
+ await clearTestCache(fixtures.config);
89
+ fixtures.cache.setVectorStore([]);
90
+ fixtures.cache.fileHashes = new Map();
91
+
92
+ const indexPromise = fixtures.indexer.indexAll(true);
93
+
94
+ // Wait for indexing to start
95
+ await new Promise(resolve => setTimeout(resolve, 100));
96
+ expect(fixtures.indexer.isIndexing).toBe(true);
97
+
98
+ // Try to clear - should fail
99
+ await expect(fixtures.cacheClearer.execute()).rejects.toThrow(
100
+ 'Cannot clear cache while indexing is in progress'
101
+ );
102
+
103
+ await indexPromise;
104
+ });
105
+
106
+ it('should allow clear after indexing completes', async () => {
107
+ // Complete indexing
108
+ await fixtures.indexer.indexAll(true);
109
+ expect(fixtures.indexer.isIndexing).toBe(false);
110
+
111
+ // Clear should work
112
+ const result = await fixtures.cacheClearer.execute();
113
+ expect(result.success).toBe(true);
114
+ });
115
+ });
116
+
117
+ describe('Protection During Save', () => {
118
+ it('should prevent clear while cache is being saved', async () => {
119
+ // Simulate save in progress
120
+ fixtures.cache.isSaving = true;
121
+
122
+ // Try to clear - should fail
123
+ await expect(fixtures.cacheClearer.execute()).rejects.toThrow(
124
+ 'Cannot clear cache while cache is being saved'
125
+ );
126
+
127
+ // Reset
128
+ fixtures.cache.isSaving = false;
129
+ });
130
+
131
+ it('should allow clear after save completes', async () => {
132
+ // Index first
133
+ await fixtures.indexer.indexAll(true);
134
+
135
+ // isSaving should be false after indexing
136
+ expect(fixtures.cache.isSaving).toBe(false);
137
+
138
+ // Clear should work
139
+ const result = await fixtures.cacheClearer.execute();
140
+ expect(result.success).toBe(true);
141
+ });
142
+ });
143
+
144
+ describe('Concurrent Clear Prevention', () => {
145
+ it('should prevent multiple concurrent clears', async () => {
146
+ // Index first
147
+ await fixtures.indexer.indexAll(true);
148
+
149
+ // Reset the isClearing flag
150
+ fixtures.cacheClearer.isClearing = false;
151
+
152
+ // Start multiple concurrent clears
153
+ const promises = [
154
+ fixtures.cacheClearer.execute(),
155
+ fixtures.cacheClearer.execute(),
156
+ fixtures.cacheClearer.execute()
157
+ ];
158
+
159
+ const results = await Promise.allSettled(promises);
160
+
161
+ // Exactly one should succeed
162
+ const successes = results.filter(r => r.status === 'fulfilled');
163
+ const failures = results.filter(r => r.status === 'rejected');
164
+
165
+ expect(successes.length).toBe(1);
166
+ expect(failures.length).toBe(2);
167
+
168
+ // Failures should have correct error message
169
+ for (const failure of failures) {
170
+ expect(failure.reason.message).toContain('already in progress');
171
+ }
172
+ });
173
+
174
+ it('should reset isClearing flag after completion', async () => {
175
+ // Index first
176
+ await fixtures.indexer.indexAll(true);
177
+
178
+ expect(fixtures.cacheClearer.isClearing).toBe(false);
179
+
180
+ // Clear
181
+ await fixtures.cacheClearer.execute();
182
+
183
+ // Flag should be reset
184
+ expect(fixtures.cacheClearer.isClearing).toBe(false);
185
+ });
186
+
187
+ it('should reset isClearing flag even on error', async () => {
188
+ // Set up for failure
189
+ fixtures.cache.isSaving = true;
190
+
191
+ try {
192
+ await fixtures.cacheClearer.execute();
193
+ } catch {
194
+ // Expected to fail
195
+ }
196
+
197
+ // isClearing should not have been set (failed before setting)
198
+ expect(fixtures.cacheClearer.isClearing).toBe(false);
199
+
200
+ // Reset
201
+ fixtures.cache.isSaving = false;
202
+ });
203
+ });
204
+ });
205
+
206
+ describe('Clear Cache Tool Handler', () => {
207
+ let fixtures;
208
+
209
+ beforeAll(async () => {
210
+ fixtures = await createTestFixtures({ workerThreads: 2 });
211
+ });
212
+
213
+ afterAll(async () => {
214
+ await cleanupFixtures(fixtures);
215
+ });
216
+
217
+ beforeEach(async () => {
218
+ fixtures.indexer.isIndexing = false;
219
+ fixtures.cache.isSaving = false;
220
+ fixtures.cacheClearer.isClearing = false;
221
+ });
222
+
223
+ describe('Tool Definition', () => {
224
+ it('should have correct tool definition', () => {
225
+ const toolDef = ClearCacheFeature.getToolDefinition();
226
+
227
+ expect(toolDef.name).toBe('c_clear_cache');
228
+ expect(toolDef.description).toContain('cache');
229
+ expect(toolDef.annotations.destructiveHint).toBe(true);
230
+ expect(toolDef.inputSchema.properties).toEqual({});
231
+ });
232
+ });
233
+
234
+ describe('Tool Handler', () => {
235
+ it('should return success message on cleared cache', async () => {
236
+ // Index first
237
+ await fixtures.indexer.indexAll(true);
238
+
239
+ const request = createMockRequest('c_clear_cache', {});
240
+ const result = await ClearCacheFeature.handleToolCall(request, fixtures.cacheClearer);
241
+
242
+ expect(result.content[0].text).toContain('Cache cleared successfully');
243
+ expect(result.content[0].text).toContain('Cache directory:');
244
+ });
245
+
246
+ it('should return error message when indexing is in progress', async () => {
247
+ // Simulate indexing
248
+ await clearTestCache(fixtures.config);
249
+ fixtures.cache.setVectorStore([]);
250
+ fixtures.cache.fileHashes = new Map();
251
+
252
+ const indexPromise = fixtures.indexer.indexAll(true);
253
+ await new Promise(resolve => setTimeout(resolve, 100));
254
+
255
+ const request = createMockRequest('c_clear_cache', {});
256
+ const result = await ClearCacheFeature.handleToolCall(request, fixtures.cacheClearer);
257
+
258
+ expect(result.content[0].text).toContain('Failed to clear cache');
259
+ expect(result.content[0].text).toContain('indexing is in progress');
260
+
261
+ await indexPromise;
262
+ });
263
+
264
+ it('should return error message when save is in progress', async () => {
265
+ fixtures.cache.isSaving = true;
266
+
267
+ const request = createMockRequest('c_clear_cache', {});
268
+ const result = await ClearCacheFeature.handleToolCall(request, fixtures.cacheClearer);
269
+
270
+ expect(result.content[0].text).toContain('Failed to clear cache');
271
+ expect(result.content[0].text).toContain('being saved');
272
+
273
+ fixtures.cache.isSaving = false;
274
+ });
275
+
276
+ it('should return error message when clear is already in progress', async () => {
277
+ fixtures.cacheClearer.isClearing = true;
278
+
279
+ const request = createMockRequest('c_clear_cache', {});
280
+ const result = await ClearCacheFeature.handleToolCall(request, fixtures.cacheClearer);
281
+
282
+ expect(result.content[0].text).toContain('Failed to clear cache');
283
+ expect(result.content[0].text).toContain('already in progress');
284
+
285
+ fixtures.cacheClearer.isClearing = false;
286
+ });
287
+ });
288
+ });