@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/README.md +118 -176
- package/dist/graph.js +25 -0
- package/dist/graph.test.js +0 -0
- package/dist/graph_repro.test.js +0 -0
- package/dist/index.js +13 -431
- package/dist/lib.js +0 -0
- package/dist/notes.js +77 -0
- package/dist/repro_search.test.js +79 -0
- package/dist/server.test.js +0 -0
- package/dist/tools/filesystem.js +152 -0
- package/dist/tools/graph.js +79 -0
- package/dist/tools/notes.js +55 -0
- package/dist/tools/thinking.js +113 -0
- package/dist/tools/web.js +134 -0
- package/dist/utils.js +35 -0
- package/dist/verify_edit.test.js +66 -0
- package/dist/verify_notes.test.js +36 -0
- package/dist/verify_viz.test.js +25 -0
- package/package.json +4 -3
- package/dist/verify_new_tools.test.js +0 -67
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
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
+
});
|
package/dist/server.test.js
CHANGED
|
File without changes
|