@gotza02/sequential-thinking 2026.1.28 → 2026.1.31
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 +35 -16
- 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 +128 -2
- 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/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 +3 -2
- package/dist/verify_new_tools.test.js +0 -67
package/README.md
CHANGED
|
@@ -96,23 +96,10 @@ Reads a webpage and converts it to clean Markdown, removing ads and navigation.
|
|
|
96
96
|
- `path` (string, optional): Root directory (defaults to `.`).
|
|
97
97
|
|
|
98
98
|
#### `get_project_graph_summary`
|
|
99
|
-
|
|
99
|
+
Get overview of project structure and most referenced files.
|
|
100
100
|
|
|
101
|
-
#### `
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
**Inputs:**
|
|
105
|
-
- `filePath` (string, required): Path to the file (e.g., `src/index.ts`).
|
|
106
|
-
**Returns:**
|
|
107
|
-
- `imports`: What this file needs.
|
|
108
|
-
- `importedBy`: Who relies on this file.
|
|
109
|
-
|
|
110
|
-
#### `search_code`
|
|
111
|
-
Searches for a text pattern across all code files in the project. Useful for finding usage examples or specific logic.
|
|
112
|
-
|
|
113
|
-
**Inputs:**
|
|
114
|
-
- `pattern` (string, required): Text to search for.
|
|
115
|
-
- `path` (string, optional): Root directory (defaults to `.`).
|
|
101
|
+
#### `get_project_graph_visualization`
|
|
102
|
+
Get a Mermaid Diagram string representing the project's dependency graph. You can render this string in a Markdown viewer that supports Mermaid.
|
|
116
103
|
|
|
117
104
|
### 🛠 System Operations
|
|
118
105
|
|
|
@@ -129,6 +116,26 @@ Creates or overwrites a file.
|
|
|
129
116
|
- `path` (string, required): File path.
|
|
130
117
|
- `content` (string, required): The full content to write.
|
|
131
118
|
|
|
119
|
+
#### `edit_file`
|
|
120
|
+
Replace a specific string in a file with a new string. Use this for surgical edits to avoid overwriting the whole file.
|
|
121
|
+
|
|
122
|
+
**Inputs:**
|
|
123
|
+
- `path` (string, required): File path.
|
|
124
|
+
- `oldText` (string, required): The exact text segment to replace.
|
|
125
|
+
- `newText` (string, required): The new text to insert.
|
|
126
|
+
- `allowMultiple` (boolean, optional): Allow replacing multiple occurrences (default: false).
|
|
127
|
+
|
|
128
|
+
#### `manage_notes`
|
|
129
|
+
Manage long-term memory/notes. Use this to save important information, rules, or learnings that should persist across sessions.
|
|
130
|
+
|
|
131
|
+
**Inputs:**
|
|
132
|
+
- `action` (enum, required): 'add', 'list', 'search', 'update', 'delete'.
|
|
133
|
+
- `title` (string): Title of the note.
|
|
134
|
+
- `content` (string): Content of the note.
|
|
135
|
+
- `tags` (array): Tags for categorization.
|
|
136
|
+
- `searchQuery` (string): Query to search notes.
|
|
137
|
+
- `noteId` (string): ID of the note.
|
|
138
|
+
|
|
132
139
|
#### `shell_execute`
|
|
133
140
|
Executes a shell command. Use for running tests (`npm test`), building (`npm run build`), or file operations (`ls`, `mkdir`).
|
|
134
141
|
|
|
@@ -222,6 +229,18 @@ npm run build
|
|
|
222
229
|
npm test
|
|
223
230
|
```
|
|
224
231
|
|
|
232
|
+
## Recent Updates (v2026.1.31)
|
|
233
|
+
- **New Tools**:
|
|
234
|
+
- `manage_notes`: Long-term memory system to store and retrieve information across sessions.
|
|
235
|
+
|
|
236
|
+
## Recent Updates (v2026.1.30)
|
|
237
|
+
- **New Tools**:
|
|
238
|
+
- `get_project_graph_visualization`: Generate Mermaid diagrams of your project structure.
|
|
239
|
+
|
|
240
|
+
## Recent Updates (v2026.1.29)
|
|
241
|
+
- **New Tools**:
|
|
242
|
+
- `edit_file`: Surgically replace text in files without overwriting the entire content.
|
|
243
|
+
|
|
225
244
|
## Recent Updates (v2026.1.28)
|
|
226
245
|
- **Robustness**:
|
|
227
246
|
- Implemented **Atomic Writes** for `thoughts_history.json` to prevent file corruption.
|
package/dist/graph.js
CHANGED
|
@@ -186,4 +186,29 @@ export class ProjectKnowledgeGraph {
|
|
|
186
186
|
}))
|
|
187
187
|
};
|
|
188
188
|
}
|
|
189
|
+
toMermaid() {
|
|
190
|
+
const lines = ['graph TD'];
|
|
191
|
+
const fileToId = new Map();
|
|
192
|
+
let idCounter = 0;
|
|
193
|
+
// Assign IDs
|
|
194
|
+
for (const [filePath, _] of this.nodes) {
|
|
195
|
+
const relative = path.relative(this.rootDir, filePath);
|
|
196
|
+
const id = `N${idCounter++}`;
|
|
197
|
+
fileToId.set(filePath, id);
|
|
198
|
+
// Escape quotes in label
|
|
199
|
+
const label = relative.replace(/"/g, "'");
|
|
200
|
+
lines.push(` ${id}["${label}"]`);
|
|
201
|
+
}
|
|
202
|
+
// Add Edges
|
|
203
|
+
for (const [filePath, node] of this.nodes) {
|
|
204
|
+
const sourceId = fileToId.get(filePath);
|
|
205
|
+
for (const importPath of node.imports) {
|
|
206
|
+
const targetId = fileToId.get(importPath);
|
|
207
|
+
if (sourceId && targetId) {
|
|
208
|
+
lines.push(` ${sourceId} --> ${targetId}`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return lines.join('\\n');
|
|
213
|
+
}
|
|
189
214
|
}
|
package/dist/graph.test.js
CHANGED
|
File without changes
|
package/dist/graph_repro.test.js
CHANGED
|
File without changes
|
package/dist/index.js
CHANGED
|
@@ -7,6 +7,7 @@ import * as fs from 'fs/promises';
|
|
|
7
7
|
import { exec } from 'child_process';
|
|
8
8
|
import { promisify } from 'util';
|
|
9
9
|
import { ProjectKnowledgeGraph } from './graph.js';
|
|
10
|
+
import { NotesManager } from './notes.js';
|
|
10
11
|
import * as path from 'path';
|
|
11
12
|
import { JSDOM } from 'jsdom';
|
|
12
13
|
import { Readability } from '@mozilla/readability';
|
|
@@ -50,6 +51,7 @@ const server = new McpServer({
|
|
|
50
51
|
});
|
|
51
52
|
const thinkingServer = new SequentialThinkingServer(process.env.THOUGHTS_STORAGE_PATH || 'thoughts_history.json', parseInt(process.env.THOUGHT_DELAY_MS || '0', 10));
|
|
52
53
|
const knowledgeGraph = new ProjectKnowledgeGraph();
|
|
54
|
+
const notesManager = new NotesManager(process.env.NOTES_STORAGE_PATH || 'project_notes.json');
|
|
53
55
|
// --- Sequential Thinking Tool ---
|
|
54
56
|
server.tool("sequentialthinking", `A detailed tool for dynamic and reflective problem-solving through thoughts.
|
|
55
57
|
This tool helps analyze problems through a flexible thinking process that can adapt and evolve.
|
|
@@ -137,7 +139,7 @@ You should:
|
|
|
137
139
|
// --- New Tools ---
|
|
138
140
|
// 1. web_search
|
|
139
141
|
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"),
|
|
142
|
+
query: z.string().min(1).describe("The search query"),
|
|
141
143
|
provider: z.enum(['brave', 'exa', 'google']).optional().describe("Preferred search provider")
|
|
142
144
|
}, async ({ query, provider }) => {
|
|
143
145
|
try {
|
|
@@ -385,6 +387,18 @@ server.tool("search_code", "Search for a text pattern in project files (excludes
|
|
|
385
387
|
path: z.string().optional().default('.').describe("Root directory to search")
|
|
386
388
|
}, async ({ pattern, path: searchPath }) => {
|
|
387
389
|
try {
|
|
390
|
+
const resolvedPath = path.resolve(searchPath || '.');
|
|
391
|
+
const stats = await fs.stat(resolvedPath);
|
|
392
|
+
if (stats.isFile()) {
|
|
393
|
+
const content = await fs.readFile(resolvedPath, 'utf-8');
|
|
394
|
+
const matches = content.includes(pattern) ? [resolvedPath] : [];
|
|
395
|
+
return {
|
|
396
|
+
content: [{
|
|
397
|
+
type: "text",
|
|
398
|
+
text: matches.length > 0 ? `Found "${pattern}" in:\n${matches.join('\n')}` : `No matches found for "${pattern}"`
|
|
399
|
+
}]
|
|
400
|
+
};
|
|
401
|
+
}
|
|
388
402
|
async function searchDir(dir) {
|
|
389
403
|
const results = [];
|
|
390
404
|
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
@@ -404,7 +418,7 @@ server.tool("search_code", "Search for a text pattern in project files (excludes
|
|
|
404
418
|
}
|
|
405
419
|
return results;
|
|
406
420
|
}
|
|
407
|
-
const matches = await searchDir(
|
|
421
|
+
const matches = await searchDir(resolvedPath);
|
|
408
422
|
return {
|
|
409
423
|
content: [{
|
|
410
424
|
type: "text",
|
|
@@ -445,6 +459,118 @@ server.tool("summarize_history", "Compress multiple thoughts into a single summa
|
|
|
445
459
|
};
|
|
446
460
|
}
|
|
447
461
|
});
|
|
462
|
+
// 13. edit_file
|
|
463
|
+
server.tool("edit_file", "Replace a specific string in a file with a new string. Use this for surgical edits to avoid overwriting the whole file.", {
|
|
464
|
+
path: z.string().describe("Path to the file"),
|
|
465
|
+
oldText: z.string().describe("The exact text segment to replace"),
|
|
466
|
+
newText: z.string().describe("The new text to insert"),
|
|
467
|
+
allowMultiple: z.boolean().optional().default(false).describe("Allow replacing multiple occurrences (default: false)")
|
|
468
|
+
}, async ({ path, oldText, newText, allowMultiple }) => {
|
|
469
|
+
try {
|
|
470
|
+
const content = await fs.readFile(path, 'utf-8');
|
|
471
|
+
// Check occurrences
|
|
472
|
+
// Escape special regex characters in oldText to treat it as literal string
|
|
473
|
+
const escapeRegExp = (string) => string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
474
|
+
const regex = new RegExp(escapeRegExp(oldText), 'g');
|
|
475
|
+
const matchCount = (content.match(regex) || []).length;
|
|
476
|
+
if (matchCount === 0) {
|
|
477
|
+
return {
|
|
478
|
+
content: [{ type: "text", text: "Error: 'oldText' not found in the file. Please ensure exact matching (including whitespace/indentation)." }],
|
|
479
|
+
isError: true
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
if (matchCount > 1 && !allowMultiple) {
|
|
483
|
+
return {
|
|
484
|
+
content: [{ type: "text", text: `Error: Found ${matchCount} occurrences of 'oldText'. Set 'allowMultiple' to true if you intend to replace all, or provide more unique context in 'oldText'.` }],
|
|
485
|
+
isError: true
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
const newContent = content.replace(allowMultiple ? regex : oldText, newText);
|
|
489
|
+
await fs.writeFile(path, newContent, 'utf-8');
|
|
490
|
+
return {
|
|
491
|
+
content: [{ type: "text", text: `Successfully replaced ${allowMultiple ? matchCount : 1} occurrence(s) in ${path}` }]
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
catch (error) {
|
|
495
|
+
return {
|
|
496
|
+
content: [{ type: "text", text: `Edit Error: ${error instanceof Error ? error.message : String(error)}` }],
|
|
497
|
+
isError: true
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
});
|
|
501
|
+
// 14. get_project_graph_visualization
|
|
502
|
+
server.tool("get_project_graph_visualization", "Get a Mermaid Diagram string representing the project's dependency graph. You can render this string in a Markdown viewer that supports Mermaid.", {}, async () => {
|
|
503
|
+
try {
|
|
504
|
+
const diagram = knowledgeGraph.toMermaid();
|
|
505
|
+
if (diagram.trim() === 'graph TD') {
|
|
506
|
+
return {
|
|
507
|
+
content: [{ type: "text", text: "Graph is empty. Please run 'build_project_graph' first." }],
|
|
508
|
+
isError: true
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
return {
|
|
512
|
+
content: [{ type: "text", text: diagram }]
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
catch (error) {
|
|
516
|
+
return {
|
|
517
|
+
content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}` }],
|
|
518
|
+
isError: true
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
});
|
|
522
|
+
// 15. manage_notes
|
|
523
|
+
server.tool("manage_notes", "Manage long-term memory/notes. Use this to save important information, rules, or learnings that should persist across sessions.", {
|
|
524
|
+
action: z.enum(['add', 'list', 'search', 'update', 'delete']).describe("Action to perform"),
|
|
525
|
+
title: z.string().optional().describe("Title of the note (for add/update)"),
|
|
526
|
+
content: z.string().optional().describe("Content of the note (for add/update)"),
|
|
527
|
+
tags: z.array(z.string()).optional().describe("Tags for categorization (for add/update)"),
|
|
528
|
+
searchQuery: z.string().optional().describe("Query to search notes (for search)"),
|
|
529
|
+
noteId: z.string().optional().describe("ID of the note (for update/delete)")
|
|
530
|
+
}, async ({ action, title, content, tags, searchQuery, noteId }) => {
|
|
531
|
+
try {
|
|
532
|
+
switch (action) {
|
|
533
|
+
case 'add':
|
|
534
|
+
if (!title || !content) {
|
|
535
|
+
return { content: [{ type: "text", text: "Error: 'title' and 'content' are required for add action." }], isError: true };
|
|
536
|
+
}
|
|
537
|
+
const newNote = await notesManager.addNote(title, content, tags);
|
|
538
|
+
return { content: [{ type: "text", text: `Note added successfully.\nID: ${newNote.id}` }] };
|
|
539
|
+
case 'list':
|
|
540
|
+
const notes = await notesManager.listNotes();
|
|
541
|
+
return { content: [{ type: "text", text: JSON.stringify(notes, null, 2) }] };
|
|
542
|
+
case 'search':
|
|
543
|
+
if (!searchQuery) {
|
|
544
|
+
return { content: [{ type: "text", text: "Error: 'searchQuery' is required for search action." }], isError: true };
|
|
545
|
+
}
|
|
546
|
+
const searchResults = await notesManager.searchNotes(searchQuery);
|
|
547
|
+
return { content: [{ type: "text", text: searchResults.length > 0 ? JSON.stringify(searchResults, null, 2) : "No matching notes found." }] };
|
|
548
|
+
case 'update':
|
|
549
|
+
if (!noteId) {
|
|
550
|
+
return { content: [{ type: "text", text: "Error: 'noteId' is required for update action." }], isError: true };
|
|
551
|
+
}
|
|
552
|
+
const updatedNote = await notesManager.updateNote(noteId, { title, content, tags });
|
|
553
|
+
if (!updatedNote) {
|
|
554
|
+
return { content: [{ type: "text", text: `Error: Note with ID ${noteId} not found.` }], isError: true };
|
|
555
|
+
}
|
|
556
|
+
return { content: [{ type: "text", text: `Note updated successfully.` }] };
|
|
557
|
+
case 'delete':
|
|
558
|
+
if (!noteId) {
|
|
559
|
+
return { content: [{ type: "text", text: "Error: 'noteId' is required for delete action." }], isError: true };
|
|
560
|
+
}
|
|
561
|
+
const deleted = await notesManager.deleteNote(noteId);
|
|
562
|
+
return { content: [{ type: "text", text: deleted ? "Note deleted successfully." : `Error: Note with ID ${noteId} not found.` }], isError: !deleted };
|
|
563
|
+
default:
|
|
564
|
+
return { content: [{ type: "text", text: `Error: Unknown action '${action}'` }], isError: true };
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
catch (error) {
|
|
568
|
+
return {
|
|
569
|
+
content: [{ type: "text", text: `Notes Error: ${error instanceof Error ? error.message : String(error)}` }],
|
|
570
|
+
isError: true
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
});
|
|
448
574
|
runServer().catch((error) => {
|
|
449
575
|
console.error("Fatal error running server:", error);
|
|
450
576
|
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
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import * as fs from 'fs/promises';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
// Replicating the logic from src/index.ts for testing
|
|
5
|
+
async function editFileLogic(path, oldText, newText, allowMultiple = false) {
|
|
6
|
+
try {
|
|
7
|
+
const content = await fs.readFile(path, 'utf-8');
|
|
8
|
+
const escapeRegExp = (string) => string.replace(/[.*+?^${}()|[\\]/g, '\\$&');
|
|
9
|
+
const regex = new RegExp(escapeRegExp(oldText), 'g');
|
|
10
|
+
const matchCount = (content.match(regex) || []).length;
|
|
11
|
+
if (matchCount === 0) {
|
|
12
|
+
return { error: "Error: 'oldText' not found" };
|
|
13
|
+
}
|
|
14
|
+
if (matchCount > 1 && !allowMultiple) {
|
|
15
|
+
return { error: `Error: Found ${matchCount} occurrences` };
|
|
16
|
+
}
|
|
17
|
+
const newContent = content.replace(allowMultiple ? regex : oldText, newText);
|
|
18
|
+
await fs.writeFile(path, newContent, 'utf-8');
|
|
19
|
+
return { success: true };
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
return { error: String(error) };
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
describe('edit_file logic', () => {
|
|
26
|
+
const testFile = path.join(__dirname, 'test_edit.txt');
|
|
27
|
+
beforeEach(async () => {
|
|
28
|
+
await fs.writeFile(testFile, 'Line 1\nTarget\nLine 3\nTarget again');
|
|
29
|
+
});
|
|
30
|
+
afterEach(async () => {
|
|
31
|
+
try {
|
|
32
|
+
await fs.unlink(testFile);
|
|
33
|
+
}
|
|
34
|
+
catch { }
|
|
35
|
+
});
|
|
36
|
+
it('should replace single occurrence', async () => {
|
|
37
|
+
await fs.writeFile(testFile, 'Line 1\nUnique\nLine 3');
|
|
38
|
+
const result = await editFileLogic(testFile, 'Unique', 'Replaced');
|
|
39
|
+
expect(result.error).toBeUndefined();
|
|
40
|
+
const content = await fs.readFile(testFile, 'utf-8');
|
|
41
|
+
expect(content).toContain('Line 1\nReplaced\nLine 3');
|
|
42
|
+
});
|
|
43
|
+
it('should fail if text not found', async () => {
|
|
44
|
+
const result = await editFileLogic(testFile, 'Missing', 'New');
|
|
45
|
+
expect(result.error).toContain("not found");
|
|
46
|
+
});
|
|
47
|
+
it('should fail if multiple found and allowMultiple=false', async () => {
|
|
48
|
+
const result = await editFileLogic(testFile, 'Target', 'New');
|
|
49
|
+
expect(result.error).toContain("Found 2 occurrences");
|
|
50
|
+
});
|
|
51
|
+
it('should replace multiple if allowMultiple=true', async () => {
|
|
52
|
+
const result = await editFileLogic(testFile, 'Target', 'New', true);
|
|
53
|
+
expect(result.error).toBeUndefined();
|
|
54
|
+
const content = await fs.readFile(testFile, 'utf-8');
|
|
55
|
+
expect(content).toBe('Line 1\nNew\nLine 3\nNew again');
|
|
56
|
+
});
|
|
57
|
+
it('should handle special regex characters in text', async () => {
|
|
58
|
+
await fs.writeFile(testFile, 'func(a, b) { return a+b; }');
|
|
59
|
+
const oldText = 'func(a, b) { return a+b; }';
|
|
60
|
+
const newText = 'replacement';
|
|
61
|
+
const result = await editFileLogic(testFile, oldText, newText);
|
|
62
|
+
expect(result.error).toBeUndefined();
|
|
63
|
+
const content = await fs.readFile(testFile, 'utf-8');
|
|
64
|
+
expect(content).toBe('replacement');
|
|
65
|
+
});
|
|
66
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { NotesManager } from './notes.js';
|
|
3
|
+
import * as fs from 'fs/promises';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
describe('Notes Manager', () => {
|
|
6
|
+
const testFile = path.join(__dirname, 'test_notes.json');
|
|
7
|
+
let manager;
|
|
8
|
+
beforeEach(async () => {
|
|
9
|
+
manager = new NotesManager(testFile);
|
|
10
|
+
});
|
|
11
|
+
afterEach(async () => {
|
|
12
|
+
try {
|
|
13
|
+
await fs.unlink(testFile);
|
|
14
|
+
}
|
|
15
|
+
catch { }
|
|
16
|
+
});
|
|
17
|
+
it('should add and list notes', async () => {
|
|
18
|
+
await manager.addNote("My Note", "Content", ["tag1"]);
|
|
19
|
+
const notes = await manager.listNotes();
|
|
20
|
+
expect(notes.length).toBe(1);
|
|
21
|
+
expect(notes[0].title).toBe("My Note");
|
|
22
|
+
});
|
|
23
|
+
it('should search notes', async () => {
|
|
24
|
+
await manager.addNote("React Tips", "Use hooks", ["react"]);
|
|
25
|
+
await manager.addNote("Vue Tips", "Use composition", ["vue"]);
|
|
26
|
+
const results = await manager.searchNotes("hooks");
|
|
27
|
+
expect(results.length).toBe(1);
|
|
28
|
+
expect(results[0].title).toBe("React Tips");
|
|
29
|
+
});
|
|
30
|
+
it('should delete note', async () => {
|
|
31
|
+
const note = await manager.addNote("To Delete", "...");
|
|
32
|
+
await manager.deleteNote(note.id);
|
|
33
|
+
const notes = await manager.listNotes();
|
|
34
|
+
expect(notes.length).toBe(0);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { ProjectKnowledgeGraph } from './graph.js';
|
|
3
|
+
import * as fs from 'fs/promises';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
describe('Graph Visualization', () => {
|
|
6
|
+
const testDir = path.join(__dirname, 'test_viz_env');
|
|
7
|
+
const graph = new ProjectKnowledgeGraph();
|
|
8
|
+
beforeEach(async () => {
|
|
9
|
+
await fs.mkdir(testDir, { recursive: true });
|
|
10
|
+
await fs.writeFile(path.join(testDir, 'a.ts'), 'import { b } from "./b";');
|
|
11
|
+
await fs.writeFile(path.join(testDir, 'b.ts'), 'export const b = 1;');
|
|
12
|
+
});
|
|
13
|
+
afterEach(async () => {
|
|
14
|
+
await fs.rm(testDir, { recursive: true, force: true });
|
|
15
|
+
});
|
|
16
|
+
it('should generate mermaid diagram', async () => {
|
|
17
|
+
await graph.build(testDir);
|
|
18
|
+
const mermaid = graph.toMermaid();
|
|
19
|
+
console.log("Generated Mermaid:", mermaid);
|
|
20
|
+
expect(mermaid).toContain('graph TD');
|
|
21
|
+
expect(mermaid).toMatch(/N\d+\["a\.ts"\]/);
|
|
22
|
+
expect(mermaid).toMatch(/N\d+\["b\.ts"\]/);
|
|
23
|
+
expect(mermaid).toContain('-->');
|
|
24
|
+
});
|
|
25
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gotza02/sequential-thinking",
|
|
3
|
-
"version": "2026.1.
|
|
3
|
+
"version": "2026.1.31",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -22,7 +22,8 @@
|
|
|
22
22
|
"dist"
|
|
23
23
|
],
|
|
24
24
|
"scripts": {
|
|
25
|
-
"build": "tsc
|
|
25
|
+
"build": "tsc",
|
|
26
|
+
"start": "node dist/index.js",
|
|
26
27
|
"prepare": "npm run build",
|
|
27
28
|
"watch": "tsc --watch",
|
|
28
29
|
"test": "vitest run --coverage"
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
-
import * as fs from 'fs/promises';
|
|
3
|
-
import * as path from 'path';
|
|
4
|
-
import { SequentialThinkingServer } from './lib';
|
|
5
|
-
// Mock dependencies for read_webpage if needed,
|
|
6
|
-
// but for now let's test the logic we can control.
|
|
7
|
-
describe('New Tools Verification', () => {
|
|
8
|
-
const testDir = path.resolve('test_sandbox');
|
|
9
|
-
beforeEach(async () => {
|
|
10
|
-
await fs.mkdir(testDir, { recursive: true });
|
|
11
|
-
});
|
|
12
|
-
afterEach(async () => {
|
|
13
|
-
await fs.rm(testDir, { recursive: true, force: true });
|
|
14
|
-
});
|
|
15
|
-
it('search_code should find patterns and ignore node_modules', async () => {
|
|
16
|
-
// Setup files
|
|
17
|
-
await fs.writeFile(path.join(testDir, 'target.ts'), 'const x = "FIND_ME";');
|
|
18
|
-
await fs.writeFile(path.join(testDir, 'other.ts'), 'const y = "nope";');
|
|
19
|
-
const modulesDir = path.join(testDir, 'node_modules');
|
|
20
|
-
await fs.mkdir(modulesDir);
|
|
21
|
-
await fs.writeFile(path.join(modulesDir, 'ignored.ts'), 'const z = "FIND_ME";');
|
|
22
|
-
// Logic from search_code (replicated here for unit testing the logic itself,
|
|
23
|
-
// effectively testing the implementation I wrote in index.ts)
|
|
24
|
-
async function searchDir(dir, pattern) {
|
|
25
|
-
const results = [];
|
|
26
|
-
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
27
|
-
for (const entry of entries) {
|
|
28
|
-
const fullPath = path.join(dir, entry.name);
|
|
29
|
-
if (entry.isDirectory()) {
|
|
30
|
-
if (['node_modules', '.git', 'dist', 'coverage', '.gemini'].includes(entry.name))
|
|
31
|
-
continue;
|
|
32
|
-
results.push(...await searchDir(fullPath, pattern));
|
|
33
|
-
}
|
|
34
|
-
else if (/\.(ts|js|json|md|txt|html|css|py|java|c|cpp|h|rs|go)$/.test(entry.name)) {
|
|
35
|
-
const content = await fs.readFile(fullPath, 'utf-8');
|
|
36
|
-
if (content.includes(pattern)) {
|
|
37
|
-
results.push(fullPath);
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
return results;
|
|
42
|
-
}
|
|
43
|
-
const results = await searchDir(testDir, 'FIND_ME');
|
|
44
|
-
expect(results).toHaveLength(1);
|
|
45
|
-
expect(results[0]).toContain('target.ts');
|
|
46
|
-
expect(results[0]).not.toContain('node_modules');
|
|
47
|
-
});
|
|
48
|
-
it('SequentialThinkingServer should clear history', async () => {
|
|
49
|
-
const historyFile = path.join(testDir, 'test_history.json');
|
|
50
|
-
const server = new SequentialThinkingServer(historyFile);
|
|
51
|
-
// Add a thought
|
|
52
|
-
await server.processThought({
|
|
53
|
-
thought: "Test thought",
|
|
54
|
-
thoughtNumber: 1,
|
|
55
|
-
totalThoughts: 1,
|
|
56
|
-
nextThoughtNeeded: false
|
|
57
|
-
});
|
|
58
|
-
// Verify it was written
|
|
59
|
-
const contentBefore = JSON.parse(await fs.readFile(historyFile, 'utf-8'));
|
|
60
|
-
expect(contentBefore).toHaveLength(1);
|
|
61
|
-
// Clear history
|
|
62
|
-
await server.clearHistory();
|
|
63
|
-
// Verify it is empty
|
|
64
|
-
const contentAfter = JSON.parse(await fs.readFile(historyFile, 'utf-8'));
|
|
65
|
-
expect(contentAfter).toHaveLength(0);
|
|
66
|
-
});
|
|
67
|
-
});
|