@gotza02/sequential-thinking 2026.1.28 → 2026.2.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/dist/index.js CHANGED
@@ -1,450 +1,32 @@
1
1
  #!/usr/bin/env node
2
2
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
- import { z } from "zod";
5
4
  import { SequentialThinkingServer } from './lib.js';
6
- import * as fs from 'fs/promises';
7
- import { exec } from 'child_process';
8
- import { promisify } from 'util';
9
5
  import { ProjectKnowledgeGraph } from './graph.js';
10
- import * as path from 'path';
11
- import { JSDOM } from 'jsdom';
12
- import { Readability } from '@mozilla/readability';
13
- import TurndownService from 'turndown';
14
- const execAsync = promisify(exec);
15
- const DEFAULT_HEADERS = {
16
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
17
- 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8',
18
- 'Accept-Language': 'en-US,en;q=0.9',
19
- };
20
- async function fetchWithRetry(url, options = {}, retries = 3, backoff = 1000) {
21
- const fetchOptions = {
22
- ...options,
23
- headers: { ...DEFAULT_HEADERS, ...options.headers }
24
- };
25
- try {
26
- const response = await fetch(url, fetchOptions);
27
- if (response.status === 429 && retries > 0) {
28
- const retryAfter = response.headers.get('Retry-After');
29
- const waitTime = retryAfter ? parseInt(retryAfter) * 1000 : backoff;
30
- await new Promise(resolve => setTimeout(resolve, waitTime));
31
- return fetchWithRetry(url, options, retries - 1, backoff * 2);
32
- }
33
- if (!response.ok && retries > 0 && response.status >= 500) {
34
- await new Promise(resolve => setTimeout(resolve, backoff));
35
- return fetchWithRetry(url, options, retries - 1, backoff * 2);
36
- }
37
- return response;
38
- }
39
- catch (error) {
40
- if (retries > 0) {
41
- await new Promise(resolve => setTimeout(resolve, backoff));
42
- return fetchWithRetry(url, options, retries - 1, backoff * 2);
43
- }
44
- throw error;
45
- }
46
- }
6
+ import { NotesManager } from './notes.js';
7
+ import { registerThinkingTools } from './tools/thinking.js';
8
+ import { registerWebTools } from './tools/web.js';
9
+ import { registerFileSystemTools } from './tools/filesystem.js';
10
+ import { registerGraphTools } from './tools/graph.js';
11
+ import { registerNoteTools } from './tools/notes.js';
47
12
  const server = new McpServer({
48
13
  name: "sequential-thinking-server",
49
14
  version: "2026.1.18",
50
15
  });
51
16
  const thinkingServer = new SequentialThinkingServer(process.env.THOUGHTS_STORAGE_PATH || 'thoughts_history.json', parseInt(process.env.THOUGHT_DELAY_MS || '0', 10));
52
17
  const knowledgeGraph = new ProjectKnowledgeGraph();
53
- // --- Sequential Thinking Tool ---
54
- server.tool("sequentialthinking", `A detailed tool for dynamic and reflective problem-solving through thoughts.
55
- This tool helps analyze problems through a flexible thinking process that can adapt and evolve.
56
- Each thought can build on, question, or revise previous insights as understanding deepens.
57
-
58
- When to use this tool:
59
- - Breaking down complex problems into steps
60
- - Planning and design with room for revision
61
- - Analysis that might need course correction
62
- - Problems where the full scope might not be clear initially
63
- - Problems that require a multi-step solution
64
- - Tasks that need to maintain context over multiple steps
65
- - Situations where irrelevant information needs to be filtered out
66
-
67
- Key features:
68
- - You can adjust total_thoughts up or down as you progress
69
- - You can question or revise previous thoughts
70
- - You can add more thoughts even after reaching what seemed like the end
71
- - You can express uncertainty and explore alternative approaches
72
- - Not every thought needs to build linearly - you can branch or backtrack
73
- - Iterative Reasoning: Think step-by-step in a structured manner
74
- - Tree of Thoughts: Generate and evaluate multiple options (Conservative/Balanced/Aggressive)
75
- - Self-Critique: Check for risks, biases, and errors in thinking
76
- - Branch Merging: Combine insights from multiple divergent paths
77
- - Hypothesis Testing: Formulate and verify hypotheses
78
- - Generates a solution hypothesis
79
- - Verifies the hypothesis based on the Chain of Thought steps
80
- - Repeats the process until satisfied
81
- - Provides a correct answer
82
-
83
- Parameters explained:
84
- - thought: Your current thinking step, which can include:
85
- * Regular analytical steps
86
- * Revisions of previous thoughts
87
- * Questions about previous decisions
88
- * Realizations about needing more analysis
89
- * Changes in approach
90
- * Hypothesis generation
91
- * Hypothesis verification
92
- - nextThoughtNeeded: True if you need more thinking, even if at what seemed like the end
93
- - thoughtNumber: Current number in sequence (can go beyond initial total if needed)
94
- - totalThoughts: Current estimate of thoughts needed (can be adjusted up/down)
95
- - isRevision: A boolean indicating if this thought revises previous thinking
96
- - revisesThought: If is_revision is true, which thought number is being reconsidered
97
- - branchFromThought: If branching, which thought number is the branching point
98
- - branchId: Identifier for the current branch (if any)
99
- - needsMoreThoughts: If reaching end but realizing more thoughts needed
100
- - thoughtType: The type of thought (analysis, generation, evaluation, reflexion, selection)
101
- - score: Score for evaluation (1-10)
102
- - options: List of options generated
103
- - selectedOption: The option selected
104
-
105
- You should:
106
- 1. Start with an initial estimate of needed thoughts, but be ready to adjust
107
- 2. Feel free to question or revise previous thoughts
108
- 3. Don't hesitate to add more thoughts if needed, even at the "end"
109
- 4. Express uncertainty when present
110
- 5. Mark thoughts that revise previous thinking or branch into new paths
111
- 6. Ignore information that is irrelevant to the current step
112
- 7. Generate a solution hypothesis when appropriate
113
- 8. Verify the hypothesis based on the Chain of Thought steps
114
- 9. Repeat the process until satisfied with the solution
115
- 10. Provide a single, ideally correct answer as the final output
116
- 11. Only set nextThoughtNeeded to false when truly done and a satisfactory answer is reached`, {
117
- thought: z.string().describe("Your current thinking step"),
118
- nextThoughtNeeded: z.boolean().describe("Whether another thought step is needed"),
119
- thoughtNumber: z.number().int().min(1).describe("Current thought number (numeric value, e.g., 1, 2, 3)"),
120
- totalThoughts: z.number().int().min(1).describe("Estimated total thoughts needed (numeric value, e.g., 5, 10)"),
121
- isRevision: z.boolean().optional().describe("Whether this revises previous thinking"),
122
- revisesThought: z.number().int().min(1).optional().describe("Which thought is being reconsidered"),
123
- branchFromThought: z.number().int().min(1).optional().describe("Branching point thought number"),
124
- branchId: z.string().optional().describe("Branch identifier"),
125
- needsMoreThoughts: z.boolean().optional().describe("If more thoughts are needed"),
126
- thoughtType: z.enum(['analysis', 'generation', 'evaluation', 'reflexion', 'selection']).optional().describe("The type of thought"),
127
- score: z.number().min(1).max(10).optional().describe("Score for evaluation (1-10)"),
128
- options: z.array(z.string()).optional().describe("List of options generated"),
129
- selectedOption: z.string().optional().describe("The option selected")
130
- }, async (args) => {
131
- const result = await thinkingServer.processThought(args);
132
- return {
133
- content: result.content,
134
- isError: result.isError
135
- };
136
- });
137
- // --- New Tools ---
138
- // 1. web_search
139
- server.tool("web_search", "Search the web using Brave or Exa APIs (requires API keys in environment variables: BRAVE_API_KEY or EXA_API_KEY).", {
140
- query: z.string().describe("The search query"),
141
- provider: z.enum(['brave', 'exa', 'google']).optional().describe("Preferred search provider")
142
- }, async ({ query, provider }) => {
143
- try {
144
- // Priority: User Preference > Brave > Exa > Google
145
- let selectedProvider = provider;
146
- if (!selectedProvider) {
147
- if (process.env.BRAVE_API_KEY)
148
- selectedProvider = 'brave';
149
- else if (process.env.EXA_API_KEY)
150
- selectedProvider = 'exa';
151
- else if (process.env.GOOGLE_SEARCH_API_KEY)
152
- selectedProvider = 'google';
153
- else
154
- return { content: [{ type: "text", text: "Error: No search provider configured. Please set BRAVE_API_KEY, EXA_API_KEY, or GOOGLE_SEARCH_API_KEY." }], isError: true };
155
- }
156
- if (selectedProvider === 'brave') {
157
- if (!process.env.BRAVE_API_KEY)
158
- throw new Error("BRAVE_API_KEY not found");
159
- const response = await fetchWithRetry(`https://api.search.brave.com/res/v1/web/search?q=${encodeURIComponent(query)}&count=5`, {
160
- headers: { 'X-Subscription-Token': process.env.BRAVE_API_KEY }
161
- });
162
- if (!response.ok)
163
- throw new Error(`Brave API error: ${response.statusText}`);
164
- const data = await response.json();
165
- return { content: [{ type: "text", text: JSON.stringify(data.web?.results || data, null, 2) }] };
166
- }
167
- if (selectedProvider === 'exa') {
168
- if (!process.env.EXA_API_KEY)
169
- throw new Error("EXA_API_KEY not found");
170
- const response = await fetchWithRetry('https://api.exa.ai/search', {
171
- method: 'POST',
172
- headers: {
173
- 'x-api-key': process.env.EXA_API_KEY,
174
- 'Content-Type': 'application/json'
175
- },
176
- body: JSON.stringify({ query, numResults: 5 })
177
- });
178
- if (!response.ok)
179
- throw new Error(`Exa API error: ${response.statusText}`);
180
- const data = await response.json();
181
- return { content: [{ type: "text", text: JSON.stringify(data.results || data, null, 2) }] };
182
- }
183
- if (selectedProvider === 'google') {
184
- if (!process.env.GOOGLE_SEARCH_API_KEY)
185
- throw new Error("GOOGLE_SEARCH_API_KEY not found");
186
- if (!process.env.GOOGLE_SEARCH_CX)
187
- throw new Error("GOOGLE_SEARCH_CX (Search Engine ID) not found");
188
- const response = await fetchWithRetry(`https://www.googleapis.com/customsearch/v1?key=${process.env.GOOGLE_SEARCH_API_KEY}&cx=${process.env.GOOGLE_SEARCH_CX}&q=${encodeURIComponent(query)}&num=5`);
189
- if (!response.ok)
190
- throw new Error(`Google API error: ${response.statusText}`);
191
- const data = await response.json();
192
- // Extract relevant fields to keep output clean
193
- const results = data.items?.map((item) => ({
194
- title: item.title,
195
- link: item.link,
196
- snippet: item.snippet
197
- })) || [];
198
- return { content: [{ type: "text", text: JSON.stringify(results, null, 2) }] };
199
- }
200
- return { content: [{ type: "text", text: "Error: Unsupported or unconfigured provider." }], isError: true };
201
- }
202
- catch (error) {
203
- return {
204
- content: [{ type: "text", text: `Search Error: ${error instanceof Error ? error.message : String(error)}` }],
205
- isError: true
206
- };
207
- }
208
- });
209
- // 2. fetch
210
- server.tool("fetch", "Perform an HTTP request to a specific URL.", {
211
- url: z.string().url().describe("The URL to fetch"),
212
- method: z.enum(['GET', 'POST', 'PUT', 'DELETE']).optional().default('GET').describe("HTTP Method"),
213
- headers: z.record(z.string(), z.string()).optional().describe("HTTP Headers"),
214
- body: z.string().optional().describe("Request body (for POST/PUT)")
215
- }, async ({ url, method, headers, body }) => {
216
- try {
217
- const response = await fetchWithRetry(url, {
218
- method,
219
- headers: headers || {},
220
- body: body
221
- });
222
- const text = await response.text();
223
- return {
224
- content: [{
225
- type: "text",
226
- text: `Status: ${response.status}\n\n${text.substring(0, 10000)}${text.length > 10000 ? '\n...(truncated)' : ''}`
227
- }]
228
- };
229
- }
230
- catch (error) {
231
- return {
232
- content: [{ type: "text", text: `Fetch Error: ${error instanceof Error ? error.message : String(error)}` }],
233
- isError: true
234
- };
235
- }
236
- });
237
- // 3. shell_execute
238
- server.tool("shell_execute", "Execute a shell command. Use with caution.", {
239
- command: z.string().describe("The bash command to execute")
240
- }, async ({ command }) => {
241
- try {
242
- const { stdout, stderr } = await execAsync(command);
243
- return {
244
- content: [{
245
- type: "text",
246
- text: `STDOUT:\n${stdout}\n\nSTDERR:\n${stderr}`
247
- }]
248
- };
249
- }
250
- catch (error) {
251
- return {
252
- content: [{ type: "text", text: `Shell Error: ${error instanceof Error ? error.message : String(error)}` }],
253
- isError: true
254
- };
255
- }
256
- });
257
- // 4. read_file
258
- server.tool("read_file", "Read the contents of a file.", {
259
- path: z.string().describe("Path to the file")
260
- }, async ({ path }) => {
261
- try {
262
- const content = await fs.readFile(path, 'utf-8');
263
- return {
264
- content: [{ type: "text", text: content }]
265
- };
266
- }
267
- catch (error) {
268
- return {
269
- content: [{ type: "text", text: `Read Error: ${error instanceof Error ? error.message : String(error)}` }],
270
- isError: true
271
- };
272
- }
273
- });
274
- // 5. write_file
275
- server.tool("write_file", "Write content to a file (overwrites existing).", {
276
- path: z.string().describe("Path to the file"),
277
- content: z.string().describe("Content to write")
278
- }, async ({ path, content }) => {
279
- try {
280
- await fs.writeFile(path, content, 'utf-8');
281
- return {
282
- content: [{ type: "text", text: `Successfully wrote to ${path}` }]
283
- };
284
- }
285
- catch (error) {
286
- return {
287
- content: [{ type: "text", text: `Write Error: ${error instanceof Error ? error.message : String(error)}` }],
288
- isError: true
289
- };
290
- }
291
- });
292
- // --- Project Knowledge Graph Tools ---
293
- // 6. build_project_graph
294
- server.tool("build_project_graph", "Scan the directory and build a dependency graph of the project (Analyzing imports/exports).", {
295
- path: z.string().optional().default('.').describe("Root directory path to scan (default: current dir)")
296
- }, async ({ path }) => {
297
- try {
298
- const result = await knowledgeGraph.build(path || '.');
299
- return {
300
- content: [{ type: "text", text: `Graph built successfully.\nNodes: ${result.nodeCount}\nTotal Scanned Files: ${result.totalFiles}` }]
301
- };
302
- }
303
- catch (error) {
304
- return {
305
- content: [{ type: "text", text: `Graph Build Error: ${error instanceof Error ? error.message : String(error)}` }],
306
- isError: true
307
- };
308
- }
309
- });
310
- // 7. get_file_relationships
311
- server.tool("get_file_relationships", "Get dependencies and references for a specific file from the built graph.", {
312
- filePath: z.string().describe("Path to the file (e.g., 'src/index.ts')")
313
- }, async ({ filePath }) => {
314
- try {
315
- const rel = knowledgeGraph.getRelationships(filePath);
316
- if (!rel) {
317
- return {
318
- content: [{ type: "text", text: `File not found in graph: ${filePath}. (Did you run 'build_project_graph'?)` }],
319
- isError: true
320
- };
321
- }
322
- return {
323
- content: [{ type: "text", text: JSON.stringify(rel, null, 2) }]
324
- };
325
- }
326
- catch (error) {
327
- return {
328
- content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}` }],
329
- isError: true
330
- };
331
- }
332
- });
333
- // 8. get_project_graph_summary
334
- server.tool("get_project_graph_summary", "Get a summary of the project structure (most referenced files, total count).", {}, async () => {
335
- try {
336
- const summary = knowledgeGraph.getSummary();
337
- return {
338
- content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
339
- };
340
- }
341
- catch (error) {
342
- return {
343
- content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}` }],
344
- isError: true
345
- };
346
- }
347
- });
18
+ const notesManager = new NotesManager(process.env.NOTES_STORAGE_PATH || 'project_notes.json');
19
+ // Register tools
20
+ registerThinkingTools(server, thinkingServer);
21
+ registerWebTools(server);
22
+ registerFileSystemTools(server);
23
+ registerGraphTools(server, knowledgeGraph);
24
+ registerNoteTools(server, notesManager);
348
25
  async function runServer() {
349
26
  const transport = new StdioServerTransport();
350
27
  await server.connect(transport);
351
28
  console.error("Sequential Thinking MCP Server (Extended) running on stdio");
352
29
  }
353
- // --- New Tools v2026.1.27 ---
354
- // 9. read_webpage
355
- server.tool("read_webpage", "Read a webpage and convert it to clean Markdown (removes ads, navs, etc.).", {
356
- url: z.string().url().describe("The URL to read")
357
- }, async ({ url }) => {
358
- try {
359
- const response = await fetchWithRetry(url);
360
- const html = await response.text();
361
- const doc = new JSDOM(html, { url });
362
- const reader = new Readability(doc.window.document);
363
- const article = reader.parse();
364
- if (!article)
365
- throw new Error("Could not parse article content");
366
- const turndownService = new TurndownService();
367
- const markdown = turndownService.turndown(article.content || "");
368
- return {
369
- content: [{
370
- type: "text",
371
- text: `Title: ${article.title}\n\n${markdown}`
372
- }]
373
- };
374
- }
375
- catch (error) {
376
- return {
377
- content: [{ type: "text", text: `Read Error: ${error instanceof Error ? error.message : String(error)}` }],
378
- isError: true
379
- };
380
- }
381
- });
382
- // 10. search_code
383
- server.tool("search_code", "Search for a text pattern in project files (excludes node_modules, etc.).", {
384
- pattern: z.string().describe("The text to search for"),
385
- path: z.string().optional().default('.').describe("Root directory to search")
386
- }, async ({ pattern, path: searchPath }) => {
387
- try {
388
- async function searchDir(dir) {
389
- const results = [];
390
- const entries = await fs.readdir(dir, { withFileTypes: true });
391
- for (const entry of entries) {
392
- const fullPath = path.join(dir, entry.name);
393
- if (entry.isDirectory()) {
394
- if (['node_modules', '.git', 'dist', 'coverage', '.gemini'].includes(entry.name))
395
- continue;
396
- results.push(...await searchDir(fullPath));
397
- }
398
- else if (/\.(ts|js|json|md|txt|html|css|py|java|c|cpp|h|rs|go)$/.test(entry.name)) {
399
- const content = await fs.readFile(fullPath, 'utf-8');
400
- if (content.includes(pattern)) {
401
- results.push(fullPath);
402
- }
403
- }
404
- }
405
- return results;
406
- }
407
- const matches = await searchDir(path.resolve(searchPath || '.'));
408
- return {
409
- content: [{
410
- type: "text",
411
- text: matches.length > 0 ? `Found "${pattern}" in:\n${matches.join('\n')}` : `No matches found for "${pattern}"`
412
- }]
413
- };
414
- }
415
- catch (error) {
416
- return {
417
- content: [{ type: "text", text: `Search Error: ${error instanceof Error ? error.message : String(error)}` }],
418
- isError: true
419
- };
420
- }
421
- });
422
- // 11. clear_thought_history
423
- server.tool("clear_thought_history", "Clear the sequential thinking history.", {}, async () => {
424
- await thinkingServer.clearHistory();
425
- return {
426
- content: [{ type: "text", text: "Thought history cleared." }]
427
- };
428
- });
429
- // 12. summarize_history
430
- server.tool("summarize_history", "Compress multiple thoughts into a single summary thought to save space/context.", {
431
- startIndex: z.number().int().min(1).describe("The starting thought number to summarize"),
432
- endIndex: z.number().int().min(1).describe("The ending thought number to summarize"),
433
- summary: z.string().describe("The summary text that replaces the range")
434
- }, async ({ startIndex, endIndex, summary }) => {
435
- try {
436
- const result = await thinkingServer.archiveHistory(startIndex, endIndex, summary);
437
- return {
438
- content: [{ type: "text", text: `Successfully summarized thoughts ${startIndex}-${endIndex}. New history length: ${result.newHistoryLength}` }]
439
- };
440
- }
441
- catch (error) {
442
- return {
443
- content: [{ type: "text", text: `Archive Error: ${error instanceof Error ? error.message : String(error)}` }],
444
- isError: true
445
- };
446
- }
447
- });
448
30
  runServer().catch((error) => {
449
31
  console.error("Fatal error running server:", error);
450
32
  process.exit(1);
package/dist/lib.js CHANGED
File without changes
package/dist/notes.js ADDED
@@ -0,0 +1,77 @@
1
+ import * as fs from 'fs/promises';
2
+ import * as path from 'path';
3
+ export class NotesManager {
4
+ filePath;
5
+ notes = [];
6
+ loaded = false;
7
+ constructor(storagePath = 'project_notes.json') {
8
+ this.filePath = path.resolve(storagePath);
9
+ }
10
+ async load() {
11
+ if (this.loaded)
12
+ return;
13
+ try {
14
+ const data = await fs.readFile(this.filePath, 'utf-8');
15
+ this.notes = JSON.parse(data);
16
+ }
17
+ catch (error) {
18
+ // If file doesn't exist, start with empty array
19
+ this.notes = [];
20
+ }
21
+ this.loaded = true;
22
+ }
23
+ async save() {
24
+ await fs.writeFile(this.filePath, JSON.stringify(this.notes, null, 2), 'utf-8');
25
+ }
26
+ async addNote(title, content, tags = []) {
27
+ await this.load();
28
+ const note = {
29
+ id: Date.now().toString(36) + Math.random().toString(36).substring(2, 7),
30
+ title,
31
+ content,
32
+ tags,
33
+ createdAt: new Date().toISOString(),
34
+ updatedAt: new Date().toISOString()
35
+ };
36
+ this.notes.push(note);
37
+ await this.save();
38
+ return note;
39
+ }
40
+ async listNotes(tag) {
41
+ await this.load();
42
+ if (tag) {
43
+ return this.notes.filter(n => n.tags.includes(tag));
44
+ }
45
+ return this.notes;
46
+ }
47
+ async searchNotes(query) {
48
+ await this.load();
49
+ const lowerQuery = query.toLowerCase();
50
+ return this.notes.filter(n => n.title.toLowerCase().includes(lowerQuery) ||
51
+ n.content.toLowerCase().includes(lowerQuery) ||
52
+ n.tags.some(t => t.toLowerCase().includes(lowerQuery)));
53
+ }
54
+ async deleteNote(id) {
55
+ await this.load();
56
+ const initialLength = this.notes.length;
57
+ this.notes = this.notes.filter(n => n.id !== id);
58
+ if (this.notes.length !== initialLength) {
59
+ await this.save();
60
+ return true;
61
+ }
62
+ return false;
63
+ }
64
+ async updateNote(id, updates) {
65
+ await this.load();
66
+ const index = this.notes.findIndex(n => n.id === id);
67
+ if (index === -1)
68
+ return null;
69
+ this.notes[index] = {
70
+ ...this.notes[index],
71
+ ...updates,
72
+ updatedAt: new Date().toISOString()
73
+ };
74
+ await this.save();
75
+ return this.notes[index];
76
+ }
77
+ }
@@ -0,0 +1,79 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import * as fs from 'fs/promises';
3
+ import * as path from 'path';
4
+ async function searchCodeLogic(pattern, searchPath = '.') {
5
+ try {
6
+ const resolvedPath = path.resolve(searchPath);
7
+ const stat = await fs.stat(resolvedPath);
8
+ if (stat.isFile()) {
9
+ const content = await fs.readFile(resolvedPath, 'utf-8');
10
+ if (content.includes(pattern)) {
11
+ return {
12
+ content: [{ type: "text", text: `Found "${pattern}" in:\n${resolvedPath}` }]
13
+ };
14
+ }
15
+ else {
16
+ return {
17
+ content: [{ type: "text", text: `No matches found for "${pattern}"` }]
18
+ };
19
+ }
20
+ }
21
+ async function searchDir(dir) {
22
+ const results = [];
23
+ const entries = await fs.readdir(dir, { withFileTypes: true });
24
+ for (const entry of entries) {
25
+ const fullPath = path.join(dir, entry.name);
26
+ if (entry.isDirectory()) {
27
+ if (['node_modules', '.git', 'dist', 'coverage', '.gemini'].includes(entry.name))
28
+ continue;
29
+ results.push(...await searchDir(fullPath));
30
+ }
31
+ else if (new RegExp('\\.(ts|js|json|md|txt|html|css|py|java|c|cpp|h|rs|go)$').test(entry.name)) {
32
+ const content = await fs.readFile(fullPath, 'utf-8');
33
+ if (content.includes(pattern)) {
34
+ results.push(fullPath);
35
+ }
36
+ }
37
+ }
38
+ return results;
39
+ }
40
+ const matches = await searchDir(resolvedPath);
41
+ const joinedMatches = matches.join('\n');
42
+ return {
43
+ content: [{
44
+ type: "text",
45
+ text: matches.length > 0 ? `Found "${pattern}" in:\n${joinedMatches}` : `No matches found for "${pattern}"`
46
+ }]
47
+ };
48
+ }
49
+ catch (error) {
50
+ return {
51
+ content: [{ type: "text", text: `Search Error: ${error instanceof Error ? error.message : String(error)}` }],
52
+ isError: true
53
+ };
54
+ }
55
+ }
56
+ describe('search_code tool', () => {
57
+ const testDir = path.join(__dirname, 'test_search_env');
58
+ beforeEach(async () => {
59
+ await fs.mkdir(testDir, { recursive: true });
60
+ await fs.writeFile(path.join(testDir, 'target.ts'), 'function myFunction() { return "found me"; }');
61
+ await fs.writeFile(path.join(testDir, 'other.ts'), 'const x = 10;');
62
+ await fs.mkdir(path.join(testDir, 'nested'), { recursive: true });
63
+ await fs.writeFile(path.join(testDir, 'nested', 'deep.ts'), 'export const secret = "found me too";');
64
+ await fs.mkdir(path.join(testDir, 'node_modules'), { recursive: true });
65
+ await fs.writeFile(path.join(testDir, 'node_modules', 'ignored.ts'), 'found me');
66
+ });
67
+ afterEach(async () => {
68
+ await fs.rm(testDir, { recursive: true, force: true });
69
+ });
70
+ // ... (previous tests)
71
+ it('should handle single file path', async () => {
72
+ // This test will FAIL with current implementation (simulated here with the fix applied? No, I need to test failure first)
73
+ // Wait, I updated searchCodeLogic above with the FIX.
74
+ // So this test checks if the FIX works.
75
+ const result = await searchCodeLogic('found me', path.join(testDir, 'target.ts'));
76
+ expect(result.isError).toBeUndefined();
77
+ expect(result.content[0].text).toContain('target.ts');
78
+ });
79
+ });
File without changes