@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 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
- Returns high-level stats: total files and the top 5 most-referenced files. Use this to identify the "core" modules of the application.
99
+ Get overview of project structure and most referenced files.
100
100
 
101
- #### `get_file_relationships`
102
- Zoom in on a specific file to see its context.
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
  }
File without changes
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(path.resolve(searchPath || '.'));
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
+ });
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.28",
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 && shx chmod +x dist/*.js",
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
- });