@gotza02/sequential-thinking 2026.2.17 → 2026.2.19
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 +2 -2
- package/dist/filesystem.test.js +28 -13
- package/dist/graph.js +90 -16
- package/dist/graph.test.js +18 -0
- package/dist/tools/filesystem.js +49 -31
- package/dist/verify_cache.test.js +27 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ Advanced MCP Server enabling AI to act as a Software Engineer with Deep Thinking
|
|
|
4
4
|
|
|
5
5
|
## ✨ Key Features
|
|
6
6
|
- **Deep Thinking:** Step-by-step reasoning with auto-correction (Loop Breaker).
|
|
7
|
-
- **Code Intelligence:** Dependency graph mapping (`build_project_graph`) & surgical code editing.
|
|
7
|
+
- **Code Intelligence:** Dependency graph mapping (`build_project_graph`) with **Incremental Caching** & surgical code editing.
|
|
8
8
|
- **Memory:** Long-term project notes, reusable code database, and thought history.
|
|
9
9
|
- **Research:** Integrated Web search (Brave/Exa) & webpage reading.
|
|
10
10
|
- **REST API:** Built-in HTTP server wrapper for easy integration with web tools and external services.
|
|
@@ -62,7 +62,7 @@ Add this to your MCP settings (`~/.gemini/settings.json` or `claude_desktop_conf
|
|
|
62
62
|
|
|
63
63
|
## 🛠️ Tools Summary
|
|
64
64
|
- **Thinking:** `sequentialthinking`, `summarize_history`, `clear_thought_history`
|
|
65
|
-
- **Code:** `build_project_graph`, `deep_code_analyze`, `deep_code_edit`, `search_code`
|
|
65
|
+
- **Code:** `build_project_graph`, `deep_code_analyze`, `deep_code_edit`, `search_code` (Regex/Line Numbers)
|
|
66
66
|
- **Memory:** `add_code_snippet`, `search_code_db`, `manage_notes`
|
|
67
67
|
- **Web:** `web_search`, `read_webpage`, `fetch`
|
|
68
68
|
- **System:** `read_file`, `write_file`, `edit_file`, `shell_execute`
|
package/dist/filesystem.test.js
CHANGED
|
@@ -113,22 +113,40 @@ describe('FileSystem Tools', () => {
|
|
|
113
113
|
});
|
|
114
114
|
});
|
|
115
115
|
describe('search_code', () => {
|
|
116
|
-
it('should find pattern in single file', async () => {
|
|
116
|
+
it('should find pattern in single file with line number', async () => {
|
|
117
117
|
registerFileSystemTools(mockServer);
|
|
118
118
|
const handler = registeredTools['search_code'];
|
|
119
119
|
fs.stat.mockResolvedValue({ isFile: () => true });
|
|
120
|
-
fs.readFile.mockResolvedValue('
|
|
120
|
+
fs.readFile.mockResolvedValue('line1\nconst x = "target";\nline3');
|
|
121
121
|
const result = await handler({ pattern: 'target', path: 'file.ts' });
|
|
122
|
-
expect(result.content[0].text).toContain('Found "target"
|
|
123
|
-
expect(result.content[0].text).toContain('file.ts');
|
|
122
|
+
expect(result.content[0].text).toContain('Found matches for "target"');
|
|
123
|
+
expect(result.content[0].text).toContain('file.ts:2: const x = "target";');
|
|
124
|
+
});
|
|
125
|
+
it('should use regex when requested', async () => {
|
|
126
|
+
registerFileSystemTools(mockServer);
|
|
127
|
+
const handler = registeredTools['search_code'];
|
|
128
|
+
fs.stat.mockResolvedValue({ isFile: () => true });
|
|
129
|
+
fs.readFile.mockResolvedValue('const x = 123;');
|
|
130
|
+
const result = await handler({ pattern: '\\d+', path: 'file.ts', useRegex: true });
|
|
131
|
+
expect(result.content[0].text).toContain('Found matches');
|
|
132
|
+
expect(result.content[0].text).toContain('file.ts:1: const x = 123;');
|
|
133
|
+
});
|
|
134
|
+
it('should handle case sensitivity', async () => {
|
|
135
|
+
registerFileSystemTools(mockServer);
|
|
136
|
+
const handler = registeredTools['search_code'];
|
|
137
|
+
fs.stat.mockResolvedValue({ isFile: () => true });
|
|
138
|
+
fs.readFile.mockResolvedValue('TARGET');
|
|
139
|
+
// Case sensitive search for lowercase 'target' should fail
|
|
140
|
+
const result = await handler({ pattern: 'target', path: 'file.ts', caseSensitive: true });
|
|
141
|
+
expect(result.content[0].text).toContain('No matches found');
|
|
142
|
+
// Case insensitive (default) should pass
|
|
143
|
+
const result2 = await handler({ pattern: 'target', path: 'file.ts', caseSensitive: false });
|
|
144
|
+
expect(result2.content[0].text).toContain('Found matches');
|
|
124
145
|
});
|
|
125
146
|
it('should recursively search directory ignoring node_modules', async () => {
|
|
126
147
|
registerFileSystemTools(mockServer);
|
|
127
148
|
const handler = registeredTools['search_code'];
|
|
128
|
-
|
|
129
|
-
// Root -> [src (dir), node_modules (dir), root.ts (file)]
|
|
130
|
-
// src -> [deep.ts (file)]
|
|
131
|
-
fs.stat.mockResolvedValue({ isFile: () => false }); // Root is dir
|
|
149
|
+
fs.stat.mockResolvedValue({ isFile: () => false });
|
|
132
150
|
fs.readdir.mockImplementation(async (dirPath) => {
|
|
133
151
|
if (dirPath.endsWith('src')) {
|
|
134
152
|
return [{ name: 'deep.ts', isDirectory: () => false }];
|
|
@@ -136,7 +154,6 @@ describe('FileSystem Tools', () => {
|
|
|
136
154
|
if (dirPath.endsWith('node_modules')) {
|
|
137
155
|
return [{ name: 'lib.ts', isDirectory: () => false }];
|
|
138
156
|
}
|
|
139
|
-
// Root
|
|
140
157
|
return [
|
|
141
158
|
{ name: 'src', isDirectory: () => true },
|
|
142
159
|
{ name: 'node_modules', isDirectory: () => true },
|
|
@@ -148,13 +165,11 @@ describe('FileSystem Tools', () => {
|
|
|
148
165
|
return 'no match';
|
|
149
166
|
if (filePath.includes('deep.ts'))
|
|
150
167
|
return 'const a = "target";';
|
|
151
|
-
if (filePath.includes('lib.ts'))
|
|
152
|
-
return 'const a = "target";'; // Should be ignored
|
|
153
168
|
return '';
|
|
154
169
|
});
|
|
155
170
|
const result = await handler({ pattern: 'target', path: '.' });
|
|
156
|
-
expect(result.content[0].text).toContain('deep.ts');
|
|
157
|
-
expect(result.content[0].text).not.toContain('lib.ts');
|
|
171
|
+
expect(result.content[0].text).toContain('deep.ts:1: const a = "target";');
|
|
172
|
+
expect(result.content[0].text).not.toContain('lib.ts');
|
|
158
173
|
});
|
|
159
174
|
});
|
|
160
175
|
// Keeping original security tests if needed, or merging them.
|
package/dist/graph.js
CHANGED
|
@@ -4,16 +4,20 @@ import ts from 'typescript';
|
|
|
4
4
|
export class ProjectKnowledgeGraph {
|
|
5
5
|
nodes = new Map();
|
|
6
6
|
rootDir = '';
|
|
7
|
+
cache = { version: '1.0', files: {} };
|
|
8
|
+
cachePath = '';
|
|
7
9
|
constructor() { }
|
|
8
10
|
async build(rootDir) {
|
|
9
11
|
try {
|
|
10
12
|
this.rootDir = path.resolve(rootDir);
|
|
13
|
+
this.cachePath = path.join(this.rootDir, '.gemini_graph_cache.json');
|
|
11
14
|
// Check if rootDir exists and is a directory
|
|
12
15
|
const stats = await fs.stat(this.rootDir);
|
|
13
16
|
if (!stats.isDirectory()) {
|
|
14
17
|
throw new Error(`Path '${rootDir}' is not a directory.`);
|
|
15
18
|
}
|
|
16
19
|
this.nodes.clear();
|
|
20
|
+
await this.loadCache();
|
|
17
21
|
const files = await this.getAllFiles(this.rootDir);
|
|
18
22
|
// Step 1: Initialize nodes
|
|
19
23
|
for (const file of files) {
|
|
@@ -24,15 +28,56 @@ export class ProjectKnowledgeGraph {
|
|
|
24
28
|
symbols: []
|
|
25
29
|
});
|
|
26
30
|
}
|
|
27
|
-
// Step 2:
|
|
31
|
+
// Step 2: Identify files to parse vs cache hits
|
|
32
|
+
const filesToParse = [];
|
|
33
|
+
const extractionMap = new Map();
|
|
34
|
+
for (const file of files) {
|
|
35
|
+
try {
|
|
36
|
+
const stats = await fs.stat(file);
|
|
37
|
+
const cached = this.cache.files[file];
|
|
38
|
+
if (cached && cached.mtime === stats.mtimeMs) {
|
|
39
|
+
extractionMap.set(file, { imports: cached.imports, symbols: cached.symbols });
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
filesToParse.push(file);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
catch (e) {
|
|
46
|
+
filesToParse.push(file);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
// Step 3: Parse new/modified files concurrently
|
|
28
50
|
const CONCURRENCY_LIMIT = 20;
|
|
29
|
-
for (let i = 0; i <
|
|
30
|
-
const chunk =
|
|
31
|
-
await Promise.all(chunk.map(file =>
|
|
51
|
+
for (let i = 0; i < filesToParse.length; i += CONCURRENCY_LIMIT) {
|
|
52
|
+
const chunk = filesToParse.slice(i, i + CONCURRENCY_LIMIT);
|
|
53
|
+
await Promise.all(chunk.map(async (file) => {
|
|
54
|
+
const data = await this.parseFile(file);
|
|
55
|
+
extractionMap.set(file, data);
|
|
56
|
+
// Update cache
|
|
57
|
+
const stats = await fs.stat(file);
|
|
58
|
+
this.cache.files[file] = {
|
|
59
|
+
mtime: stats.mtimeMs,
|
|
60
|
+
imports: data.imports,
|
|
61
|
+
symbols: data.symbols
|
|
62
|
+
};
|
|
63
|
+
}));
|
|
64
|
+
}
|
|
65
|
+
// Prune deleted files from cache
|
|
66
|
+
for (const cachedFile of Object.keys(this.cache.files)) {
|
|
67
|
+
if (!this.nodes.has(cachedFile)) {
|
|
68
|
+
delete this.cache.files[cachedFile];
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
await this.saveCache();
|
|
72
|
+
// Step 4: Link Graph (Resolve imports and build importedBy)
|
|
73
|
+
for (const [filePath, data] of extractionMap.entries()) {
|
|
74
|
+
await this.linkFileNodes(filePath, data.imports, data.symbols);
|
|
32
75
|
}
|
|
33
76
|
return {
|
|
34
77
|
nodeCount: this.nodes.size,
|
|
35
|
-
totalFiles: files.length
|
|
78
|
+
totalFiles: files.length,
|
|
79
|
+
cachedFiles: files.length - filesToParse.length,
|
|
80
|
+
parsedFiles: filesToParse.length
|
|
36
81
|
};
|
|
37
82
|
}
|
|
38
83
|
catch (error) {
|
|
@@ -40,6 +85,27 @@ export class ProjectKnowledgeGraph {
|
|
|
40
85
|
throw error;
|
|
41
86
|
}
|
|
42
87
|
}
|
|
88
|
+
async loadCache() {
|
|
89
|
+
try {
|
|
90
|
+
const content = await fs.readFile(this.cachePath, 'utf-8');
|
|
91
|
+
const data = JSON.parse(content);
|
|
92
|
+
if (data.version === '1.0') {
|
|
93
|
+
this.cache = data;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
catch (e) {
|
|
97
|
+
// Ignore cache errors (start fresh)
|
|
98
|
+
this.cache = { version: '1.0', files: {} };
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
async saveCache() {
|
|
102
|
+
try {
|
|
103
|
+
await fs.writeFile(this.cachePath, JSON.stringify(this.cache, null, 2), 'utf-8');
|
|
104
|
+
}
|
|
105
|
+
catch (e) {
|
|
106
|
+
console.error('Failed to save graph cache:', e);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
43
109
|
async getAllFiles(dir) {
|
|
44
110
|
try {
|
|
45
111
|
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
@@ -58,6 +124,9 @@ export class ProjectKnowledgeGraph {
|
|
|
58
124
|
}
|
|
59
125
|
else {
|
|
60
126
|
if (/\.(ts|js|tsx|jsx|json|py|go|rs|java|c|cpp|h)$/.test(entry.name)) {
|
|
127
|
+
// Ignore cache file itself
|
|
128
|
+
if (entry.name === '.gemini_graph_cache.json')
|
|
129
|
+
continue;
|
|
61
130
|
files.push(res);
|
|
62
131
|
}
|
|
63
132
|
}
|
|
@@ -72,10 +141,10 @@ export class ProjectKnowledgeGraph {
|
|
|
72
141
|
async parseFile(filePath) {
|
|
73
142
|
const ext = path.extname(filePath);
|
|
74
143
|
if (['.ts', '.js', '.tsx', '.jsx'].includes(ext)) {
|
|
75
|
-
await this.parseTypeScript(filePath);
|
|
144
|
+
return await this.parseTypeScript(filePath);
|
|
76
145
|
}
|
|
77
146
|
else {
|
|
78
|
-
await this.parseGeneric(filePath);
|
|
147
|
+
return await this.parseGeneric(filePath);
|
|
79
148
|
}
|
|
80
149
|
}
|
|
81
150
|
async parseTypeScript(filePath) {
|
|
@@ -138,10 +207,11 @@ export class ProjectKnowledgeGraph {
|
|
|
138
207
|
ts.forEachChild(node, visit);
|
|
139
208
|
};
|
|
140
209
|
visit(sourceFile);
|
|
141
|
-
|
|
210
|
+
return { imports, symbols };
|
|
142
211
|
}
|
|
143
212
|
catch (error) {
|
|
144
213
|
console.error(`Error parsing TypeScript file ${filePath}:`, error);
|
|
214
|
+
return { imports: [], symbols: [] };
|
|
145
215
|
}
|
|
146
216
|
}
|
|
147
217
|
async parseGeneric(filePath) {
|
|
@@ -176,14 +246,14 @@ export class ProjectKnowledgeGraph {
|
|
|
176
246
|
else if (ext === '.go') {
|
|
177
247
|
// 1. Go Imports
|
|
178
248
|
// Single line: import "fmt"
|
|
179
|
-
const singleImportMatches = content.matchAll(/import\s
|
|
249
|
+
const singleImportMatches = content.matchAll(/import\s+\"([^\"]+)\"/g);
|
|
180
250
|
for (const match of singleImportMatches)
|
|
181
251
|
imports.push(match[1]);
|
|
182
252
|
// Block: import ( "fmt"; "os" )
|
|
183
253
|
const blockImportMatches = content.matchAll(/import\s+\(([\s\S]*?)\)/g);
|
|
184
254
|
for (const match of blockImportMatches) {
|
|
185
255
|
const block = match[1];
|
|
186
|
-
const innerMatches = block.matchAll(/"([
|
|
256
|
+
const innerMatches = block.matchAll(/"([^\"]+)"/g);
|
|
187
257
|
for (const im of innerMatches)
|
|
188
258
|
imports.push(im[1]);
|
|
189
259
|
}
|
|
@@ -197,18 +267,19 @@ export class ProjectKnowledgeGraph {
|
|
|
197
267
|
for (const match of typeMatches)
|
|
198
268
|
symbols.push(`type:${match[1]}`);
|
|
199
269
|
}
|
|
200
|
-
|
|
270
|
+
return { imports, symbols };
|
|
201
271
|
}
|
|
202
272
|
catch (error) {
|
|
203
273
|
console.error(`Error parsing generic file ${filePath}:`, error);
|
|
274
|
+
return { imports: [], symbols: [] };
|
|
204
275
|
}
|
|
205
276
|
}
|
|
206
|
-
async
|
|
277
|
+
async linkFileNodes(filePath, rawImports, symbols) {
|
|
207
278
|
const currentNode = this.nodes.get(filePath);
|
|
208
279
|
if (!currentNode)
|
|
209
280
|
return;
|
|
210
281
|
currentNode.symbols = symbols;
|
|
211
|
-
for (const importPath of
|
|
282
|
+
for (const importPath of rawImports) {
|
|
212
283
|
let resolvedPath = null;
|
|
213
284
|
if (importPath.startsWith('.')) {
|
|
214
285
|
resolvedPath = await this.resolvePath(path.dirname(filePath), importPath);
|
|
@@ -225,9 +296,12 @@ export class ProjectKnowledgeGraph {
|
|
|
225
296
|
}
|
|
226
297
|
}
|
|
227
298
|
else {
|
|
228
|
-
// If we can't resolve to a local file, keep the original import string
|
|
229
|
-
|
|
230
|
-
|
|
299
|
+
// If we can't resolve to a local file, keep the original import string ONLY if it looks like an external package
|
|
300
|
+
// Ignore relative paths that failed to resolve (broken links)
|
|
301
|
+
if (!importPath.startsWith('.') && !path.isAbsolute(importPath)) {
|
|
302
|
+
if (!currentNode.imports.includes(importPath)) {
|
|
303
|
+
currentNode.imports.push(importPath);
|
|
304
|
+
}
|
|
231
305
|
}
|
|
232
306
|
}
|
|
233
307
|
}
|
package/dist/graph.test.js
CHANGED
|
@@ -7,6 +7,12 @@ describe('ProjectKnowledgeGraph', () => {
|
|
|
7
7
|
beforeEach(() => {
|
|
8
8
|
graph = new ProjectKnowledgeGraph();
|
|
9
9
|
vi.resetAllMocks();
|
|
10
|
+
// Default mocks for stat and writeFile
|
|
11
|
+
fs.stat.mockResolvedValue({
|
|
12
|
+
isDirectory: () => true,
|
|
13
|
+
mtimeMs: 1000
|
|
14
|
+
});
|
|
15
|
+
fs.writeFile.mockResolvedValue(undefined);
|
|
10
16
|
});
|
|
11
17
|
it('should ignore imports in comments', async () => {
|
|
12
18
|
const mockFiles = ['/app/index.ts', '/app/utils.ts', '/app/oldUtils.ts'];
|
|
@@ -22,6 +28,8 @@ describe('ProjectKnowledgeGraph', () => {
|
|
|
22
28
|
{ name: 'oldUtils.ts', isDirectory: () => false }
|
|
23
29
|
]);
|
|
24
30
|
fs.readFile.mockImplementation(async (path) => {
|
|
31
|
+
if (path.includes('graph_cache.json'))
|
|
32
|
+
return '{}'; // Mock empty cache
|
|
25
33
|
if (path.includes('index.ts'))
|
|
26
34
|
return mockContentIndex;
|
|
27
35
|
return '';
|
|
@@ -50,6 +58,8 @@ describe('ProjectKnowledgeGraph', () => {
|
|
|
50
58
|
{ name: 'lib.ts', isDirectory: () => false }
|
|
51
59
|
]);
|
|
52
60
|
fs.readFile.mockImplementation(async (filePath) => {
|
|
61
|
+
if (filePath.includes('graph_cache.json'))
|
|
62
|
+
return '{}';
|
|
53
63
|
if (filePath.endsWith('index.ts'))
|
|
54
64
|
return mockContentIndex;
|
|
55
65
|
return '';
|
|
@@ -68,6 +78,8 @@ describe('ProjectKnowledgeGraph', () => {
|
|
|
68
78
|
{ name: 'Button.jsx', isDirectory: () => false }
|
|
69
79
|
]);
|
|
70
80
|
fs.readFile.mockImplementation(async (filePath) => {
|
|
81
|
+
if (filePath.includes('graph_cache.json'))
|
|
82
|
+
return '{}';
|
|
71
83
|
if (filePath.endsWith('index.js'))
|
|
72
84
|
return mockContentIndex;
|
|
73
85
|
return '';
|
|
@@ -84,6 +96,8 @@ describe('ProjectKnowledgeGraph', () => {
|
|
|
84
96
|
{ name: 'b.ts', isDirectory: () => false }
|
|
85
97
|
]);
|
|
86
98
|
fs.readFile.mockImplementation(async (filePath) => {
|
|
99
|
+
if (filePath.includes('graph_cache.json'))
|
|
100
|
+
return '{}';
|
|
87
101
|
if (filePath.endsWith('a.ts'))
|
|
88
102
|
return contentA;
|
|
89
103
|
if (filePath.endsWith('b.ts'))
|
|
@@ -104,6 +118,8 @@ describe('ProjectKnowledgeGraph', () => {
|
|
|
104
118
|
{ name: 'a.ts', isDirectory: () => false }
|
|
105
119
|
]);
|
|
106
120
|
fs.readFile.mockImplementation(async (filePath) => {
|
|
121
|
+
if (filePath.includes('graph_cache.json'))
|
|
122
|
+
return '{}';
|
|
107
123
|
if (filePath.endsWith('a.ts'))
|
|
108
124
|
return contentA;
|
|
109
125
|
return '';
|
|
@@ -120,6 +136,8 @@ describe('ProjectKnowledgeGraph', () => {
|
|
|
120
136
|
{ name: 'a.ts', isDirectory: () => false }
|
|
121
137
|
]);
|
|
122
138
|
fs.readFile.mockImplementation(async (filePath) => {
|
|
139
|
+
if (filePath.includes('graph_cache.json'))
|
|
140
|
+
return '{}';
|
|
123
141
|
if (filePath.endsWith('a.ts'))
|
|
124
142
|
return contentA;
|
|
125
143
|
return '';
|
package/dist/tools/filesystem.js
CHANGED
|
@@ -7,7 +7,7 @@ export function registerFileSystemTools(server) {
|
|
|
7
7
|
server.tool("shell_execute", "Execute a shell command. SECURITY WARNING: Use this ONLY for safe, non-destructive commands. Avoid 'rm -rf /', format, or destructive operations.", {
|
|
8
8
|
command: z.string().describe("The bash command to execute")
|
|
9
9
|
}, async ({ command }) => {
|
|
10
|
-
const dangerousPatterns = [
|
|
10
|
+
const dangerousPatterns = [new RegExp('rm\\s+-rf\\s+\\/'), /mkfs/, /dd\s+if=/];
|
|
11
11
|
if (dangerousPatterns.some(p => p.test(command))) {
|
|
12
12
|
return {
|
|
13
13
|
content: [{ type: "text", text: "Error: Dangerous command pattern detected. Execution blocked for safety." }],
|
|
@@ -68,47 +68,65 @@ export function registerFileSystemTools(server) {
|
|
|
68
68
|
}
|
|
69
69
|
});
|
|
70
70
|
// 10. search_code
|
|
71
|
-
server.tool("search_code", "Search for a text pattern in project files (
|
|
72
|
-
pattern: z.string().describe("The text to search for"),
|
|
73
|
-
path: z.string().optional().default('.').describe("Root directory to search")
|
|
74
|
-
|
|
71
|
+
server.tool("search_code", "Search for a text pattern in project files with advanced options (regex, case sensitivity).", {
|
|
72
|
+
pattern: z.string().describe("The text or regex pattern to search for"),
|
|
73
|
+
path: z.string().optional().default('.').describe("Root directory to search"),
|
|
74
|
+
useRegex: z.boolean().optional().default(false).describe("Treat pattern as a regular expression"),
|
|
75
|
+
caseSensitive: z.boolean().optional().default(false).describe("Match case sensitive (default: false)")
|
|
76
|
+
}, async ({ pattern, path: searchPath, useRegex, caseSensitive }) => {
|
|
75
77
|
try {
|
|
76
78
|
const resolvedPath = validatePath(searchPath || '.');
|
|
77
79
|
const stats = await fs.stat(resolvedPath);
|
|
80
|
+
const results = [];
|
|
81
|
+
const searchFile = async (filePath) => {
|
|
82
|
+
try {
|
|
83
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
84
|
+
const lines = content.split('\n');
|
|
85
|
+
let regex;
|
|
86
|
+
if (useRegex) {
|
|
87
|
+
regex = new RegExp(pattern, caseSensitive ? 'g' : 'gi');
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
// Escape regex special chars if not using regex
|
|
91
|
+
const escaped = pattern.replace(/[.*+?^${}()|[\\]/g, '\\$&');
|
|
92
|
+
regex = new RegExp(escaped, caseSensitive ? 'g' : 'gi');
|
|
93
|
+
}
|
|
94
|
+
lines.forEach((line, index) => {
|
|
95
|
+
if (regex.test(line)) {
|
|
96
|
+
// Reset lastIndex if global (not strictly needed for test() but good practice)
|
|
97
|
+
regex.lastIndex = 0;
|
|
98
|
+
results.push(`${filePath}:${index + 1}: ${line.trim()}`);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
catch (err) {
|
|
103
|
+
// Ignore read errors (binary files etc)
|
|
104
|
+
}
|
|
105
|
+
};
|
|
78
106
|
if (stats.isFile()) {
|
|
79
|
-
|
|
80
|
-
const matches = content.includes(pattern) ? [resolvedPath] : [];
|
|
81
|
-
return {
|
|
82
|
-
content: [{
|
|
83
|
-
type: "text",
|
|
84
|
-
text: matches.length > 0 ? `Found "${pattern}" in:\n${matches.join('\n')}` : `No matches found for "${pattern}"`
|
|
85
|
-
}]
|
|
86
|
-
};
|
|
107
|
+
await searchFile(resolvedPath);
|
|
87
108
|
}
|
|
88
|
-
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
if (content.includes(pattern)) {
|
|
101
|
-
results.push(fullPath);
|
|
109
|
+
else {
|
|
110
|
+
const searchDir = async (dir) => {
|
|
111
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
112
|
+
for (const entry of entries) {
|
|
113
|
+
const fullPath = path.join(dir, entry.name);
|
|
114
|
+
if (entry.isDirectory()) {
|
|
115
|
+
if (['node_modules', '.git', 'dist', 'coverage', '.gemini'].includes(entry.name))
|
|
116
|
+
continue;
|
|
117
|
+
await searchDir(fullPath);
|
|
118
|
+
}
|
|
119
|
+
else if (/\.(ts|js|json|md|txt|html|css|py|java|c|cpp|h|rs|go|sh|yaml|yml)$/.test(entry.name)) {
|
|
120
|
+
await searchFile(fullPath);
|
|
102
121
|
}
|
|
103
122
|
}
|
|
104
|
-
}
|
|
105
|
-
|
|
123
|
+
};
|
|
124
|
+
await searchDir(resolvedPath);
|
|
106
125
|
}
|
|
107
|
-
const matches = await searchDir(resolvedPath);
|
|
108
126
|
return {
|
|
109
127
|
content: [{
|
|
110
128
|
type: "text",
|
|
111
|
-
text:
|
|
129
|
+
text: results.length > 0 ? `Found matches for "${pattern}":\n${results.join('\n')}` : `No matches found for "${pattern}"`
|
|
112
130
|
}]
|
|
113
131
|
};
|
|
114
132
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { ProjectKnowledgeGraph } from './graph.js';
|
|
3
|
+
import * as fs from 'fs/promises';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
describe('Graph Caching Verification', () => {
|
|
6
|
+
it('should use cache on second run', async () => {
|
|
7
|
+
const graph = new ProjectKnowledgeGraph();
|
|
8
|
+
const root = process.cwd();
|
|
9
|
+
// Cleanup existing cache
|
|
10
|
+
const cachePath = path.join(root, '.gemini_graph_cache.json');
|
|
11
|
+
try {
|
|
12
|
+
await fs.unlink(cachePath);
|
|
13
|
+
}
|
|
14
|
+
catch (e) { }
|
|
15
|
+
console.log('--- Run 1 (Fresh) ---');
|
|
16
|
+
const res1 = await graph.build(root);
|
|
17
|
+
console.log('Result 1:', res1);
|
|
18
|
+
expect(res1.parsedFiles).toBeGreaterThan(0);
|
|
19
|
+
expect(res1.cachedFiles).toBe(0);
|
|
20
|
+
console.log('--- Run 2 (Cached) ---');
|
|
21
|
+
const res2 = await graph.build(root);
|
|
22
|
+
console.log('Result 2:', res2);
|
|
23
|
+
expect(res2.parsedFiles).toBe(0);
|
|
24
|
+
expect(res2.cachedFiles).toBeGreaterThan(0);
|
|
25
|
+
expect(res2.nodeCount).toBe(res1.nodeCount);
|
|
26
|
+
});
|
|
27
|
+
});
|